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 }