View Javadoc
1   /*
2    * Copyright 2012 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;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.util.ByteProcessor;
21  
22  import java.util.List;
23  
24  /**
25   * A decoder that splits the received {@link ByteBuf}s on line endings.
26   * <p>
27   * Both {@code "\n"} and {@code "\r\n"} are handled.
28   * <p>
29   * The byte stream is expected to be in UTF-8 character encoding or ASCII. The current implementation
30   * uses direct {@code byte} to {@code char} cast and then compares that {@code char} to a few low range
31   * ASCII characters like {@code '\n'} or {@code '\r'}. UTF-8 is not using low range [0..0x7F]
32   * byte values for multibyte codepoint representations therefore fully supported by this implementation.
33   * <p>
34   * For a more general delimiter-based decoder, see {@link DelimiterBasedFrameDecoder}.
35   * <p>
36   * Users should be aware that used as is, the lenient approach on lone {@code '\n} might result on a parser
37   * diffenrencial on line based protocols requiring the use of {@code "\r\n"} delimiters like SMTP and can
38   * result in attacks similar to
39   * <a href="https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/">SMTP smuggling</a>.
40   * Validating afterward the end of line pattern can be a possible mitigation.
41   */
42  public class LineBasedFrameDecoder extends ByteToMessageDecoder {
43  
44      /** Maximum length of a frame we're willing to decode.  */
45      private final int maxLength;
46      /** Whether or not to throw an exception as soon as we exceed maxLength. */
47      private final boolean failFast;
48      private final boolean stripDelimiter;
49  
50      /** True if we're discarding input because we're already over maxLength.  */
51      private boolean discarding;
52      private int discardedBytes;
53  
54      /** Last scan position. */
55      private int offset;
56  
57      /**
58       * Creates a new decoder.
59       * @param maxLength  the maximum length of the decoded frame.
60       *                   A {@link TooLongFrameException} is thrown if
61       *                   the length of the frame exceeds this value.
62       */
63      public LineBasedFrameDecoder(final int maxLength) {
64          this(maxLength, true, false);
65      }
66  
67      /**
68       * Creates a new decoder.
69       * @param maxLength  the maximum length of the decoded frame.
70       *                   A {@link TooLongFrameException} is thrown if
71       *                   the length of the frame exceeds this value.
72       * @param stripDelimiter  whether the decoded frame should strip out the
73       *                        delimiter or not
74       * @param failFast  If <tt>true</tt>, a {@link TooLongFrameException} is
75       *                  thrown as soon as the decoder notices the length of the
76       *                  frame will exceed <tt>maxFrameLength</tt> regardless of
77       *                  whether the entire frame has been read.
78       *                  If <tt>false</tt>, a {@link TooLongFrameException} is
79       *                  thrown after the entire frame that exceeds
80       *                  <tt>maxFrameLength</tt> has been read.
81       */
82      public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
83          this.maxLength = maxLength;
84          this.failFast = failFast;
85          this.stripDelimiter = stripDelimiter;
86      }
87  
88      @Override
89      protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
90          Object decoded = decode(ctx, in);
91          if (decoded != null) {
92              out.add(decoded);
93          }
94      }
95  
96      /**
97       * Create a frame out of the {@link ByteBuf} and return it.
98       *
99       * @param   ctx             the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
100      * @param   buffer          the {@link ByteBuf} from which to read data
101      * @return  frame           the {@link ByteBuf} which represent the frame or {@code null} if no frame could
102      *                          be created.
103      */
104     protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
105         final int eol = findEndOfLine(buffer);
106         if (!discarding) {
107             if (eol >= 0) {
108                 final ByteBuf frame;
109                 final int length = eol - buffer.readerIndex();
110                 final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
111 
112                 if (length > maxLength) {
113                     buffer.readerIndex(eol + delimLength);
114                     fail(ctx, length);
115                     return null;
116                 }
117 
118                 if (stripDelimiter) {
119                     frame = buffer.readRetainedSlice(length);
120                     buffer.skipBytes(delimLength);
121                 } else {
122                     frame = buffer.readRetainedSlice(length + delimLength);
123                 }
124 
125                 return frame;
126             } else {
127                 final int length = buffer.readableBytes();
128                 if (length > maxLength) {
129                     discardedBytes = length;
130                     buffer.readerIndex(buffer.writerIndex());
131                     discarding = true;
132                     offset = 0;
133                     if (failFast) {
134                         fail(ctx, "over " + discardedBytes);
135                     }
136                 }
137                 return null;
138             }
139         } else {
140             if (eol >= 0) {
141                 final int length = discardedBytes + eol - buffer.readerIndex();
142                 final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
143                 buffer.readerIndex(eol + delimLength);
144                 discardedBytes = 0;
145                 discarding = false;
146                 if (!failFast) {
147                     fail(ctx, length);
148                 }
149             } else {
150                 discardedBytes += buffer.readableBytes();
151                 buffer.readerIndex(buffer.writerIndex());
152                 // We skip everything in the buffer, we need to set the offset to 0 again.
153                 offset = 0;
154             }
155             return null;
156         }
157     }
158 
159     private void fail(final ChannelHandlerContext ctx, int length) {
160         fail(ctx, String.valueOf(length));
161     }
162 
163     private void fail(final ChannelHandlerContext ctx, String length) {
164         ctx.fireExceptionCaught(
165                 new TooLongFrameException(
166                         "frame length (" + length + ") exceeds the allowed maximum (" + maxLength + ')'));
167     }
168 
169     /**
170      * Returns the index in the buffer of the end of line found.
171      * Returns -1 if no end of line was found in the buffer.
172      */
173     private int findEndOfLine(final ByteBuf buffer) {
174         int totalLength = buffer.readableBytes();
175         int i = buffer.indexOf(buffer.readerIndex() + offset,
176                                buffer.readerIndex() + totalLength, (byte) '\n');
177         if (i >= 0) {
178             offset = 0;
179             if (i > 0 && buffer.getByte(i - 1) == '\r') {
180                 i--;
181             }
182         } else {
183             offset = totalLength;
184         }
185         return i;
186     }
187 }