/*
 * Copyright 2015-2025 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package sockslib.server.msg;

import sockslib.common.AddressType;
import sockslib.common.SocksCommand;
import sockslib.common.SocksException;
import sockslib.utils.SocksUtil;
import sockslib.utils.StreamUtil;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;

import static sockslib.utils.StreamUtil.checkEnd;

/**
 * The class <code>RequestCommandMessage</code> represents a SOCKS5 command message.
 *
 * @author Youchao Feng
 * @version 1.0
 * @date Apr 6, 2015 11:10:12 AM
 */
public class CommandMessage implements ReadableMessage, WritableMessage {

  /**
   * Value of CONNECT command.
   */
  protected static final int CMD_CONNECT = 0x01;

  /**
   * Value of BIND command.
   */
  protected static final int CMD_BIND = 0x02;

  /**
   * Value of UDP ASSOCIATE command.
   */
  protected static final int CMD_UDP_ASSOCIATE = 0x03;

  /**
   * Value of RESERVED field.
   */
  private static final int RESERVED = 0x00;

  /**
   * Version.
   */
  private int version;

  /**
   * IP address of destination.
   */
  private InetAddress inetAddress;

  /**
   * Port of destination.
   */
  private int port;

  /**
   * Host of destination.
   */
  private String host;

  /**
   * SOCKS command.
   */
  private SocksCommand command;

  /**
   * Reserved field.
   */
  private int reserved;

  /**
   * Address type.
   */
  private int addressType;

  /**
   * SOCKS exception in Command message.
   */
  private SocksException socksException;

  @Override
  public int getLength() {
    return getBytes().length;
  }

  @Override
  public byte[] getBytes() {
    byte[] bytes = null;

    switch (addressType) {
      case AddressType.IPV4:
        bytes = new byte[10];
        byte[] ipv4Bytes = inetAddress.getAddress();// todo
        System.arraycopy(ipv4Bytes, 0, bytes, 4, ipv4Bytes.length);
        bytes[8] = SocksUtil.getFirstByteFromInt(port);
        bytes[9] = SocksUtil.getSecondByteFromInt(port);
        break;

      case AddressType.IPV6:
        bytes = new byte[22];
        byte[] ipv6Bytes = inetAddress.getAddress();// todo
        System.arraycopy(ipv6Bytes, 0, bytes, 4, ipv6Bytes.length);
        bytes[20] = SocksUtil.getFirstByteFromInt(port);
        bytes[21] = SocksUtil.getSecondByteFromInt(port);
        break;

      case AddressType.DOMAIN_NAME:
        final int hostLength = host.getBytes().length;
        bytes = new byte[7 + hostLength];
        bytes[4] = (byte) hostLength;
        for (int i = 0; i < hostLength; i++) {
          bytes[5 + i] = host.getBytes()[i];
        }
        bytes[5 + hostLength] = SocksUtil.getFirstByteFromInt(port);
        bytes[6 + hostLength] = SocksUtil.getSecondByteFromInt(port);
        break;
      default:
        break;
    }

    bytes[0] = (byte) version;
    bytes[1] = (byte) command.getValue();
    bytes[2] = RESERVED;
    bytes[3] = (byte) addressType;

    return bytes;
  }

  @Override
  public void read(InputStream inputStream) throws SocksException, IOException {

    version = checkEnd(inputStream.read());
    int cmd = checkEnd(inputStream.read());

    switch (cmd) {
      case CMD_CONNECT:
        command = SocksCommand.CONNECT;
        break;
      case CMD_BIND:
        command = SocksCommand.BIND;
        break;
      case CMD_UDP_ASSOCIATE:
        command = SocksCommand.UDP_ASSOCIATE;
        break;

      default:
        socksException = SocksException.serverReplyException(ServerReply.COMMAND_NOT_SUPPORTED);
    }
    reserved = checkEnd(inputStream.read());
    addressType = checkEnd(inputStream.read());

    if (!AddressType.isSupport(addressType) && socksException == null) {
      socksException = SocksException.serverReplyException(ServerReply.ADDRESS_TYPE_NOT_SUPPORTED);
    }

    // read address
    switch (addressType) {

      case AddressType.IPV4:
        byte[] addressBytes = StreamUtil.read(inputStream, 4);
        inetAddress = InetAddress.getByAddress(addressBytes);
        break;

      case AddressType.DOMAIN_NAME:
        int domainLength = checkEnd(inputStream.read());
        if (domainLength < 1) {
          throw new SocksException("Length of domain must great than 0");
        }
        byte[] domainBytes = StreamUtil.read(inputStream, domainLength);
        host = new String(domainBytes, Charset.forName("UTF-8"));
        try {
          inetAddress = InetAddress.getByName(host);
        } catch (UnknownHostException e) {
          if (socksException == null) {
            socksException = SocksException.serverReplyException(ServerReply.HOST_UNREACHABLE);
          }
        }
        break;
      default:
        // TODO Implement later.
        break;
    }

    // Read port
    byte[] portBytes = StreamUtil.read(inputStream, 2);
    port = SocksUtil.bytesToInt(portBytes);

  }

  /**
   * Returns version.
   *
   * @return Version.
   */
  public int getVersion() {
    return version;
  }

  /**
   * Sets version.
   *
   * @param version Version.
   */
  public void setVersion(int version) {
    this.version = version;
  }

  public boolean hasSocksException() {
    return socksException != null;
  }

  public InetAddress getInetAddress() {
    return inetAddress;
  }

  public void setInetAddress(InetAddress inetAddress) {
    this.inetAddress = inetAddress;
  }

  public int getPort() {
    return port;
  }

  public void setPort(int port) {
    this.port = port;
  }

  public String getHost() {
    return host;
  }

  public void setHost(String host) {
    this.host = host;
  }

  public SocksCommand getCommand() {
    return command;
  }

  public void setCommand(SocksCommand command) {
    this.command = command;
  }

  public int getReserved() {
    return reserved;
  }

  public void setReserved(int reserved) {
    this.reserved = reserved;
  }

  public int getAddressType() {
    return addressType;
  }

  public void setAddressType(int addressType) {
    this.addressType = addressType;
  }

  public SocketAddress getSocketAddress() {
    return new InetSocketAddress(inetAddress, port);
  }

  public SocksException getSocksException() {
    return socksException;
  }

  public void setSocksException(SocksException socksException) {
    this.socksException = socksException;
  }

}

@@ -4706,32 +4706,205 @@
         break;%0A%0A
+      case AddressType.IPV6:%0A        byte%5B%5D addressBytes6 = StreamUtil.read(inputStream, 16);%0A        inetAddress = InetAddress.getByAddress(addressBytes6);%0A        break;%0A%0A
       case Addre

support ipv6 addresses in command message