// 
// Decompiled by Procyon v0.6.0
// 

package io.netty.handler.pcap;

import io.netty.channel.DefaultAddressedEnvelope;
import java.net.UnknownHostException;
import java.net.InetAddress;
import io.netty.channel.Channel;
import io.netty.channel.socket.DatagramPacket;
import java.net.Inet6Address;
import io.netty.util.NetUtil;
import java.net.Inet4Address;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelPromise;
import io.netty.util.ReferenceCountUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.ChannelHandlerContext;
import java.io.IOException;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.util.concurrent.atomic.AtomicReference;
import java.net.InetSocketAddress;
import java.io.OutputStream;
import io.netty.util.internal.logging.InternalLogger;
import java.io.Closeable;
import io.netty.channel.ChannelDuplexHandler;

public final class PcapWriteHandler extends ChannelDuplexHandler implements Closeable
{
    private final InternalLogger logger;
    private PcapWriter pCapWriter;
    private final OutputStream outputStream;
    private final boolean captureZeroByte;
    private final boolean writePcapGlobalHeader;
    private final boolean sharedOutputStream;
    private long sendSegmentNumber;
    private long receiveSegmentNumber;
    private ChannelType channelType;
    private InetSocketAddress initiatorAddr;
    private InetSocketAddress handlerAddr;
    private boolean isServerPipeline;
    private final AtomicReference<State> state;
    
    @Deprecated
    public PcapWriteHandler(final OutputStream outputStream) {
        this(outputStream, false, true);
    }
    
    @Deprecated
    public PcapWriteHandler(final OutputStream outputStream, final boolean captureZeroByte, final boolean writePcapGlobalHeader) {
        this.logger = InternalLoggerFactory.getInstance(PcapWriteHandler.class);
        this.sendSegmentNumber = 1L;
        this.receiveSegmentNumber = 1L;
        this.state = new AtomicReference<State>(State.INIT);
        this.outputStream = ObjectUtil.checkNotNull(outputStream, "OutputStream");
        this.captureZeroByte = captureZeroByte;
        this.writePcapGlobalHeader = writePcapGlobalHeader;
        this.sharedOutputStream = false;
    }
    
    private PcapWriteHandler(final Builder builder, final OutputStream outputStream) {
        this.logger = InternalLoggerFactory.getInstance(PcapWriteHandler.class);
        this.sendSegmentNumber = 1L;
        this.receiveSegmentNumber = 1L;
        this.state = new AtomicReference<State>(State.INIT);
        this.outputStream = outputStream;
        this.captureZeroByte = builder.captureZeroByte;
        this.sharedOutputStream = builder.sharedOutputStream;
        this.writePcapGlobalHeader = builder.writePcapGlobalHeader;
        this.channelType = builder.channelType;
        this.handlerAddr = builder.handlerAddr;
        this.initiatorAddr = builder.initiatorAddr;
        this.isServerPipeline = builder.isServerPipeline;
    }
    
    public static void writeGlobalHeader(final OutputStream outputStream) throws IOException {
        PcapHeaders.writeGlobalHeader(outputStream);
    }
    
