View Javadoc
1   /*
2    * Copyright 2014 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.haproxy;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.handler.codec.ByteToMessageDecoder;
21  import io.netty.handler.codec.ProtocolDetectionResult;
22  import io.netty.util.CharsetUtil;
23  
24  import java.util.List;
25  
26  import static io.netty.handler.codec.haproxy.HAProxyConstants.*;
27  
28  /**
29   * Decodes an HAProxy proxy protocol header
30   *
31   * @see <a href="https://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt">Proxy Protocol Specification</a>
32   */
33  public class HAProxyMessageDecoder extends ByteToMessageDecoder {
34      /**
35       * Maximum possible length of a v1 proxy protocol header per spec
36       */
37      private static final int V1_MAX_LENGTH = 108;
38  
39      /**
40       * Maximum possible length of a v2 proxy protocol header (fixed 16 bytes + max unsigned short)
41       */
42      private static final int V2_MAX_LENGTH = 16 + 65535;
43  
44      /**
45       * Minimum possible length of a fully functioning v2 proxy protocol header (fixed 16 bytes + v2 address info space)
46       */
47      private static final int V2_MIN_LENGTH = 16 + 216;
48  
49      /**
50       * Maximum possible length for v2 additional TLV data (max unsigned short - max v2 address info space)
51       */
52      private static final int V2_MAX_TLV = 65535 - 216;
53  
54      /**
55       * Binary header prefix length
56       */
57      private static final int BINARY_PREFIX_LENGTH = BINARY_PREFIX.length;
58  
59      /**
60       * {@link ProtocolDetectionResult} for {@link HAProxyProtocolVersion#V1}.
61       */
62      private static final ProtocolDetectionResult<HAProxyProtocolVersion> DETECTION_RESULT_V1 =
63              ProtocolDetectionResult.detected(HAProxyProtocolVersion.V1);
64  
65      /**
66       * {@link ProtocolDetectionResult} for {@link HAProxyProtocolVersion#V2}.
67       */
68      private static final ProtocolDetectionResult<HAProxyProtocolVersion> DETECTION_RESULT_V2 =
69              ProtocolDetectionResult.detected(HAProxyProtocolVersion.V2);
70  
71      /**
72       * Used to extract a header frame out of the {@link ByteBuf} and return it.
73       */
74      private HeaderExtractor headerExtractor;
75  
76      /**
77       * {@code true} if we're discarding input because we're already over maxLength
78       */
79      private boolean discarding;
80  
81      /**
82       * Number of discarded bytes
83       */
84      private int discardedBytes;
85  
86      /**
87       * Whether or not to throw an exception as soon as we exceed maxLength.
88       */
89      private final boolean failFast;
90  
91      /**
92       * {@code true} if we're finished decoding the proxy protocol header
93       */
94      private boolean finished;
95  
96      /**
97       * Protocol specification version
98       */
99      private int version = -1;
100 
101     /**
102      * The latest v2 spec (2014/05/18) allows for additional data to be sent in the proxy protocol header beyond the
103      * address information block so now we need a configurable max header size
104      */
105     private final int v2MaxHeaderSize;
106 
107     /**
108      * Creates a new decoder with no additional data (TLV) restrictions, and should throw an exception as soon as
109      * we exceed maxLength.
110      */
111     public HAProxyMessageDecoder() {
112         this(true);
113     }
114 
115     /**
116      * Creates a new decoder with no additional data (TLV) restrictions, whether or not to throw an exception as soon
117      * as we exceed maxLength.
118      *
119      * @param failFast Whether or not to throw an exception as soon as we exceed maxLength
120      */
121     public HAProxyMessageDecoder(boolean failFast) {
122         v2MaxHeaderSize = V2_MAX_LENGTH;
123         this.failFast = failFast;
124     }
125 
126     /**
127      * Creates a new decoder with restricted additional data (TLV) size, and should throw an exception as soon as
128      * we exceed maxLength.
129      * <p>
130      * <b>Note:</b> limiting TLV size only affects processing of v2, binary headers. Also, as allowed by the 1.5 spec
131      * TLV data is currently ignored. For maximum performance it would be best to configure your upstream proxy host to
132      * <b>NOT</b> send TLV data and instantiate with a max TLV size of {@code 0}.
133      * </p>
134      *
135      * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header
136      */
137     public HAProxyMessageDecoder(int maxTlvSize) {
138         this(maxTlvSize, true);
139     }
140 
141     /**
142      * Creates a new decoder with restricted additional data (TLV) size, whether or not to throw an exception as soon
143      * as we exceed maxLength.
144      *
145      * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header
146      * @param failFast Whether or not to throw an exception as soon as we exceed maxLength
147      */
148     public HAProxyMessageDecoder(int maxTlvSize, boolean failFast) {
149         if (maxTlvSize < 1) {
150             v2MaxHeaderSize = V2_MIN_LENGTH;
151         } else if (maxTlvSize > V2_MAX_TLV) {
152             v2MaxHeaderSize = V2_MAX_LENGTH;
153         } else {
154             int calcMax = maxTlvSize + V2_MIN_LENGTH;
155             if (calcMax > V2_MAX_LENGTH) {
156                 v2MaxHeaderSize = V2_MAX_LENGTH;
157             } else {
158                 v2MaxHeaderSize = calcMax;
159             }
160         }
161         this.failFast = failFast;
162     }
163 
164     /**
165      * Returns the proxy protocol specification version in the buffer if the version is found.
166      * Returns -1 if no version was found in the buffer.
167      */
168     private static int findVersion(final ByteBuf buffer) {
169         final int n = buffer.readableBytes();
170         // per spec, the version number is found in the 13th byte
171         if (n < 13) {
172             return -1;
173         }
174 
175         int idx = buffer.readerIndex();
176         return match(BINARY_PREFIX, buffer, idx) ? buffer.getUnsignedByte(idx + BINARY_PREFIX_LENGTH) : 1;
177     }
178 
179     /**
180      * Returns the index in the buffer of the end of header if found.
181      * Returns -1 if no end of header was found in the buffer.
182      */
183     private static int findEndOfHeader(final ByteBuf buffer) {
184         final int n = buffer.readableBytes();
185 
186         // per spec, the 15th and 16th bytes contain the address length in bytes
187         if (n < 16) {
188             return -1;
189         }
190 
191         int offset = buffer.readerIndex() + 14;
192 
193         // the total header length will be a fixed 16 byte sequence + the dynamic address information block
194         int totalHeaderBytes = 16 + buffer.getUnsignedShort(offset);
195 
196         // ensure we actually have the full header available
197         if (n >= totalHeaderBytes) {
198             return buffer.readerIndex() + totalHeaderBytes;
199         } else {
200             return -1;
201         }
202     }
203 
204     /**
205      * Returns the index in the buffer of the end of line found.
206      * Returns -1 if no end of line was found in the buffer.
207      */
208     private static int findEndOfLine(final ByteBuf buffer) {
209         final int n = buffer.writerIndex();
210         for (int i = buffer.readerIndex(); i < n; i++) {
211             final byte b = buffer.getByte(i);
212             if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
213                 return i;  // \r\n
214             }
215         }
216         return -1;  // Not found.
217     }
218 
219     @Override
220     public boolean isSingleDecode() {
221         // ByteToMessageDecoder uses this method to optionally break out of the decoding loop after each unit of work.
222         // Since we only ever want to decode a single header we always return true to save a bit of work here.
223         return true;
224     }
225 
226     @Override
227     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
228         super.channelRead(ctx, msg);
229         if (finished) {
230             ctx.pipeline().remove(this);
231         }
232     }
233 
234     @Override
235     protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
236         if (finished) {
237             // The header was already decoded, or decoding failed and the channel is closed. Do not decode the
238             // remaining bytes as another header.
239             return;
240         }
241 
242         // determine the specification version
243         if (version == -1) {
244             if ((version = findVersion(in)) == -1) {
245                 return;
246             }
247         }
248 
249         ByteBuf decoded;
250 
251         if (version == 1) {
252             decoded = decodeLine(ctx, in);
253         } else {
254             decoded = decodeStruct(ctx, in);
255         }
256 
257         if (decoded != null) {
258             finished = true;
259             try {
260                 if (version == 1) {
261                     out.add(HAProxyMessage.decodeHeader(decoded.toString(CharsetUtil.US_ASCII)));
262                 } else {
263                     out.add(HAProxyMessage.decodeHeader(decoded));
264                 }
265             } catch (HAProxyProtocolException e) {
266                 fail(ctx, null, e);
267             }
268         }
269     }
270 
271     /**
272      * Create a frame out of the {@link ByteBuf} and return it.
273      *
274      * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
275      * @param buffer  the {@link ByteBuf} from which to read data
276      * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
277      *                be created
278      */
279     private ByteBuf decodeStruct(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
280         if (headerExtractor == null) {
281             headerExtractor = new StructHeaderExtractor(v2MaxHeaderSize);
282         }
283         return headerExtractor.extract(ctx, buffer);
284     }
285 
286     /**
287      * Create a frame out of the {@link ByteBuf} and return it.
288      *
289      * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
290      * @param buffer  the {@link ByteBuf} from which to read data
291      * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
292      *                be created
293      */
294     private ByteBuf decodeLine(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
295         if (headerExtractor == null) {
296             headerExtractor = new LineHeaderExtractor(V1_MAX_LENGTH);
297         }
298         return headerExtractor.extract(ctx, buffer);
299     }
300 
301     private void failOverLimit(final ChannelHandlerContext ctx, int length) {
302         failOverLimit(ctx, String.valueOf(length));
303     }
304 
305     private void failOverLimit(final ChannelHandlerContext ctx, String length) {
306         int maxLength = version == 1 ? V1_MAX_LENGTH : v2MaxHeaderSize;
307         fail(ctx, "header length (" + length + ") exceeds the allowed maximum (" + maxLength + ')', null);
308     }
309 
310     private void fail(final ChannelHandlerContext ctx, String errMsg, Exception e) {
311         finished = true;
312         ctx.close(); // drop connection immediately per spec
313         HAProxyProtocolException ppex;
314         if (errMsg != null && e != null) {
315             ppex = new HAProxyProtocolException(errMsg, e);
316         } else if (errMsg != null) {
317             ppex = new HAProxyProtocolException(errMsg);
318         } else if (e != null) {
319             ppex = new HAProxyProtocolException(e);
320         } else {
321             ppex = new HAProxyProtocolException();
322         }
323         throw ppex;
324     }
325 
326     /**
327      * Returns the {@link ProtocolDetectionResult} for the given {@link ByteBuf}.
328      */
329     public static ProtocolDetectionResult<HAProxyProtocolVersion> detectProtocol(ByteBuf buffer) {
330         if (buffer.readableBytes() < 12) {
331             return ProtocolDetectionResult.needsMoreData();
332         }
333 
334         int idx = buffer.readerIndex();
335 
336         if (match(BINARY_PREFIX, buffer, idx)) {
337             return DETECTION_RESULT_V2;
338         }
339         if (match(TEXT_PREFIX, buffer, idx)) {
340             return DETECTION_RESULT_V1;
341         }
342         return ProtocolDetectionResult.invalid();
343     }
344 
345     private static boolean match(byte[] prefix, ByteBuf buffer, int idx) {
346         for (int i = 0; i < prefix.length; i++) {
347             final byte b = buffer.getByte(idx + i);
348             if (b != prefix[i]) {
349                 return false;
350             }
351         }
352         return true;
353     }
354 
355     /**
356      * HeaderExtractor create a header frame out of the {@link ByteBuf}.
357      */
358     private abstract class HeaderExtractor {
359         /** Header max size */
360         private final int maxHeaderSize;
361 
362         protected HeaderExtractor(int maxHeaderSize) {
363             this.maxHeaderSize = maxHeaderSize;
364         }
365 
366         /**
367          * Create a frame out of the {@link ByteBuf} and return it.
368          *
369          * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
370          * @param buffer  the {@link ByteBuf} from which to read data
371          * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
372          *                be created
373          * @throws Exception if exceed maxLength
374          */
375         public ByteBuf extract(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
376             final int eoh = findEndOfHeader(buffer);
377             if (!discarding) {
378                 if (eoh >= 0) {
379                     final int length = eoh - buffer.readerIndex();
380                     if (length > maxHeaderSize) {
381                         buffer.readerIndex(eoh + delimiterLength(buffer, eoh));
382                         failOverLimit(ctx, length);
383                         return null;
384                     }
385                     ByteBuf frame = buffer.readSlice(length);
386                     buffer.skipBytes(delimiterLength(buffer, eoh));
387                     return frame;
388                 } else {
389                     final int length = buffer.readableBytes();
390                     if (length > maxHeaderSize) {
391                         discardedBytes = length;
392                         buffer.skipBytes(length);
393                         discarding = true;
394                         if (failFast) {
395                             failOverLimit(ctx, "over " + discardedBytes);
396                         }
397                     }
398                     return null;
399                 }
400             } else {
401                 if (eoh >= 0) {
402                     final int length = discardedBytes + eoh - buffer.readerIndex();
403                     buffer.readerIndex(eoh + delimiterLength(buffer, eoh));
404                     discardedBytes = 0;
405                     discarding = false;
406                     if (!failFast) {
407                         failOverLimit(ctx, "over " + length);
408                     }
409                 } else {
410                     discardedBytes += buffer.readableBytes();
411                     buffer.skipBytes(buffer.readableBytes());
412                 }
413                 return null;
414             }
415         }
416 
417         /**
418          * Find the end of the header from the given {@link ByteBuf},the end may be a CRLF, or the length given by the
419          * header.
420          *
421          * @param buffer the buffer to be searched
422          * @return {@code -1} if can not find the end, otherwise return the buffer index of end
423          */
424         protected abstract int findEndOfHeader(ByteBuf buffer);
425 
426         /**
427          * Get the length of the header delimiter.
428          *
429          * @param buffer the buffer where delimiter is located
430          * @param eoh index of delimiter
431          * @return length of the delimiter
432          */
433         protected abstract int delimiterLength(ByteBuf buffer, int eoh);
434     }
435 
436     private final class LineHeaderExtractor extends HeaderExtractor {
437 
438         LineHeaderExtractor(int maxHeaderSize) {
439             super(maxHeaderSize);
440         }
441 
442         @Override
443         protected int findEndOfHeader(ByteBuf buffer) {
444             return findEndOfLine(buffer);
445         }
446 
447         @Override
448         protected int delimiterLength(ByteBuf buffer, int eoh) {
449             return buffer.getByte(eoh) == '\r' ? 2 : 1;
450         }
451     }
452 
453     private final class StructHeaderExtractor extends HeaderExtractor {
454 
455         StructHeaderExtractor(int maxHeaderSize) {
456             super(maxHeaderSize);
457         }
458 
459         @Override
460         protected int findEndOfHeader(ByteBuf buffer) {
461             return HAProxyMessageDecoder.findEndOfHeader(buffer);
462         }
463 
464         @Override
465         protected int delimiterLength(ByteBuf buffer, int eoh) {
466             return 0;
467         }
468     }
469 }