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

package com.hypixel.hytale.server.core.io.transport;

import io.netty.bootstrap.AbstractBootstrap;
import javax.annotation.Nullable;
import java.security.cert.Certificate;
import javax.net.ssl.SSLEngine;
import java.time.Duration;
import com.hypixel.hytale.server.core.io.netty.HytaleChannelInitializer;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import com.hypixel.hytale.protocol.packets.connection.Disconnect;
import com.hypixel.hytale.protocol.packets.connection.DisconnectType;
import com.hypixel.hytale.protocol.io.netty.ProtocolUtil;
import io.netty.handler.codec.quic.QuicChannel;
import io.netty.handler.codec.quic.QuicCongestionControlAlgorithm;
import io.netty.handler.codec.quic.QuicTokenHandler;
import io.netty.handler.codec.quic.InsecureQuicTokenHandler;
import io.netty.handler.codec.quic.QuicServerCodecBuilder;
import com.hypixel.hytale.server.core.HytaleServer;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.concurrent.TimeUnit;
import java.net.Inet6Address;
import java.net.SocketAddress;
import java.net.Inet4Address;
import io.netty.channel.ChannelFuture;
import java.net.InetSocketAddress;
import io.netty.channel.socket.DatagramChannel;
import io.netty.handler.codec.quic.QuicSslContext;
import io.netty.channel.ChannelHandler;
import java.net.SocketOption;
import io.netty.channel.socket.nio.NioChannelOption;
import jdk.net.ExtendedSocketOptions;
import io.netty.channel.ChannelOption;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.socket.SocketProtocolFamily;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.codec.quic.QuicSslContextBuilder;
import com.hypixel.hytale.server.core.auth.CertificateUtil;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.auth.ServerAuthManager;
import java.security.cert.CertificateException;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
import io.netty.bootstrap.Bootstrap;
import javax.annotation.Nonnull;
import io.netty.channel.EventLoopGroup;
import java.security.cert.X509Certificate;
import io.netty.util.AttributeKey;
import com.hypixel.hytale.logger.HytaleLogger;

public class QUICTransport implements Transport
{
    private static final HytaleLogger LOGGER;
    public static final AttributeKey<X509Certificate> CLIENT_CERTIFICATE_ATTR;
    public static final AttributeKey<Integer> ALPN_REJECT_ERROR_CODE_ATTR;
    @Nonnull
    private final EventLoopGroup workerGroup;
    private final Bootstrap bootstrapIpv4;
    private final Bootstrap bootstrapIpv6;
    