    private void initializeIfNecessary(final ChannelHandlerContext ctx) throws Exception {
        if (this.state.get() != State.INIT) {
            return;
        }
        this.pCapWriter = new PcapWriter(this);
        if (this.channelType == null) {
            if (ctx.channel() instanceof SocketChannel) {
                this.channelType = ChannelType.TCP;
                if (ctx.channel().parent() instanceof ServerSocketChannel) {
                    this.isServerPipeline = true;
                    this.initiatorAddr = (InetSocketAddress)ctx.channel().remoteAddress();
                    this.handlerAddr = getLocalAddress(ctx.channel(), this.initiatorAddr);
                }
                else {
                    this.isServerPipeline = false;
                    this.handlerAddr = (InetSocketAddress)ctx.channel().remoteAddress();
                    this.initiatorAddr = getLocalAddress(ctx.channel(), this.handlerAddr);
                }
            }
            else if (ctx.channel() instanceof DatagramChannel) {
                this.channelType = ChannelType.UDP;
                final DatagramChannel datagramChannel = (DatagramChannel)ctx.channel();
                if (datagramChannel.isConnected()) {
                    this.handlerAddr = (InetSocketAddress)ctx.channel().remoteAddress();
                    this.initiatorAddr = getLocalAddress(ctx.channel(), this.handlerAddr);
                }
            }
        }
        if (this.channelType == ChannelType.TCP) {
            this.logger.debug("Initiating Fake TCP 3-Way Handshake");
            final ByteBuf tcpBuf = ctx.alloc().buffer();
            try {
                TCPPacket.writePacket(tcpBuf, null, 0L, 0L, this.initiatorAddr.getPort(), this.handlerAddr.getPort(), TCPPacket.TCPFlag.SYN);
                this.completeTCPWrite(this.initiatorAddr, this.handlerAddr, tcpBuf, ctx.alloc(), ctx);
                TCPPacket.writePacket(tcpBuf, null, 0L, 1L, this.handlerAddr.getPort(), this.initiatorAddr.getPort(), TCPPacket.TCPFlag.SYN, TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(this.handlerAddr, this.initiatorAddr, tcpBuf, ctx.alloc(), ctx);
                TCPPacket.writePacket(tcpBuf, null, 1L, 1L, this.initiatorAddr.getPort(), this.handlerAddr.getPort(), TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(this.initiatorAddr, this.handlerAddr, tcpBuf, ctx.alloc(), ctx);
            }
            finally {
                tcpBuf.release();
            }
            this.logger.debug("Finished Fake TCP 3-Way Handshake");
        }
        this.state.set(State.WRITING);
    }
    
    @Override
    public void channelActive(final ChannelHandlerContext ctx) throws Exception {
        this.initializeIfNecessary(ctx);
        super.channelActive(ctx);
    }
    
    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
        if (this.state.get() == State.INIT) {
            try {
                this.initializeIfNecessary(ctx);
            }
            catch (final Exception ex) {
                ReferenceCountUtil.release(msg);
                throw ex;
            }
        }
        if (this.state.get() == State.WRITING) {
            if (this.channelType == ChannelType.TCP) {
                this.handleTCP(ctx, msg, false);
            }
            else if (this.channelType == ChannelType.UDP) {
                this.handleUDP(ctx, msg, false);
            }
            else {
                this.logDiscard();
            }
        }
        super.channelRead(ctx, msg);
    }
    
    @Override
    public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception {
        if (this.state.get() == State.INIT) {
            try {
                this.initializeIfNecessary(ctx);
            }
            catch (final Exception ex) {
                ReferenceCountUtil.release(msg);
                promise.setFailure((Throwable)ex);
                return;
            }
        }
        if (this.state.get() == State.WRITING) {
            if (this.channelType == ChannelType.TCP) {
                this.handleTCP(ctx, msg, true);
            }
            else if (this.channelType == ChannelType.UDP) {
                this.handleUDP(ctx, msg, true);
            }
            else {
                this.logDiscard();
            }
        }
        super.write(ctx, msg, promise);
    }
    
    private void handleTCP(final ChannelHandlerContext ctx, final Object msg, final boolean isWriteOperation) {
        if (msg instanceof ByteBuf) {
            final int totalBytes = ((ByteBuf)msg).readableBytes();
            if (totalBytes == 0 && !this.captureZeroByte) {
                this.logger.debug("Discarding Zero Byte TCP Packet. isWriteOperation {}", (Object)isWriteOperation);
                return;
            }
            final ByteBufAllocator byteBufAllocator = ctx.alloc();
            if (totalBytes == 0) {
                this.handleTcpPacket(ctx, (ByteBuf)msg, isWriteOperation, byteBufAllocator);
                return;
            }
            for (int maxTcpPayload = 65495, i = 0; i < totalBytes; i += maxTcpPayload) {
                final ByteBuf packet = ((ByteBuf)msg).slice(i, Math.min(maxTcpPayload, totalBytes - i));
                this.handleTcpPacket(ctx, packet, isWriteOperation, byteBufAllocator);
            }
        }
        else {
            this.logger.debug("Discarding Pcap Write for TCP Object: {}", msg);
        }
    }
    
    private void handleTcpPacket(final ChannelHandlerContext ctx, final ByteBuf packet, final boolean isWriteOperation, final ByteBufAllocator byteBufAllocator) {
        final ByteBuf tcpBuf = byteBufAllocator.buffer();
        final int bytes = packet.readableBytes();
        try {
            if (isWriteOperation) {
                InetSocketAddress srcAddr;
                InetSocketAddress dstAddr;
                if (this.isServerPipeline) {
                    srcAddr = this.handlerAddr;
                    dstAddr = this.initiatorAddr;
                }
                else {
                    srcAddr = this.initiatorAddr;
                    dstAddr = this.handlerAddr;
                }
                TCPPacket.writePacket(tcpBuf, packet, this.sendSegmentNumber, this.receiveSegmentNumber, srcAddr.getPort(), dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
                this.logTCP(true, bytes, this.sendSegmentNumber, this.receiveSegmentNumber, srcAddr, dstAddr, false);
                this.sendSegmentNumber = incrementUintSegmentNumber(this.sendSegmentNumber, bytes);
                TCPPacket.writePacket(tcpBuf, null, this.receiveSegmentNumber, this.sendSegmentNumber, dstAddr.getPort(), srcAddr.getPort(), TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
                this.logTCP(true, bytes, this.sendSegmentNumber, this.receiveSegmentNumber, dstAddr, srcAddr, true);
            }
            else {
                InetSocketAddress srcAddr;
                InetSocketAddress dstAddr;
                if (this.isServerPipeline) {
                    srcAddr = this.initiatorAddr;
                    dstAddr = this.handlerAddr;
                }
                else {
                    srcAddr = this.handlerAddr;
                    dstAddr = this.initiatorAddr;
                }
                TCPPacket.writePacket(tcpBuf, packet, this.receiveSegmentNumber, this.sendSegmentNumber, srcAddr.getPort(), dstAddr.getPort(), TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(srcAddr, dstAddr, tcpBuf, byteBufAllocator, ctx);
                this.logTCP(false, bytes, this.receiveSegmentNumber, this.sendSegmentNumber, srcAddr, dstAddr, false);
                this.receiveSegmentNumber = incrementUintSegmentNumber(this.receiveSegmentNumber, bytes);
                TCPPacket.writePacket(tcpBuf, null, this.sendSegmentNumber, this.receiveSegmentNumber, dstAddr.getPort(), srcAddr.getPort(), TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(dstAddr, srcAddr, tcpBuf, byteBufAllocator, ctx);
                this.logTCP(false, bytes, this.sendSegmentNumber, this.receiveSegmentNumber, dstAddr, srcAddr, true);
            }
        }
        finally {
            tcpBuf.release();
        }
    }
    
    private void completeTCPWrite(final InetSocketAddress srcAddr, final InetSocketAddress dstAddr, final ByteBuf tcpBuf, final ByteBufAllocator byteBufAllocator, final ChannelHandlerContext ctx) {
        final ByteBuf ipBuf = byteBufAllocator.buffer();
        final ByteBuf ethernetBuf = byteBufAllocator.buffer();
        final ByteBuf pcap = byteBufAllocator.buffer();
        try {
            if (srcAddr.getAddress() instanceof Inet4Address && dstAddr.getAddress() instanceof Inet4Address) {
                IPPacket.writeTCPv4(ipBuf, tcpBuf, NetUtil.ipv4AddressToInt((Inet4Address)srcAddr.getAddress()), NetUtil.ipv4AddressToInt((Inet4Address)dstAddr.getAddress()));
                EthernetPacket.writeIPv4(ethernetBuf, ipBuf);
            }
            else {
                if (!(srcAddr.getAddress() instanceof Inet6Address) || !(dstAddr.getAddress() instanceof Inet6Address)) {
                    this.logger.error("Source and Destination IP Address versions are not same. Source Address: {}, Destination Address: {}", srcAddr.getAddress(), dstAddr.getAddress());
                    return;
                }
                IPPacket.writeTCPv6(ipBuf, tcpBuf, srcAddr.getAddress().getAddress(), dstAddr.getAddress().getAddress());
                EthernetPacket.writeIPv6(ethernetBuf, ipBuf);
            }
            this.pCapWriter.writePacket(pcap, ethernetBuf);
        }
        catch (final IOException ex) {
            this.logger.error("Caught Exception While Writing Packet into Pcap", ex);
            ctx.fireExceptionCaught((Throwable)ex);
        }
        finally {
            ipBuf.release();
            ethernetBuf.release();
            pcap.release();
        }
    }
    
    private static long incrementUintSegmentNumber(final long sequenceNumber, final int value) {
        return (sequenceNumber + value) % 4294967296L;
    }
    
    private void handleUDP(final ChannelHandlerContext ctx, final Object msg, final boolean isWriteOperation) {
        final ByteBuf udpBuf = ctx.alloc().buffer();
        try {
            if (msg instanceof DatagramPacket) {
                if (((DefaultAddressedEnvelope<ByteBuf, A>)msg).content().readableBytes() == 0 && !this.captureZeroByte) {
                    this.logger.debug("Discarding Zero Byte UDP Packet");
                    return;
                }
                if (((DefaultAddressedEnvelope<ByteBuf, A>)msg).content().readableBytes() > 65507) {
                    this.logger.warn("Unable to write UDP packet to PCAP. Payload of size {} exceeds max size of 65507");
                    return;
                }
                final DatagramPacket datagramPacket = ((DatagramPacket)msg).duplicate();
                InetSocketAddress srcAddr = ((DefaultAddressedEnvelope<M, InetSocketAddress>)datagramPacket).sender();
                final InetSocketAddress dstAddr = ((DefaultAddressedEnvelope<M, InetSocketAddress>)datagramPacket).recipient();
                if (srcAddr == null) {
                    srcAddr = getLocalAddress(ctx.channel(), dstAddr);
                }
                this.logger.debug("Writing UDP Data of {} Bytes, isWriteOperation {}, Src Addr {}, Dst Addr {}", ((DefaultAddressedEnvelope<ByteBuf, A>)datagramPacket).content().readableBytes(), isWriteOperation, srcAddr, dstAddr);
                UDPPacket.writePacket(udpBuf, ((DefaultAddressedEnvelope<ByteBuf, A>)datagramPacket).content(), srcAddr.getPort(), dstAddr.getPort());
                this.completeUDPWrite(srcAddr, dstAddr, udpBuf, ctx.alloc(), ctx);
            }
            else if (msg instanceof ByteBuf && (!(ctx.channel() instanceof DatagramChannel) || ((DatagramChannel)ctx.channel()).isConnected())) {
                if (((ByteBuf)msg).readableBytes() == 0 && !this.captureZeroByte) {
                    this.logger.debug("Discarding Zero Byte UDP Packet");
                    return;
                }
                if (((ByteBuf)msg).readableBytes() > 65507) {
                    this.logger.warn("Unable to write UDP packet to PCAP. Payload of size {} exceeds max size of 65507");
                    return;
                }
                final ByteBuf byteBuf = ((ByteBuf)msg).duplicate();
                final InetSocketAddress sourceAddr = isWriteOperation ? this.initiatorAddr : this.handlerAddr;
                final InetSocketAddress destinationAddr = isWriteOperation ? this.handlerAddr : this.initiatorAddr;
                this.logger.debug("Writing UDP Data of {} Bytes, Src Addr {}, Dst Addr {}", byteBuf.readableBytes(), sourceAddr, destinationAddr);
                UDPPacket.writePacket(udpBuf, byteBuf, sourceAddr.getPort(), destinationAddr.getPort());
                this.completeUDPWrite(sourceAddr, destinationAddr, udpBuf, ctx.alloc(), ctx);
            }
            else {
                this.logger.debug("Discarding Pcap Write for UDP Object: {}", msg);
            }
        }
        finally {
            udpBuf.release();
        }
    }
    
    private void completeUDPWrite(final InetSocketAddress srcAddr, final InetSocketAddress dstAddr, final ByteBuf udpBuf, final ByteBufAllocator byteBufAllocator, final ChannelHandlerContext ctx) {
        final ByteBuf ipBuf = byteBufAllocator.buffer();
        final ByteBuf ethernetBuf = byteBufAllocator.buffer();
        final ByteBuf pcap = byteBufAllocator.buffer();
        try {
            if (srcAddr.getAddress() instanceof Inet4Address && dstAddr.getAddress() instanceof Inet4Address) {
                IPPacket.writeUDPv4(ipBuf, udpBuf, NetUtil.ipv4AddressToInt((Inet4Address)srcAddr.getAddress()), NetUtil.ipv4AddressToInt((Inet4Address)dstAddr.getAddress()));
                EthernetPacket.writeIPv4(ethernetBuf, ipBuf);
            }
            else {
                if (!(srcAddr.getAddress() instanceof Inet6Address) || !(dstAddr.getAddress() instanceof Inet6Address)) {
                    this.logger.error("Source and Destination IP Address versions are not same. Source Address: {}, Destination Address: {}", srcAddr.getAddress(), dstAddr.getAddress());
                    return;
                }
                IPPacket.writeUDPv6(ipBuf, udpBuf, srcAddr.getAddress().getAddress(), dstAddr.getAddress().getAddress());
                EthernetPacket.writeIPv6(ethernetBuf, ipBuf);
            }
            this.pCapWriter.writePacket(pcap, ethernetBuf);
        }
        catch (final IOException ex) {
            this.logger.error("Caught Exception While Writing Packet into Pcap", ex);
            ctx.fireExceptionCaught((Throwable)ex);
        }
        finally {
            ipBuf.release();
            ethernetBuf.release();
            pcap.release();
        }
    }
    
    private static InetSocketAddress getLocalAddress(final Channel ch, final InetSocketAddress remote) {
        final InetSocketAddress local = (InetSocketAddress)ch.localAddress();
        if (remote != null && local.getAddress().isAnyLocalAddress()) {
            if (local.getAddress() instanceof Inet4Address && remote.getAddress() instanceof Inet6Address) {
                return new InetSocketAddress(WildcardAddressHolder.wildcard6, local.getPort());
            }
            if (local.getAddress() instanceof Inet6Address && remote.getAddress() instanceof Inet4Address) {
                return new InetSocketAddress(WildcardAddressHolder.wildcard4, local.getPort());
            }
        }
        return local;
    }
    
    @Override
    public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
        if (this.channelType == ChannelType.TCP && this.state.get() == State.WRITING) {
            this.logger.debug("Starting Fake TCP FIN+ACK Flow to close connection");
            final ByteBufAllocator byteBufAllocator = ctx.alloc();
            final ByteBuf tcpBuf = byteBufAllocator.buffer();
            try {
                long initiatorSegmentNumber = this.isServerPipeline ? this.receiveSegmentNumber : this.sendSegmentNumber;
                long initiatorAckNumber = this.isServerPipeline ? this.sendSegmentNumber : this.receiveSegmentNumber;
                TCPPacket.writePacket(tcpBuf, null, initiatorSegmentNumber, initiatorAckNumber, this.initiatorAddr.getPort(), this.handlerAddr.getPort(), TCPPacket.TCPFlag.FIN, TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(this.initiatorAddr, this.handlerAddr, tcpBuf, byteBufAllocator, ctx);
                TCPPacket.writePacket(tcpBuf, null, initiatorAckNumber, initiatorSegmentNumber, this.handlerAddr.getPort(), this.initiatorAddr.getPort(), TCPPacket.TCPFlag.FIN, TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(this.handlerAddr, this.initiatorAddr, tcpBuf, byteBufAllocator, ctx);
                this.sendSegmentNumber = incrementUintSegmentNumber(this.sendSegmentNumber, 1);
                this.receiveSegmentNumber = incrementUintSegmentNumber(this.receiveSegmentNumber, 1);
                initiatorSegmentNumber = (this.isServerPipeline ? this.receiveSegmentNumber : this.sendSegmentNumber);
                initiatorAckNumber = (this.isServerPipeline ? this.sendSegmentNumber : this.receiveSegmentNumber);
                TCPPacket.writePacket(tcpBuf, null, initiatorSegmentNumber, initiatorAckNumber, this.initiatorAddr.getPort(), this.handlerAddr.getPort(), TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(this.initiatorAddr, this.handlerAddr, tcpBuf, byteBufAllocator, ctx);
            }
            finally {
                tcpBuf.release();
            }
            this.logger.debug("Finished Fake TCP FIN+ACK Flow to close connection");
        }
        this.close();
        super.handlerRemoved(ctx);
    }
    
    @Override
    public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
        if (this.channelType == ChannelType.TCP && this.state.get() == State.WRITING) {
            final ByteBuf tcpBuf = ctx.alloc().buffer();
            try {
                TCPPacket.writePacket(tcpBuf, null, this.sendSegmentNumber, this.receiveSegmentNumber, this.initiatorAddr.getPort(), this.handlerAddr.getPort(), TCPPacket.TCPFlag.RST, TCPPacket.TCPFlag.ACK);
                this.completeTCPWrite(this.initiatorAddr, this.handlerAddr, tcpBuf, ctx.alloc(), ctx);
            }
            finally {
                tcpBuf.release();
            }
            this.logger.debug("Sent Fake TCP RST to close connection");
        }
        this.close();
        ctx.fireExceptionCaught(cause);
    }
    
    private void logTCP(final boolean isWriteOperation, final int bytes, final long sendSegmentNumber, final long receiveSegmentNumber, final InetSocketAddress srcAddr, final InetSocketAddress dstAddr, final boolean ackOnly) {
        if (this.logger.isDebugEnabled()) {
            if (ackOnly) {
                this.logger.debug("Writing TCP ACK, isWriteOperation {}, Segment Number {}, Ack Number {}, Src Addr {}, Dst Addr {}", isWriteOperation, sendSegmentNumber, receiveSegmentNumber, dstAddr, srcAddr);
            }
            else {
                this.logger.debug("Writing TCP Data of {} Bytes, isWriteOperation {}, Segment Number {}, Ack Number {}, Src Addr {}, Dst Addr {}", bytes, isWriteOperation, sendSegmentNumber, receiveSegmentNumber, srcAddr, dstAddr);
            }
        }
    }
    
    OutputStream outputStream() {
        return this.outputStream;
    }
    
    boolean sharedOutputStream() {
        return this.sharedOutputStream;
    }
    
    boolean writePcapGlobalHeader() {
        return this.writePcapGlobalHeader;
    }
    
    public boolean isWriting() {
        return this.state.get() == State.WRITING;
    }
    
    State state() {
        return this.state.get();
    }
    
    public void pause() {
        if (!this.state.compareAndSet(State.WRITING, State.PAUSED)) {
            throw new IllegalStateException("State must be 'STARTED' to pause but current state is: " + this.state);
        }
    }
    
    public void resume() {
        if (!this.state.compareAndSet(State.PAUSED, State.WRITING)) {
            throw new IllegalStateException("State must be 'PAUSED' to resume but current state is: " + this.state);
        }
    }
    
    void markClosed() {
        if (this.state.get() != State.CLOSED) {
            this.state.set(State.CLOSED);
        }
    }
    
    PcapWriter pCapWriter() {
        return this.pCapWriter;
    }
    
    private void logDiscard() {
        this.logger.warn("Discarding pcap write because channel type is unknown. The channel this handler is registered on is not a SocketChannel or DatagramChannel, so the inference does not work. Please call forceTcpChannel or forceUdpChannel before registering the handler.");
    }
    
    @Override
    public String toString() {
        return "PcapWriteHandler{captureZeroByte=" + this.captureZeroByte + ", writePcapGlobalHeader=" + this.writePcapGlobalHeader + ", sharedOutputStream=" + this.sharedOutputStream + ", sendSegmentNumber=" + this.sendSegmentNumber + ", receiveSegmentNumber=" + this.receiveSegmentNumber + ", channelType=" + this.channelType + ", initiatorAddr=" + this.initiatorAddr + ", handlerAddr=" + this.handlerAddr + ", isServerPipeline=" + this.isServerPipeline + ", state=" + this.state + '}';
    }
    
    @Override
    public void close() throws IOException {
        if (this.state.get() == State.CLOSED) {
            this.logger.debug("PcapWriterHandler is already closed");
        }
        else {
            if (this.pCapWriter == null) {
                this.pCapWriter = new PcapWriter(this);
            }
            this.pCapWriter.close();
            this.markClosed();
            this.logger.debug("PcapWriterHandler is now closed");
        }
    }
    
    public static Builder builder() {
        return new Builder();
    }
    
    private enum ChannelType
    {
        TCP, 
        UDP;
    }
    
    public static final class Builder
    {
        private boolean captureZeroByte;
        private boolean sharedOutputStream;
        private boolean writePcapGlobalHeader;
        private ChannelType channelType;
        private InetSocketAddress initiatorAddr;
        private InetSocketAddress handlerAddr;
        private boolean isServerPipeline;
        
        private Builder() {
            this.writePcapGlobalHeader = true;
        }
        
        public Builder captureZeroByte(final boolean captureZeroByte) {
            this.captureZeroByte = captureZeroByte;
            return this;
        }
        
        public Builder sharedOutputStream(final boolean sharedOutputStream) {
            this.sharedOutputStream = sharedOutputStream;
            return this;
        }
        
        public Builder writePcapGlobalHeader(final boolean writePcapGlobalHeader) {
            this.writePcapGlobalHeader = writePcapGlobalHeader;
            return this;
        }
        
        public Builder forceTcpChannel(final InetSocketAddress serverAddress, final InetSocketAddress clientAddress, final boolean isServerPipeline) {
            this.channelType = ChannelType.TCP;
            this.handlerAddr = ObjectUtil.checkNotNull(serverAddress, "serverAddress");
            this.initiatorAddr = ObjectUtil.checkNotNull(clientAddress, "clientAddress");
            this.isServerPipeline = isServerPipeline;
            return this;
        }
        
        public Builder forceUdpChannel(final InetSocketAddress localAddress, final InetSocketAddress remoteAddress) {
            this.channelType = ChannelType.UDP;
            this.handlerAddr = ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
            this.initiatorAddr = ObjectUtil.checkNotNull(localAddress, "localAddress");
            return this;
        }
        
        public PcapWriteHandler build(final OutputStream outputStream) {
            ObjectUtil.checkNotNull(outputStream, "outputStream");
            return new PcapWriteHandler(this, outputStream, null);
        }
    }
    
    private static final class WildcardAddressHolder
    {
        static final InetAddress wildcard4;
        static final InetAddress wildcard6;
        
        static {
            try {
                wildcard4 = InetAddress.getByAddress(new byte[4]);
                wildcard6 = InetAddress.getByAddress(new byte[16]);
            }
            catch (final UnknownHostException e) {
                throw new AssertionError((Object)e);
            }
        }
    }
}
