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.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily;
20  import io.netty.util.AbstractReferenceCounted;
21  import io.netty.util.CharsetUtil;
22  import io.netty.util.NetUtil;
23  import io.netty.util.ResourceLeakDetector;
24  import io.netty.util.ResourceLeakDetectorFactory;
25  import io.netty.util.ResourceLeakTracker;
26  import io.netty.util.internal.ObjectUtil;
27  import io.netty.util.internal.PlatformDependent;
28  import io.netty.util.internal.StringUtil;
29  
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.List;
33  
34  /**
35   * Message container for decoded HAProxy proxy protocol parameters
36   */
37  public final class HAProxyMessage extends AbstractReferenceCounted {
38  
39      // Let's pick some conservative limit here.
40      private static final int MAX_NESTING_LEVEL = 128;
41      private static final ResourceLeakDetector<HAProxyMessage> leakDetector =
42              ResourceLeakDetectorFactory.instance().newResourceLeakDetector(HAProxyMessage.class);
43  
44      private final ResourceLeakTracker<HAProxyMessage> leak;
45      private final HAProxyProtocolVersion protocolVersion;
46      private final HAProxyCommand command;
47      private final HAProxyProxiedProtocol proxiedProtocol;
48      private final String sourceAddress;
49      private final String destinationAddress;
50      private final int sourcePort;
51      private final int destinationPort;
52      private final List<HAProxyTLV> tlvs;
53  
54      /**
55       * Creates a new instance
56       */
57      private HAProxyMessage(
58              HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
59              String sourceAddress, String destinationAddress, String sourcePort, String destinationPort) {
60          this(
61                  protocolVersion, command, proxiedProtocol,
62                  sourceAddress, destinationAddress, portStringToInt(sourcePort), portStringToInt(destinationPort));
63      }
64  
65      /**
66       * Creates a new instance of HAProxyMessage.
67       * @param protocolVersion the protocol version.
68       * @param command the command.
69       * @param proxiedProtocol the protocol containing the address family and transport protocol.
70       * @param sourceAddress the source address.
71       * @param destinationAddress the destination address.
72       * @param sourcePort the source port. This value must be 0 for unix, unspec addresses.
73       * @param destinationPort the destination port. This value must be 0 for unix, unspec addresses.
74       */
75      public HAProxyMessage(
76              HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
77              String sourceAddress, String destinationAddress, int sourcePort, int destinationPort) {
78  
79          this(protocolVersion, command, proxiedProtocol,
80               sourceAddress, destinationAddress, sourcePort, destinationPort, Collections.<HAProxyTLV>emptyList());
81      }
82  
83      /**
84       * Creates a new instance of HAProxyMessage.
85       * @param protocolVersion the protocol version.
86       * @param command the command.
87       * @param proxiedProtocol the protocol containing the address family and transport protocol.
88       * @param sourceAddress the source address.
89       * @param destinationAddress the destination address.
90       * @param sourcePort the source port. This value must be 0 for unix, unspec addresses.
91       * @param destinationPort the destination port. This value must be 0 for unix, unspec addresses.
92       * @param tlvs the list of tlvs.
93       */
94      public HAProxyMessage(
95              HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
96              String sourceAddress, String destinationAddress, int sourcePort, int destinationPort,
97              List<? extends HAProxyTLV> tlvs) {
98  
99          ObjectUtil.checkNotNull(protocolVersion, "protocolVersion");
100         ObjectUtil.checkNotNull(proxiedProtocol, "proxiedProtocol");
101         ObjectUtil.checkNotNull(tlvs, "tlvs");
102         AddressFamily addrFamily = proxiedProtocol.addressFamily();
103 
104         checkAddress(sourceAddress, addrFamily);
105         checkAddress(destinationAddress, addrFamily);
106         checkPort(sourcePort, addrFamily);
107         checkPort(destinationPort, addrFamily);
108 
109         this.protocolVersion = protocolVersion;
110         this.command = command;
111         this.proxiedProtocol = proxiedProtocol;
112         this.sourceAddress = sourceAddress;
113         this.destinationAddress = destinationAddress;
114         this.sourcePort = sourcePort;
115         this.destinationPort = destinationPort;
116         this.tlvs = Collections.unmodifiableList(tlvs);
117 
118         leak = leakDetector.track(this);
119     }
120 
121     /**
122      * Decodes a version 2, binary proxy protocol header.
123      *
124      * @param header                     a version 2 proxy protocol header
125      * @return                           {@link HAProxyMessage} instance
126      * @throws HAProxyProtocolException  if any portion of the header is invalid
127      */
128     static HAProxyMessage decodeHeader(ByteBuf header) {
129         ObjectUtil.checkNotNull(header, "header");
130 
131         if (header.readableBytes() < 16) {
132             throw new HAProxyProtocolException(
133                     "incomplete header: " + header.readableBytes() + " bytes (expected: 16+ bytes)");
134         }
135 
136         // Per spec, the 13th byte is the protocol version and command byte
137         header.skipBytes(12);
138         final byte verCmdByte = header.readByte();
139 
140         HAProxyProtocolVersion ver;
141         try {
142             ver = HAProxyProtocolVersion.valueOf(verCmdByte);
143         } catch (IllegalArgumentException e) {
144             throw new HAProxyProtocolException(e);
145         }
146 
147         if (ver != HAProxyProtocolVersion.V2) {
148             throw new HAProxyProtocolException("version 1 unsupported: 0x" + Integer.toHexString(verCmdByte));
149         }
150 
151         HAProxyCommand cmd;
152         try {
153             cmd = HAProxyCommand.valueOf(verCmdByte);
154         } catch (IllegalArgumentException e) {
155             throw new HAProxyProtocolException(e);
156         }
157 
158         if (cmd == HAProxyCommand.LOCAL) {
159             return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL);
160         }
161 
162         // Per spec, the 14th byte is the protocol and address family byte
163         HAProxyProxiedProtocol protAndFam;
164         try {
165             protAndFam = HAProxyProxiedProtocol.valueOf(header.readByte());
166         } catch (IllegalArgumentException e) {
167             throw new HAProxyProtocolException(e);
168         }
169 
170         if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) {
171             return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY);
172         }
173 
174         int addressInfoLen = header.readUnsignedShort();
175 
176         String srcAddress;
177         String dstAddress;
178         int addressLen;
179         int srcPort = 0;
180         int dstPort = 0;
181 
182         AddressFamily addressFamily = protAndFam.addressFamily();
183 
184         if (addressFamily == AddressFamily.AF_UNIX) {
185             // unix sockets require 216 bytes for address information
186             if (addressInfoLen < 216 || header.readableBytes() < 216) {
187                 throw new HAProxyProtocolException(
188                     "incomplete UNIX socket address information: " +
189                             Math.min(addressInfoLen, header.readableBytes()) + " bytes (expected: 216+ bytes)");
190             }
191             int startIdx = header.readerIndex();
192             int addressEnd = header.indexOf(startIdx, startIdx + 108, (byte) 0); // FIND_NUL
193             if (addressEnd == -1) {
194                 addressLen = 108;
195             } else {
196                 addressLen = addressEnd - startIdx;
197             }
198             srcAddress = header.toString(startIdx, addressLen, CharsetUtil.US_ASCII);
199 
200             startIdx += 108;
201 
202             addressEnd = header.indexOf(startIdx, startIdx + 108, (byte) 0); // FIND_NUL
203             if (addressEnd == -1) {
204                 addressLen = 108;
205             } else {
206                 addressLen = addressEnd - startIdx;
207             }
208             dstAddress = header.toString(startIdx, addressLen, CharsetUtil.US_ASCII);
209             // AF_UNIX defines that exactly 108 bytes are reserved for the address. The previous methods
210             // did not increase the reader index although we already consumed the information.
211             header.readerIndex(startIdx + 108);
212         } else {
213             if (addressFamily == AddressFamily.AF_IPv4) {
214                 // IPv4 requires 12 bytes for address information
215                 if (addressInfoLen < 12 || header.readableBytes() < 12) {
216                     throw new HAProxyProtocolException(
217                         "incomplete IPv4 address information: " +
218                                 Math.min(addressInfoLen, header.readableBytes()) + " bytes (expected: 12+ bytes)");
219                 }
220                 addressLen = 4;
221             } else if (addressFamily == AddressFamily.AF_IPv6) {
222                 // IPv6 requires 36 bytes for address information
223                 if (addressInfoLen < 36 || header.readableBytes() < 36) {
224                     throw new HAProxyProtocolException(
225                         "incomplete IPv6 address information: " +
226                                 Math.min(addressInfoLen, header.readableBytes()) + " bytes (expected: 36+ bytes)");
227                 }
228                 addressLen = 16;
229             } else {
230                 throw new HAProxyProtocolException(
231                     "unable to parse address information (unknown address family: " + addressFamily + ')');
232             }
233 
234             // Per spec, the src address begins at the 17th byte
235             srcAddress = ipBytesToString(header, addressLen);
236             dstAddress = ipBytesToString(header, addressLen);
237             srcPort = header.readUnsignedShort();
238             dstPort = header.readUnsignedShort();
239         }
240 
241         final List<HAProxyTLV> tlvs = readTlvs(header);
242 
243         return new HAProxyMessage(ver, cmd, protAndFam, srcAddress, dstAddress, srcPort, dstPort, tlvs);
244     }
245 
246     private static List<HAProxyTLV> readTlvs(final ByteBuf header) {
247         HAProxyTLV haProxyTLV = readNextTLV(header, 0);
248         if (haProxyTLV == null) {
249             return Collections.emptyList();
250         }
251         // In most cases there are less than 4 TLVs available
252         List<HAProxyTLV> haProxyTLVs = new ArrayList<HAProxyTLV>(4);
253 
254         try {
255             do {
256                 haProxyTLVs.add(haProxyTLV);
257                 if (haProxyTLV instanceof HAProxySSLTLV) {
258                     haProxyTLVs.addAll(((HAProxySSLTLV) haProxyTLV).encapsulatedTLVs());
259                 }
260             } while ((haProxyTLV = readNextTLV(header, 0)) != null);
261         } catch (Throwable t) {
262             // Release all previously read TLVs before rethrowing as otherwise we would leak.
263             releaseTlvs(haProxyTLVs);
264             PlatformDependent.throwException(t);
265         }
266         return haProxyTLVs;
267     }
268 
269     private static void releaseDeep(List<HAProxyTLV> children) {
270         for (HAProxyTLV child : children) {
271             child.release();
272             if (child instanceof HAProxySSLTLV) {
273                 releaseDeep(((HAProxySSLTLV) child).encapsulatedTLVs());
274             }
275         }
276     }
277 
278     private static void releaseTlvs(List<HAProxyTLV> tlvs) {
279         int skip = 0;
280         for (HAProxyTLV tlv : tlvs) {
281             if (skip > 0) {
282                 skip--;
283                 // This TLV is a flattened depth-1 child. If it encapsulates anything (depth-2+),
284                 // those deeper children were NOT flattened, so we must release them recursively.
285                 if (tlv instanceof HAProxySSLTLV) {
286                     releaseDeep(((HAProxySSLTLV) tlv).encapsulatedTLVs());
287                 }
288             } else if (tlv instanceof HAProxySSLTLV) {
289                 // This is a top-level (depth-0) SSL TLV.
290                 // Its immediate children (depth-1) were flattened into this list,
291                 // so we must skip them in the outer loop to avoid treating them as top-level TLVs.
292                 skip = ((HAProxySSLTLV) tlv).encapsulatedTLVs().size();
293             }
294             tlv.release();
295         }
296     }
297 
298     private static HAProxyTLV readNextTLV(final ByteBuf header, int nestingLevel) {
299         if (nestingLevel > MAX_NESTING_LEVEL) {
300             throw new HAProxyProtocolException(
301                     "Maximum TLV nesting level reached: " + nestingLevel + " (expected: < " + MAX_NESTING_LEVEL + ')');
302         }
303         // We need at least 4 bytes for a TLV
304         if (header.readableBytes() < 4) {
305             return null;
306         }
307 
308         final byte typeAsByte = header.readByte();
309         final HAProxyTLV.Type type = HAProxyTLV.Type.typeForByteValue(typeAsByte);
310 
311         final int length = header.readUnsignedShort();
312         switch (type) {
313         case PP2_TYPE_SSL:
314             if (length < 5) {
315                 throw new HAProxyProtocolException("TLV length must be at least 5 but was: " + length);
316             }
317             if (length > header.readableBytes()) {
318                 throw new HAProxyProtocolException("TLV length must be smaller or equal the readable bytes (" +
319                         header.readableBytes() + ") but was: " + length);
320             }
321             // Slice the rawContent but only retain it if we didn't see an error as otherwise we might
322             // leak.
323             final ByteBuf rawContent = header.slice(header.readerIndex(), length);
324             final ByteBuf byteBuf = header.readSlice(length);
325             final byte client = byteBuf.readByte();
326             final int verify = byteBuf.readInt();
327 
328             if (byteBuf.readableBytes() >= 4) {
329 
330                 final List<HAProxyTLV> encapsulatedTlvs = new ArrayList<HAProxyTLV>(4);
331                 try {
332                     do {
333                         final HAProxyTLV haProxyTLV = readNextTLV(byteBuf, nestingLevel + 1);
334                         if (haProxyTLV == null) {
335                             break;
336                         }
337                         encapsulatedTlvs.add(haProxyTLV);
338                     } while (byteBuf.readableBytes() >= 4);
339                 } catch (Throwable t) {
340                     releaseTlvs(encapsulatedTlvs);
341                     PlatformDependent.throwException(t);
342                 }
343 
344                 return new HAProxySSLTLV(verify, client, encapsulatedTlvs, rawContent.retain());
345             }
346             return new HAProxySSLTLV(verify, client, Collections.<HAProxyTLV>emptyList(), rawContent.retain());
347         // If we're not dealing with an SSL Type, we can use the same mechanism
348         case PP2_TYPE_ALPN:
349         case PP2_TYPE_AUTHORITY:
350         case PP2_TYPE_SSL_VERSION:
351         case PP2_TYPE_SSL_CN:
352         case PP2_TYPE_NETNS:
353         case OTHER:
354             return new HAProxyTLV(type, typeAsByte, header.readRetainedSlice(length));
355         default:
356             return null;
357         }
358     }
359 
360     /**
361      * Decodes a version 1, human-readable proxy protocol header.
362      *
363      * @param header                     a version 1 proxy protocol header
364      * @return                           {@link HAProxyMessage} instance
365      * @throws HAProxyProtocolException  if any portion of the header is invalid
366      */
367     static HAProxyMessage decodeHeader(String header) {
368         if (header == null) {
369             throw new HAProxyProtocolException("header");
370         }
371 
372         String[] parts = header.split(" ");
373         int numParts = parts.length;
374 
375         if (numParts < 2) {
376             throw new HAProxyProtocolException(
377                     "invalid header: " + header + " (expected: 'PROXY' and proxied protocol values)");
378         }
379 
380         if (!"PROXY".equals(parts[0])) {
381             throw new HAProxyProtocolException("unknown identifier: " + parts[0]);
382         }
383 
384         HAProxyProxiedProtocol protAndFam;
385         try {
386             protAndFam = HAProxyProxiedProtocol.valueOf(parts[1]);
387         } catch (IllegalArgumentException e) {
388             throw new HAProxyProtocolException(e);
389         }
390 
391         if (protAndFam != HAProxyProxiedProtocol.TCP4 &&
392                 protAndFam != HAProxyProxiedProtocol.TCP6 &&
393                 protAndFam != HAProxyProxiedProtocol.UNKNOWN) {
394             throw new HAProxyProtocolException("unsupported v1 proxied protocol: " + parts[1]);
395         }
396 
397         if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) {
398             return unknownMsg(HAProxyProtocolVersion.V1, HAProxyCommand.PROXY);
399         }
400 
401         if (numParts != 6) {
402             throw new HAProxyProtocolException("invalid TCP4/6 header: " + header + " (expected: 6 parts)");
403         }
404 
405         try {
406             return new HAProxyMessage(
407                     HAProxyProtocolVersion.V1, HAProxyCommand.PROXY,
408                     protAndFam, parts[2], parts[3], parts[4], parts[5]);
409         } catch (RuntimeException e) {
410             throw new HAProxyProtocolException("invalid HAProxy message", e);
411         }
412     }
413 
414     /**
415      * Proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is
416      * 'UNKNOWN' we must discard all other header values.
417      */
418     private static HAProxyMessage unknownMsg(HAProxyProtocolVersion version, HAProxyCommand command) {
419         return new HAProxyMessage(version, command, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0);
420     }
421 
422     /**
423      * Convert ip address bytes to string representation
424      *
425      * @param header     buffer containing ip address bytes
426      * @param addressLen number of bytes to read (4 bytes for IPv4, 16 bytes for IPv6)
427      * @return           string representation of the ip address
428      */
429     private static String ipBytesToString(ByteBuf header, int addressLen) {
430         StringBuilder sb = new StringBuilder();
431         final int ipv4Len = 4;
432         final int ipv6Len = 8;
433         if (addressLen == ipv4Len) {
434             for (int i = 0; i < ipv4Len; i++) {
435                 sb.append(header.readByte() & 0xff);
436                 sb.append('.');
437             }
438         } else {
439             for (int i = 0; i < ipv6Len; i++) {
440                 sb.append(Integer.toHexString(header.readUnsignedShort()));
441                 sb.append(':');
442             }
443         }
444         sb.setLength(sb.length() - 1);
445         return sb.toString();
446     }
447 
448     /**
449      * Convert port to integer
450      *
451      * @param value                      the port
452      * @return                           port as an integer
453      * @throws IllegalArgumentException  if port is not a valid integer
454      */
455     private static int portStringToInt(String value) {
456         int port;
457         try {
458             port = Integer.parseInt(value);
459         } catch (NumberFormatException e) {
460             throw new IllegalArgumentException("invalid port: " + value, e);
461         }
462 
463         if (port <= 0 || port > 65535) {
464             throw new IllegalArgumentException("invalid port: " + value + " (expected: 1 ~ 65535)");
465         }
466 
467         return port;
468     }
469 
470     /**
471      * Validate an address (IPv4, IPv6, Unix Socket)
472      *
473      * @param address                    human-readable address
474      * @param addrFamily                 the {@link AddressFamily} to check the address against
475      * @throws IllegalArgumentException  if the address is invalid
476      */
477     private static void checkAddress(String address, AddressFamily addrFamily) {
478         ObjectUtil.checkNotNull(addrFamily, "addrFamily");
479 
480         switch (addrFamily) {
481             case AF_UNSPEC:
482                 if (address != null) {
483                     throw new IllegalArgumentException("unable to validate an AF_UNSPEC address: " + address);
484                 }
485                 return;
486             case AF_UNIX:
487                 ObjectUtil.checkNotNull(address, "address");
488                 if (address.getBytes(CharsetUtil.US_ASCII).length > 108) {
489                     throw new IllegalArgumentException("invalid AF_UNIX address: " + address);
490                 }
491                 return;
492         }
493 
494         ObjectUtil.checkNotNull(address, "address");
495 
496         switch (addrFamily) {
497             case AF_IPv4:
498                 if (!NetUtil.isValidIpV4Address(address)) {
499                     throw new IllegalArgumentException("invalid IPv4 address: " + address);
500                 }
501                 break;
502             case AF_IPv6:
503                 if (!NetUtil.isValidIpV6Address(address)) {
504                     throw new IllegalArgumentException("invalid IPv6 address: " + address);
505                 }
506                 break;
507             default:
508                 throw new IllegalArgumentException("unexpected addrFamily: " + addrFamily);
509         }
510     }
511 
512     /**
513      * Validate the port depending on the addrFamily.
514      *
515      * @param port                       the UDP/TCP port
516      * @throws IllegalArgumentException  if the port is out of range (0-65535 inclusive)
517      */
518     private static void checkPort(int port, AddressFamily addrFamily) {
519         switch (addrFamily) {
520         case AF_IPv6:
521         case AF_IPv4:
522             if (port < 0 || port > 65535) {
523                 throw new IllegalArgumentException("invalid port: " + port + " (expected: 0 ~ 65535)");
524             }
525             break;
526         case AF_UNIX:
527         case AF_UNSPEC:
528             if (port != 0) {
529                 throw new IllegalArgumentException("port cannot be specified with addrFamily: " + addrFamily);
530             }
531             break;
532         default:
533             throw new IllegalArgumentException("unexpected addrFamily: " + addrFamily);
534         }
535     }
536 
537     /**
538      * Returns the {@link HAProxyProtocolVersion} of this {@link HAProxyMessage}.
539      */
540     public HAProxyProtocolVersion protocolVersion() {
541         return protocolVersion;
542     }
543 
544     /**
545      * Returns the {@link HAProxyCommand} of this {@link HAProxyMessage}.
546      */
547     public HAProxyCommand command() {
548         return command;
549     }
550 
551     /**
552      * Returns the {@link HAProxyProxiedProtocol} of this {@link HAProxyMessage}.
553      */
554     public HAProxyProxiedProtocol proxiedProtocol() {
555         return proxiedProtocol;
556     }
557 
558     /**
559      * Returns the human-readable source address of this {@link HAProxyMessage} or {@code null}
560      * if HAProxy performs health check with {@code send-proxy-v2}.
561      */
562     public String sourceAddress() {
563         return sourceAddress;
564     }
565 
566     /**
567      * Returns the human-readable destination address of this {@link HAProxyMessage}.
568      */
569     public String destinationAddress() {
570         return destinationAddress;
571     }
572 
573     /**
574      * Returns the UDP/TCP source port of this {@link HAProxyMessage}.
575      */
576     public int sourcePort() {
577         return sourcePort;
578     }
579 
580     /**
581      * Returns the UDP/TCP destination port of this {@link HAProxyMessage}.
582      */
583     public int destinationPort() {
584         return destinationPort;
585     }
586 
587     /**
588      * Returns a list of {@link HAProxyTLV} or an empty list if no TLVs are present.
589      * <p>
590      * TLVs are only available for the Proxy Protocol V2
591      */
592     public List<HAProxyTLV> tlvs() {
593         return tlvs;
594     }
595 
596     int tlvNumBytes() {
597         int tlvNumBytes = 0;
598         for (int i = 0; i < tlvs.size(); i++) {
599             tlvNumBytes += tlvs.get(i).totalNumBytes();
600         }
601         return tlvNumBytes;
602     }
603 
604     @Override
605     public HAProxyMessage touch() {
606         tryRecord();
607         return (HAProxyMessage) super.touch();
608     }
609 
610     @Override
611     public HAProxyMessage touch(Object hint) {
612         if (leak != null) {
613             leak.record(hint);
614         }
615         return this;
616     }
617 
618     @Override
619     public HAProxyMessage retain() {
620         tryRecord();
621         return (HAProxyMessage) super.retain();
622     }
623 
624     @Override
625     public HAProxyMessage retain(int increment) {
626         tryRecord();
627         return (HAProxyMessage) super.retain(increment);
628     }
629 
630     @Override
631     public boolean release() {
632         tryRecord();
633         return super.release();
634     }
635 
636     @Override
637     public boolean release(int decrement) {
638         tryRecord();
639         return super.release(decrement);
640     }
641 
642     private void tryRecord() {
643         if (leak != null) {
644             leak.record();
645         }
646     }
647 
648     @Override
649     protected void deallocate() {
650         try {
651             releaseTlvs(tlvs);
652         } finally {
653             final ResourceLeakTracker<HAProxyMessage> leak = this.leak;
654             if (leak != null) {
655                 boolean closed = leak.close(this);
656                 assert closed;
657             }
658         }
659     }
660 
661     @Override
662     public String toString() {
663         StringBuilder sb = new StringBuilder(256)
664                 .append(StringUtil.simpleClassName(this))
665                 .append("(protocolVersion: ").append(protocolVersion)
666                 .append(", command: ").append(command)
667                 .append(", proxiedProtocol: ").append(proxiedProtocol)
668                 .append(", sourceAddress: ").append(sourceAddress)
669                 .append(", destinationAddress: ").append(destinationAddress)
670                 .append(", sourcePort: ").append(sourcePort)
671                 .append(", destinationPort: ").append(destinationPort)
672                 .append(", tlvs: [");
673         if (!tlvs.isEmpty()) {
674             for (HAProxyTLV tlv: tlvs) {
675                 sb.append(tlv).append(", ");
676             }
677             sb.setLength(sb.length() - 2);
678         }
679         sb.append("])");
680         return sb.toString();
681     }
682 }