View Javadoc
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   * | &lt;an | Xml | Element/&gt; |
38   * +-----+-----+-----------+
39   * </pre>
40   * <p/>
41   * this decoder would output a single frame:
42   * <p/>
43   * <pre>
44   * +-----------------+
45   * | &lt;anXmlElement/&gt; |
46   * +-----------------+
47   * </pre>
48   *
49   * Given an input array of bytes split over 5 frames like this:
50   * <pre>
51   * +-----+-----+-----------+-----+----------------------------------+
52   * | &lt;an | Xml | Element/&gt; | &lt;ro | ot&gt;&lt;child&gt;content&lt;/child&gt;&lt;/root&gt; |
53   * +-----+-----+-----------+-----+----------------------------------+
54   * </pre>
55   * <p/>
56   * this decoder would output two frames:
57   * <p/>
58   * <pre>
59   * +-----------------+-------------------------------------+
60   * | &lt;anXmlElement/&gt; | &lt;root&gt;&lt;child&gt;content&lt;/child&gt;&lt;/root&gt; |
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 }