/**
   * Encodes the authentication packet for supported authentication methods.
   *
   * @param request the socks proxy request data
   * @return the encoded buffer, if null then authentication step is over and handshake process can
   *     jump immediately to the next step without waiting for a server reply.
   * @throws UnsupportedEncodingException if some string charset convertion fails
   * @throws GSSException when something fails while using GSSAPI
   */
  private IoBuffer encodeAuthenticationPacket(final SocksProxyRequest request)
      throws UnsupportedEncodingException, GSSException {
    byte method = (Byte) getSession().getAttribute(Socks5LogicHandler.SELECTED_AUTH_METHOD);

    switch (method) {
      case SocksProxyConstants.NO_AUTH:
        // In this case authentication is immediately considered as successfull
        // Next writeRequest() call will send the proxy request
        getSession().setAttribute(HANDSHAKE_STEP, SocksProxyConstants.SOCKS5_REQUEST_STEP);
        break;

      case SocksProxyConstants.GSSAPI_AUTH:
        return encodeGSSAPIAuthenticationPacket(request);

      case SocksProxyConstants.BASIC_AUTH:
        // The basic auth scheme packet is sent
        byte[] user = request.getUserName().getBytes("ASCII");
        byte[] pwd = request.getPassword().getBytes("ASCII");
        IoBuffer buf = IoBuffer.allocate(3 + user.length + pwd.length);

        buf.put(SocksProxyConstants.BASIC_AUTH_SUBNEGOTIATION_VERSION);
        buf.put((byte) user.length);
        buf.put(user);
        buf.put((byte) pwd.length);
        buf.put(pwd);

        return buf;
    }

    return null;
  }
  /**
   * Encodes the initial greeting packet.
   *
   * @param request the socks proxy request data
   * @return the encoded buffer
   */
  private IoBuffer encodeInitialGreetingPacket(final SocksProxyRequest request) {
    byte nbMethods = (byte) SocksProxyConstants.SUPPORTED_AUTH_METHODS.length;
    IoBuffer buf = IoBuffer.allocate(2 + nbMethods);

    buf.put(request.getProtocolVersion());
    buf.put(nbMethods);
    buf.put(SocksProxyConstants.SUPPORTED_AUTH_METHODS);

    return buf;
  }
  /**
   * Encodes the proxy authorization request packet.
   *
   * @param request the socks proxy request data
   * @return the encoded buffer
   * @throws UnsupportedEncodingException if request's hostname charset can't be converted to ASCII.
   */
  private IoBuffer encodeProxyRequestPacket(final SocksProxyRequest request)
      throws UnsupportedEncodingException {
    int len = 6;
    InetSocketAddress adr = request.getEndpointAddress();
    byte addressType = 0;
    byte[] host = null;

    if (adr != null && !adr.isUnresolved()) {
      if (adr.getAddress() instanceof Inet6Address) {
        len += 16;
        addressType = SocksProxyConstants.IPV6_ADDRESS_TYPE;
      } else if (adr.getAddress() instanceof Inet4Address) {
        len += 4;
        addressType = SocksProxyConstants.IPV4_ADDRESS_TYPE;
      }
    } else {
      host = request.getHost() != null ? request.getHost().getBytes("ASCII") : null;

      if (host != null) {
        len += 1 + host.length;
        addressType = SocksProxyConstants.DOMAIN_NAME_ADDRESS_TYPE;
      } else {
        throw new IllegalArgumentException(
            "SocksProxyRequest object " + "has no suitable endpoint information");
      }
    }

    IoBuffer buf = IoBuffer.allocate(len);

    buf.put(request.getProtocolVersion());
    buf.put(request.getCommandCode());
    buf.put((byte) 0x00); // Reserved
    buf.put(addressType);

    if (host == null) {
      buf.put(request.getIpAddress());
    } else {
      buf.put((byte) host.length);
      buf.put(host);
    }

    buf.put(request.getPort());

    return buf;
  }
  /**
   * Encode a SOCKS4/SOCKS4a request and writes it to the next filter so it can be sent to the proxy
   * server.
   *
   * @param nextFilter the next filter
   * @param request the request to send.
   */
  protected void writeRequest(final NextFilter nextFilter, final SocksProxyRequest request) {
    try {
      boolean isV4ARequest = Arrays.equals(request.getIpAddress(), SocksProxyConstants.FAKE_IP);
      byte[] userID = request.getUserName().getBytes("ASCII");
      byte[] host = isV4ARequest ? request.getHost().getBytes("ASCII") : null;

      int len = 9 + userID.length;

      if (isV4ARequest) {
        len += host.length + 1;
      }

      IoBuffer buf = IoBuffer.allocate(len);

      buf.put(request.getProtocolVersion());
      buf.put(request.getCommandCode());
      buf.put(request.getPort());
      buf.put(request.getIpAddress());
      buf.put(userID);
      buf.put(SocksProxyConstants.TERMINATOR);

      if (isV4ARequest) {
        buf.put(host);
        buf.put(SocksProxyConstants.TERMINATOR);
      }

      if (isV4ARequest) {
        logger.debug("  sending SOCKS4a request");
      } else {
        logger.debug("  sending SOCKS4 request");
      }

      buf.flip();
      writeData(nextFilter, buf);
    } catch (Exception ex) {
      closeSession("Unable to send Socks request: ", ex);
    }
  }
  /**
   * Encodes the authentication packet for supported authentication methods.
   *
   * @param request the socks proxy request data
   * @return the encoded buffer
   * @throws GSSException when something fails while using GSSAPI
   */
  private IoBuffer encodeGSSAPIAuthenticationPacket(final SocksProxyRequest request)
      throws GSSException {
    GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT);
    if (ctx == null) {
      // first step in the authentication process
      GSSManager manager = GSSManager.getInstance();
      GSSName serverName = manager.createName(request.getServiceKerberosName(), null);
      Oid krb5OID = new Oid(SocksProxyConstants.KERBEROS_V5_OID);

      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Available mechs:");
        for (Oid o : manager.getMechs()) {
          if (o.equals(krb5OID)) {
            LOGGER.debug("Found Kerberos V OID available");
          }
          LOGGER.debug("{} with oid = {}", manager.getNamesForMech(o), o);
        }
      }

      ctx = manager.createContext(serverName, krb5OID, null, GSSContext.DEFAULT_LIFETIME);

      ctx.requestMutualAuth(true); // Mutual authentication
      ctx.requestConf(false);
      ctx.requestInteg(false);

      getSession().setAttribute(GSS_CONTEXT, ctx);
    }

    byte[] token = (byte[]) getSession().getAttribute(GSS_TOKEN);
    if (token != null) {
      LOGGER.debug("  Received Token[{}] = {}", token.length, ByteUtilities.asHex(token));
    }
    IoBuffer buf = null;

    if (!ctx.isEstablished()) {
      // token is ignored on the first call
      if (token == null) {
        token = new byte[32];
      }

      token = ctx.initSecContext(token, 0, token.length);

      // Send a token to the server if one was generated by
      // initSecContext
      if (token != null) {
        LOGGER.debug("  Sending Token[{}] = {}", token.length, ByteUtilities.asHex(token));

        getSession().setAttribute(GSS_TOKEN, token);
        buf = IoBuffer.allocate(4 + token.length);
        buf.put(
            new byte[] {
              SocksProxyConstants.GSSAPI_AUTH_SUBNEGOTIATION_VERSION,
              SocksProxyConstants.GSSAPI_MSG_TYPE
            });

        buf.put(ByteUtilities.intToNetworkByteOrder(token.length, 2));
        buf.put(token);
      }
    }

    return buf;
  }