    public QUICTransport() throws InterruptedException {
        this.workerGroup = NettyUtil.getEventLoopGroup("ServerWorkerGroup");
        SelfSignedCertificate ssc = null;
        try {
            ssc = new SelfSignedCertificate("localhost");
        }
        catch (final CertificateException e) {
            throw new RuntimeException(e);
        }
        ServerAuthManager.getInstance().setServerCertificate(ssc.cert());
        QUICTransport.LOGGER.at(Level.INFO).log("Server certificate registered for mutual auth, fingerprint: %s", CertificateUtil.computeCertificateFingerprint(ssc.cert()));
        final QuicSslContext sslContext = QuicSslContextBuilder.forServer(ssc.key(), null, ssc.cert()).applicationProtocols("hytale/2", "hytale/1").earlyData(false).clientAuth(ClientAuth.REQUIRE).trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        final NettyUtil.ReflectiveChannelFactory<? extends DatagramChannel> channelFactoryIpv4 = NettyUtil.getDatagramChannelFactory(SocketProtocolFamily.INET);
        QUICTransport.LOGGER.at(Level.INFO).log("Using IPv4 Datagram Channel: %s...", channelFactoryIpv4.getSimpleName());
        this.bootstrapIpv4 = ((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)new Bootstrap()).group(this.workerGroup).channelFactory(channelFactoryIpv4)).option(ChannelOption.SO_REUSEADDR, true)).option((ChannelOption<Boolean>)NioChannelOption.of((SocketOption<T>)ExtendedSocketOptions.IP_DONTFRAGMENT), true)).handler(new QuicChannelInboundHandlerAdapter(sslContext)).validate();
        final NettyUtil.ReflectiveChannelFactory<? extends DatagramChannel> channelFactoryIpv5 = NettyUtil.getDatagramChannelFactory(SocketProtocolFamily.INET6);
        QUICTransport.LOGGER.at(Level.INFO).log("Using IPv6 Datagram Channel: %s...", channelFactoryIpv5.getSimpleName());
        this.bootstrapIpv6 = ((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)new Bootstrap()).group(this.workerGroup).channelFactory(channelFactoryIpv5)).option(ChannelOption.SO_REUSEADDR, true)).option((ChannelOption<Boolean>)NioChannelOption.of((SocketOption<T>)ExtendedSocketOptions.IP_DONTFRAGMENT), true)).handler(new QuicChannelInboundHandlerAdapter(sslContext)).validate();
        this.bootstrapIpv4.register().sync();
        this.bootstrapIpv6.register().sync();
    }
    
    @Nonnull
    @Override
    public TransportType getType() {
        return TransportType.QUIC;
    }
    
    @Override
    public ChannelFuture bind(@Nonnull final InetSocketAddress address) throws InterruptedException {
        if (address.getAddress() instanceof Inet4Address) {
            return this.bootstrapIpv4.bind(address).sync();
        }
        if (address.getAddress() instanceof Inet6Address) {
            return this.bootstrapIpv6.bind(address).sync();
        }
        throw new UnsupportedOperationException("Unsupported address type: " + String.valueOf(address.getAddress().getClass()));
    }
    
    @Override
    public void shutdown() {
        QUICTransport.LOGGER.at(Level.INFO).log("Shutting down workerGroup...");
        try {
            this.workerGroup.shutdownGracefully(0L, 1L, TimeUnit.SECONDS).await(1L, TimeUnit.SECONDS);
        }
        catch (final InterruptedException e) {
            QUICTransport.LOGGER.at(Level.SEVERE).withCause(e).log("Failed to await for listener to close!");
            Thread.currentThread().interrupt();
        }
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        CLIENT_CERTIFICATE_ATTR = AttributeKey.valueOf("CLIENT_CERTIFICATE");
        ALPN_REJECT_ERROR_CODE_ATTR = AttributeKey.valueOf("ALPN_REJECT_ERROR_CODE");
    }
    
    private static class QuicChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter
    {
        private final QuicSslContext sslContext;
        
        public QuicChannelInboundHandlerAdapter(final QuicSslContext sslContext) {
            this.sslContext = sslContext;
        }
        
        @Override
        public boolean isSharable() {
            return true;
        }
        
        @Override
        public void channelActive(@Nonnull final ChannelHandlerContext ctx) throws Exception {
            final Duration playTimeout = HytaleServer.get().getConfig().getConnectionTimeouts().getPlay();
            final ChannelHandler quicHandler = new QuicServerCodecBuilder().sslContext(this.sslContext).tokenHandler(InsecureQuicTokenHandler.INSTANCE).maxIdleTimeout(playTimeout.toMillis(), TimeUnit.MILLISECONDS).ackDelayExponent(3L).initialMaxData(524288L).initialMaxStreamDataUnidirectional(0L).initialMaxStreamsUnidirectional(0L).initialMaxStreamDataBidirectionalLocal(131072L).initialMaxStreamDataBidirectionalRemote(131072L).initialMaxStreamsBidirectional(1L).discoverPmtu(true).congestionControlAlgorithm(QuicCongestionControlAlgorithm.BBR).handler(new ChannelInboundHandlerAdapter() {
                @Override
                public boolean isSharable() {
                    return true;
                }
                
                @Override
                public void channelActive(@Nonnull final ChannelHandlerContext ctx) throws Exception {
                    final QuicChannel channel = (QuicChannel)ctx.channel();
                    QUICTransport.LOGGER.at(Level.INFO).log("Received connection from %s to %s", NettyUtil.formatRemoteAddress(channel), NettyUtil.formatLocalAddress(channel));
                    final String negotiatedAlpn = channel.sslEngine().getApplicationProtocol();
                    final int negotiatedVersion = this.parseProtocolVersion(negotiatedAlpn);
                    if (negotiatedVersion < 2) {
                        QUICTransport.LOGGER.at(Level.INFO).log("Marking connection from %s for rejection: ALPN %s < required %d", NettyUtil.formatRemoteAddress(channel), negotiatedAlpn, 2);
                        channel.attr(QUICTransport.ALPN_REJECT_ERROR_CODE_ATTR).set(5);
                    }
                    final X509Certificate clientCert = QuicChannelInboundHandlerAdapter.this.extractClientCertificate(channel);
                    if (clientCert == null) {
                        QUICTransport.LOGGER.at(Level.WARNING).log("Connection rejected: no client certificate from %s", NettyUtil.formatRemoteAddress(channel));
                        ProtocolUtil.closeConnection(channel);
                        return;
                    }
                    channel.attr(QUICTransport.CLIENT_CERTIFICATE_ATTR).set(clientCert);
                    QUICTransport.LOGGER.at(Level.FINE).log("Client certificate: %s", clientCert.getSubjectX500Principal().getName());
                }
                
                private int parseProtocolVersion(final String alpn) {
                    if (alpn != null && alpn.startsWith("hytale/")) {
                        try {
                            return Integer.parseInt(alpn.substring(7));
                        }
                        catch (final NumberFormatException e) {
                            return 0;
                        }
                    }
                    return 0;
                }
                
                @Override
                public void channelInactive(@Nonnull final ChannelHandlerContext ctx) {
                    ((QuicChannel)ctx.channel()).collectStats().addListener(f -> {
                        if (f.isSuccess()) {
                            QUICTransport.LOGGER.at(Level.INFO).log("Connection closed: %s", f.getNow());
                        }
                    });
                }
                
                @Override
                public void exceptionCaught(@Nonnull final ChannelHandlerContext ctx, final Throwable cause) {
                    QUICTransport.LOGGER.at(Level.WARNING).withCause(cause).log("Got exception from netty pipeline in ChannelInitializer!");
                    final Channel channel = ctx.channel();
                    if (channel.isWritable()) {
                        channel.writeAndFlush(new Disconnect("Internal server error!", DisconnectType.Crash)).addListener((GenericFutureListener<? extends Future<? super Void>>)ProtocolUtil.CLOSE_ON_COMPLETE);
                    }
                    else {
                        ProtocolUtil.closeApplicationConnection(channel);
                    }
                }
            }).streamHandler(new HytaleChannelInitializer()).build();
            ctx.channel().pipeline().addLast(quicHandler);
        }
        
        @Nullable
        private X509Certificate extractClientCertificate(final QuicChannel channel) {
            try {
                final SSLEngine sslEngine = channel.sslEngine();
                if (sslEngine == null) {
                    return null;
                }
                final Certificate[] peerCerts = sslEngine.getSession().getPeerCertificates();
                if (peerCerts != null && peerCerts.length > 0 && peerCerts[0] instanceof X509Certificate) {
                    return (X509Certificate)peerCerts[0];
                }
            }
            catch (final Exception e) {
                QUICTransport.LOGGER.at(Level.FINEST).log("No peer certificate available: %s", e.getMessage());
            }
            return null;
        }
    }
}
