1 /*
2 * Copyright 2013 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.handler.codec.xml;
17
18 import static io.netty.util.internal.ObjectUtil.checkPositive;
19
20 import io.netty.buffer.ByteBuf;
21 import io.netty.channel.ChannelHandlerContext;
22 import io.netty.handler.codec.ByteToMessageDecoder;
23 import io.netty.handler.codec.CorruptedFrameException;
24 import io.netty.handler.codec.TooLongFrameException;
25
26 import java.util.List;
27
28 /**
29 * A frame decoder for single separate XML based message streams.
30 * <p/>
31 * A couple examples will better help illustrate
32 * what this decoder actually does.
33 * <p/>
34 * Given an input array of bytes split over 3 frames like this:
35 * <pre>
36 * +-----+-----+-----------+
37 * | <an | Xml | Element/> |
38 * +-----+-----+-----------+
39 * </pre>
40 * <p/>
41 * this decoder would output a single frame:
42 * <p/>
43 * <pre>
44 * +-----------------+
45 * | <anXmlElement/> |
46 * +-----------------+
47 * </pre>
48 *
49 * Given an input array of bytes split over 5 frames like this:
50 * <pre>
51 * +-----+-----+-----------+-----+----------------------------------+
52 * | <an | Xml | Element/> | <ro | ot><child>content</child></root> |
53 * +-----+-----+-----------+-----+----------------------------------+
54 * </pre>
55 * <p/>
56 * this decoder would output two frames:
57 * <p/>
58 * <pre>
59 * +-----------------+-------------------------------------+
60 * | <anXmlElement/> | <root><child>content</child></root> |
61 * +-----------------+-------------------------------------+
62 * </pre>
63 *
64 * <p/>
65 * The byte stream is expected to be in UTF-8 character encoding or ASCII. The current implementation
66 * uses direct {@code byte} to {@code char} cast and then compares that {@code char} to a few low range
67 * ASCII characters like {@code '<'}, {@code '>'} or {@code '/'}. UTF-8 is not using low range [0..0x7F]
68 * byte values for multibyte codepoint representations therefore fully supported by this implementation.
69 * <p/>
70 * Please note that this decoder is not suitable for
71 * xml streaming protocols such as
72 * <a href="https://xmpp.org/rfcs/rfc6120.html">XMPP</a>,
73 * where an initial xml element opens the stream and only
74 * gets closed at the end of the session, although this class
75 * could probably allow for such type of message flow with
76 * minor modifications.
77 */
78 public class XmlFrameDecoder extends ByteToMessageDecoder {
79
80 private final int maxFrameLength;
81
82 public XmlFrameDecoder(int maxFrameLength) {
83 this.maxFrameLength = checkPositive(maxFrameLength, "maxFrameLength");
84 }
85
86 @Override
87 protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
88 boolean openingBracketFound = false;
89 boolean atLeastOneXmlElementFound = false;
90 boolean inCDATASection = false;
91 boolean inCommentBlock = false;
92 boolean inProcessingInstruction = false;
93 boolean inClosingTag = false;
94 long openBracketsCount = 0;
95 int length = 0;
96 int leadingWhiteSpaceCount = 0;
97 final int bufferLength = in.writerIndex();
98
99 if (bufferLength > maxFrameLength) {
100 // bufferLength exceeded maxFrameLength; dropping frame
101 in.skipBytes(in.readableBytes());
102 fail(bufferLength);
103 return;
104 }
105
106 for (int i = in.readerIndex(); i < bufferLength; i++) {
107 final byte readByte = in.getByte(i);
108 if (!openingBracketFound && Character.isWhitespace(readByte)) {
109 // xml has not started and whitespace char found
110 leadingWhiteSpaceCount++;
111 } else if (!openingBracketFound && readByte != '<') {
112 // garbage found before xml start
113 fail(ctx);
114 in.skipBytes(in.readableBytes());
115 return;
116 } else if (inClosingTag && readByte == '<') {
117 fail(ctx);
118 in.skipBytes(in.readableBytes());
119 return;
120 } else if (!inCDATASection && !inCommentBlock && !inProcessingInstruction && readByte == '<') {
121 openingBracketFound = true;
122
123 if (i < bufferLength - 1) {
124 final byte peekAheadByte = in.getByte(i + 1);
125 if (peekAheadByte == '/') {
126 // found </, we must check if it is enclosed
127 inClosingTag = true;
128 } else if (isValidStartCharForXmlElement(peekAheadByte)) {
129 atLeastOneXmlElementFound = true;
130 // char after < is a valid xml element start char,
131 // incrementing openBracketsCount
132 openBracketsCount++;
133 } else if (peekAheadByte == '!') {
134 if (isCommentBlockStart(in, i)) {
135 // <!-- comment --> start found
136 openBracketsCount++;
137 inCommentBlock = true;
138 } else if (isCDATABlockStart(in, i)) {
139 // <![CDATA[ start found
140 openBracketsCount++;
141 inCDATASection = true;
142 }
143 } else if (peekAheadByte == '?') {
144 // <?xml ?> start found
145 openBracketsCount++;
146 inProcessingInstruction = true;
147 }
148 }
149 } else if (!inCDATASection && !inCommentBlock && !inProcessingInstruction && readByte == '/') {
150 if (i < bufferLength - 1 && in.getByte(i + 1) == '>') {
151 // found />, decrementing openBracketsCount
152 openBracketsCount--;
153 }
154 } else if (readByte == '>') {
155 length = i + 1;
156
157 if (i - 1 > -1) {
158 final byte peekBehindByte = in.getByte(i - 1);
159
160 if (inCommentBlock) {
161 if (peekBehindByte == '-' && i - 2 > -1 && in.getByte(i - 2) == '-') {
162 // a <!-- comment --> was closed
163 openBracketsCount--;
164 inCommentBlock = false;
165 }
166 } else if (inProcessingInstruction) {
167 if (peekBehindByte == '?') {
168 // an <?xml ?> tag was closed
169 openBracketsCount--;
170 inProcessingInstruction = false;
171 }
172 } else if (inClosingTag) {
173 openBracketsCount--;
174 inClosingTag = false;
175 } else if (!inCDATASection) {
176 if (peekBehindByte == '?') {
177 // an <?xml ?> tag was closed
178 openBracketsCount--;
179 } else if (peekBehindByte == '-' && i - 2 > -1 && in.getByte(i - 2) == '-') {
180 // a <!-- comment --> was closed
181 openBracketsCount--;
182 }
183 } else if (inCDATASection && peekBehindByte == ']' && i - 2 > -1 && in.getByte(i - 2) == ']') {
184 // a <![CDATA[...]]> block was closed
185 openBracketsCount--;
186 inCDATASection = false;
187 }
188 }
189
190 if (atLeastOneXmlElementFound && openBracketsCount == 0) {
191 // xml is balanced, bailing out
192 break;
193 }
194 }
195 }
196
197 final int readerIndex = in.readerIndex();
198 int xmlElementLength = length - readerIndex;
199
200 if (openBracketsCount == 0 && xmlElementLength > 0) {
201 if (readerIndex + xmlElementLength >= bufferLength) {
202 xmlElementLength = in.readableBytes();
203 }
204 final ByteBuf frame =
205 extractFrame(in, readerIndex + leadingWhiteSpaceCount, xmlElementLength - leadingWhiteSpaceCount);
206 in.skipBytes(xmlElementLength);
207 out.add(frame);
208 }
209 }
210
211 private void fail(long frameLength) {
212 if (frameLength > 0) {
213 throw new TooLongFrameException(
214 "frame length exceeds " + maxFrameLength + ": " + frameLength + " - discarded");
215 } else {
216 throw new TooLongFrameException(
217 "frame length exceeds " + maxFrameLength + " - discarding");
218 }
219 }
220
221 private static void fail(ChannelHandlerContext ctx) {
222 ctx.fireExceptionCaught(new CorruptedFrameException("frame contains content before the xml starts"));
223 }
224
225 private static ByteBuf extractFrame(ByteBuf buffer, int index, int length) {
226 return buffer.copy(index, length);
227 }
228
229 /**
230 * Asks whether the given byte is a valid
231 * start char for an xml element name.
232 * <p/>
233 * Please refer to the
234 * <a href="https://www.w3.org/TR/2004/REC-xml11-20040204/#NT-NameStartChar">NameStartChar</a>
235 * formal definition in the W3C XML spec for further info.
236 *
237 * @param b the input char
238 * @return true if the char is a valid start char
239 */
240 private static boolean isValidStartCharForXmlElement(final byte b) {
241 return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b == ':' || b == '_';
242 }
243
244 private static boolean isCommentBlockStart(final ByteBuf in, final int i) {
245 return i < in.writerIndex() - 3
246 && in.getByte(i + 2) == '-'
247 && in.getByte(i + 3) == '-';
248 }
249
250 private static boolean isCDATABlockStart(final ByteBuf in, final int i) {
251 return i < in.writerIndex() - 8
252 && in.getByte(i + 2) == '['
253 && in.getByte(i + 3) == 'C'
254 && in.getByte(i + 4) == 'D'
255 && in.getByte(i + 5) == 'A'
256 && in.getByte(i + 6) == 'T'
257 && in.getByte(i + 7) == 'A'
258 && in.getByte(i + 8) == '[';
259 }
260
261 }