blob: 9d3e72eff805a487f205b839cd622709151473bf [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.core5.reactor;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.UnresolvedAddressException;
import java.nio.charset.StandardCharsets;
import org.apache.hc.core5.http.nio.command.CommandSupport;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.io.SocketTimeoutExceptionFactory;
import org.apache.hc.core5.util.Timeout;
/**
* Implements the client side of SOCKS protocol version 5 as per https://tools.ietf.org/html/rfc1928. Supports SOCKS username/password
* authentication as per https://tools.ietf.org/html/rfc1929.
*/
final class SocksProxyProtocolHandler implements IOEventHandler {
private static final int MAX_COMMAND_CONNECT_LENGTH = 22;
private static final byte CLIENT_VERSION = 5;
private static final byte NO_AUTHENTICATION_REQUIRED = 0;
private static final byte USERNAME_PASSWORD = 2;
private static final byte USERNAME_PASSWORD_VERSION = 1;
private static final byte SUCCESS = 0;
private static final byte COMMAND_CONNECT = 1;
private static final byte ATYP_IPV4 = 1;
private static final byte ATYP_DOMAINNAME = 3;
private static final byte ATYP_IPV6 = 4;
private enum State {
SEND_AUTH, RECEIVE_AUTH_METHOD, SEND_USERNAME_PASSWORD, RECEIVE_AUTH, SEND_CONNECT, RECEIVE_RESPONSE_CODE, RECEIVE_ADDRESS_TYPE, RECEIVE_ADDRESS, COMPLETE
}
private final ProtocolIOSession ioSession;
private final Object attachment;
private final InetSocketAddress targetAddress;
private final String username;
private final String password;
private final IOEventHandlerFactory eventHandlerFactory;
// a 32 byte buffer is enough for all usual SOCKS negotiations, we expand it if necessary during the processing
private ByteBuffer buffer = ByteBuffer.allocate(32);
private State state = State.SEND_AUTH;
private int remainingResponseSize = -1;
SocksProxyProtocolHandler(final ProtocolIOSession ioSession, final Object attachment, final InetSocketAddress targetAddress,
final String username, final String password, final IOEventHandlerFactory eventHandlerFactory) {
this.ioSession = ioSession;
this.attachment = attachment;
this.targetAddress = targetAddress;
this.username = username;
this.password = password;
this.eventHandlerFactory = eventHandlerFactory;
}
@Override
public void connected(final IOSession session) throws IOException {
this.buffer.put(CLIENT_VERSION);
this.buffer.put((byte) 1);
this.buffer.put(NO_AUTHENTICATION_REQUIRED);
this.buffer.flip();
session.setEventMask(SelectionKey.OP_WRITE);
}
@Override
public void outputReady(final IOSession session) throws IOException {
switch (this.state) {
case SEND_AUTH:
if (writeAndPrepareRead(session, 2)) {
session.setEventMask(SelectionKey.OP_READ);
this.state = State.RECEIVE_AUTH_METHOD;
}
break;
case SEND_USERNAME_PASSWORD:
if (writeAndPrepareRead(session, 2)) {
session.setEventMask(SelectionKey.OP_READ);
this.state = State.RECEIVE_AUTH;
}
break;
case SEND_CONNECT:
if (writeAndPrepareRead(session, 2)) {
session.setEventMask(SelectionKey.OP_READ);
this.state = State.RECEIVE_RESPONSE_CODE;
}
break;
case RECEIVE_AUTH_METHOD:
case RECEIVE_AUTH:
case RECEIVE_ADDRESS:
case RECEIVE_ADDRESS_TYPE:
case RECEIVE_RESPONSE_CODE:
session.setEventMask(SelectionKey.OP_READ);
break;
case COMPLETE:
break;
}
}
@Override
public void inputReady(final IOSession session, final ByteBuffer src) throws IOException {
if (src != null) {
try {
this.buffer.put(src);
} catch (final BufferOverflowException ex) {
throw new IOException("Unexpected input data");
}
}
switch (this.state) {
case RECEIVE_AUTH_METHOD:
if (fillBuffer(session)) {
this.buffer.flip();
final byte serverVersion = this.buffer.get();
final byte serverMethod = this.buffer.get();
if (serverVersion != CLIENT_VERSION) {
throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
}
if (serverMethod == USERNAME_PASSWORD) {
this.buffer.clear();
setBufferLimit(this.username.length() + this.password.length() + 3);
this.buffer.put(USERNAME_PASSWORD_VERSION);
this.buffer.put((byte) this.username.length());
this.buffer.put(this.username.getBytes(StandardCharsets.ISO_8859_1));
this.buffer.put((byte) this.password.length());
this.buffer.put(this.password.getBytes(StandardCharsets.ISO_8859_1));
session.setEventMask(SelectionKey.OP_WRITE);
this.state = State.SEND_USERNAME_PASSWORD;
} else if (serverMethod == NO_AUTHENTICATION_REQUIRED) {
prepareConnectCommand();
session.setEventMask(SelectionKey.OP_WRITE);
this.state = State.SEND_CONNECT;
} else {
throw new IOException("SOCKS server return unsupported authentication method: " + serverMethod);
}
}
break;
case RECEIVE_AUTH:
if (fillBuffer(session)) {
this.buffer.flip();
this.buffer.get(); // skip server auth version
final byte status = this.buffer.get();
if (status != SUCCESS) {
throw new IOException("Authentication failed for external SOCKS proxy");
}
prepareConnectCommand();
session.setEventMask(SelectionKey.OP_WRITE);
this.state = State.SEND_CONNECT;
}
break;
case RECEIVE_RESPONSE_CODE:
if (fillBuffer(session)) {
this.buffer.flip();
final byte serverVersion = this.buffer.get();
final byte responseCode = this.buffer.get();
if (serverVersion != CLIENT_VERSION) {
throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
}
if (responseCode != SUCCESS) {
throw new IOException("SOCKS server was unable to establish connection returned error code: " + responseCode);
}
this.buffer.compact();
this.buffer.limit(3);
this.state = State.RECEIVE_ADDRESS_TYPE;
// deliberate fall-through
} else {
break;
}
case RECEIVE_ADDRESS_TYPE:
if (fillBuffer(session)) {
this.buffer.flip();
this.buffer.get(); // reserved byte that has no purpose
final byte aType = this.buffer.get();
final int addressSize;
if (aType == ATYP_IPV4) {
addressSize = 4;
} else if (aType == ATYP_IPV6) {
addressSize = 16;
} else if (aType == ATYP_DOMAINNAME) {
// mask with 0xFF to convert to unsigned byte value
addressSize = this.buffer.get() & 0xFF;
} else {
throw new IOException("SOCKS server returned unsupported address type: " + aType);
}
this.remainingResponseSize = addressSize + 2;
this.buffer.compact();
// make sure we only read what we need to, don't read too much
this.buffer.limit(this.remainingResponseSize);
this.state = State.RECEIVE_ADDRESS;
// deliberate fall-through
} else {
break;
}
case RECEIVE_ADDRESS:
if (fillBuffer(session)) {
this.buffer.clear();
this.state = State.COMPLETE;
final IOEventHandler newHandler = this.eventHandlerFactory.createHandler(this.ioSession, this.attachment);
this.ioSession.upgrade(newHandler);
newHandler.connected(this.ioSession);
}
break;
case SEND_AUTH:
case SEND_USERNAME_PASSWORD:
case SEND_CONNECT:
session.setEventMask(SelectionKey.OP_WRITE);
break;
case COMPLETE:
break;
}
}
private void prepareConnectCommand() throws IOException {
final InetAddress address = this.targetAddress.getAddress();
final int port = this.targetAddress.getPort();
if (address == null || port == 0) {
throw new UnresolvedAddressException();
}
this.buffer.clear();
setBufferLimit(MAX_COMMAND_CONNECT_LENGTH);
this.buffer.put(CLIENT_VERSION);
this.buffer.put(COMMAND_CONNECT);
this.buffer.put((byte) 0); // reserved
if (address instanceof Inet4Address) {
this.buffer.put(ATYP_IPV4);
this.buffer.put(address.getAddress());
} else if (address instanceof Inet6Address) {
this.buffer.put(ATYP_IPV6);
this.buffer.put(address.getAddress());
} else {
throw new IOException("Unsupported remote address class: " + address.getClass().getName());
}
this.buffer.putShort((short) port);
this.buffer.flip();
}
private void setBufferLimit(final int newLimit) {
if (this.buffer.capacity() < newLimit) {
final ByteBuffer newBuffer = ByteBuffer.allocate(newLimit);
this.buffer.flip();
newBuffer.put(this.buffer);
this.buffer = newBuffer;
} else {
this.buffer.limit(newLimit);
}
}
private boolean writeAndPrepareRead(final ByteChannel channel, final int readSize) throws IOException {
if (writeBuffer(channel)) {
this.buffer.clear();
setBufferLimit(readSize);
return true;
}
return false;
}
private boolean writeBuffer(final ByteChannel channel) throws IOException {
if (this.buffer.hasRemaining()) {
channel.write(this.buffer);
}
return !this.buffer.hasRemaining();
}
private boolean fillBuffer(final ByteChannel channel) throws IOException {
if (this.buffer.hasRemaining()) {
channel.read(this.buffer);
}
return !this.buffer.hasRemaining();
}
@Override
public void timeout(final IOSession session, final Timeout timeout) throws IOException {
exception(session, SocketTimeoutExceptionFactory.create(timeout));
}
@Override
public void exception(final IOSession session, final Exception cause) {
session.close(CloseMode.IMMEDIATE);
CommandSupport.failCommands(session, cause);
}
@Override
public void disconnected(final IOSession session) {
CommandSupport.cancelCommands(session);
}
}