blob: 853e4e001796dc5ca7d8913d63b98aee84ec1dca [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.
*
*/
package org.apache.vysper.xmpp.extension.xep0065_socks;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link IoHandler} for a SOCKS5 proxy in accordance with RFC 1928 and XEP-0065.
*
* @author The Apache MINA Project (dev@mina.apache.org)
*/
public class Socks5AcceptorHandler extends IoHandlerAdapter {
public static final AttributeKey STATE_KEY = new AttributeKey(Socks5AcceptorHandler.class, "state");
public static final AttributeKey PAIR_KEY = new AttributeKey(Socks5AcceptorHandler.class, "pair");
private static final byte SOCKS_VERSION = 5;
private static final byte AUTH_NO_AUTH = 0;
private static final byte AUTH_USERNAME_PASSWORD = 2;
private static final byte AUTH_NOT_SUPPORTED = 0x55;
public enum ProxyState {
OPENED, INITIATED, CONNECTED, CLOSED
}
private final Logger log = LoggerFactory.getLogger(Socks5AcceptorHandler.class);
private Socks5ConnectionsRegistry connections;
public enum Socks5AuthType {
NO_AUTH, USERNAME_PASSWORD
}
/**
* Packet parsing for SOCKS5 opening packets
*/
public static class OpeningPacket {
private EnumSet<Socks5AuthType> authTypes;
public OpeningPacket(IoBuffer buffer) {
if (buffer.remaining() < 3)
throw new IllegalArgumentException();
byte socksVersion = buffer.get();
if (socksVersion != SOCKS_VERSION)
throw new IllegalArgumentException();
byte noOfAuth = buffer.get();
if (buffer.remaining() != noOfAuth)
throw new IllegalArgumentException();
byte[] authMethods = new byte[noOfAuth];
buffer.get(authMethods);
List<Socks5AuthType> authTypes = new ArrayList<Socks5AuthType>();
for (byte a : authMethods) {
if (a == AUTH_NO_AUTH)
authTypes.add(Socks5AuthType.NO_AUTH);
else if (a == AUTH_USERNAME_PASSWORD)
authTypes.add(Socks5AuthType.USERNAME_PASSWORD);
}
this.authTypes = EnumSet.copyOf(authTypes);
}
public EnumSet<Socks5AuthType> getAuthTypes() {
return authTypes;
}
public boolean isAuthSupported() {
return authTypes.contains(Socks5AuthType.NO_AUTH);
}
public IoBuffer createResponse() {
if (isAuthSupported()) {
// no auth
return IoBuffer.wrap(new byte[] { SOCKS_VERSION, AUTH_NO_AUTH });
} else {
// not supported
return IoBuffer.wrap(new byte[] { SOCKS_VERSION, AUTH_NOT_SUPPORTED });
}
}
}
/**
* Packet parsing for SOCKS5 initating packets
*/
public static class InitiatingPacket {
private static final byte RESERVED = 0;
private static final byte REQUEST_GRANTED = 0;
private static final byte ADDR_TYPE_NAME = 0x03;
// TODO charset?
private static final String CHARSET = "ASCII";
private byte addrType;
private byte[] addressBuffer;
private String address;
private short port;
public InitiatingPacket(IoBuffer buffer) {
if (buffer.remaining() < 9)
throw new IllegalArgumentException();
byte socksVersion = buffer.get();
if (socksVersion != SOCKS_VERSION) {
throw new IllegalArgumentException();
}
byte cmdCode = buffer.get();
if (cmdCode != 1) {
throw new IllegalArgumentException("Only supports TCP stream connections");
}
byte reserved = buffer.get();
if (reserved != 0) {
throw new IllegalArgumentException("Reserved bit must be 0");
}
addrType = buffer.get();
if (addrType == ADDR_TYPE_NAME) {
addressBuffer = new byte[buffer.get()];
} else {
throw new IllegalArgumentException("Must use domain name address type");
}
buffer.get(addressBuffer);
try {
this.address = new String(addressBuffer, CHARSET);
} catch (UnsupportedEncodingException ignored) {
;
}
this.port = buffer.getShort();
}
public String getAddress() {
return address;
}
public short getPort() {
return port;
}
public IoBuffer createResponse() {
IoBuffer b = IoBuffer.allocate(10).setAutoExpand(true);
b.put(SOCKS_VERSION);
b.put(REQUEST_GRANTED); // approved
b.put(RESERVED); // reserved
b.put(addrType);
b.put((byte) addressBuffer.length);
b.put(addressBuffer);
b.putShort(port);
return b.flip();
}
}
public Socks5AcceptorHandler(Socks5ConnectionsRegistry connections) {
this.connections = connections;
}
/**
* {@inheritDoc}
*/
public void sessionOpened(IoSession session) throws Exception {
log.info("SOCKS5 session opened");
session.setAttribute(STATE_KEY, ProxyState.OPENED);
}
/**
* {@inheritDoc}
*/
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
log.info("SOCKS5 connection idle, will be closed");
Socks5Pair pair = getPair(session);
if (pair != null) {
pair.close();
}
}
/**
* {@inheritDoc}
*/
public void messageReceived(final IoSession session, Object message) throws Exception {
IoBuffer buffer = (IoBuffer) message;
ProxyState state = getState(session);
try {
if (state == ProxyState.OPENED) {
OpeningPacket packet = new OpeningPacket(buffer);
if (packet.isAuthSupported()) {
// no auth
setState(session, ProxyState.INITIATED);
session.write(packet.createResponse());
log.info("SOCKS5 session initiated");
} else {
// not supported
session.write(packet.createResponse());
session.close(false);
log.info("SOCKS5 session closed");
}
} else if (state == ProxyState.INITIATED) {
InitiatingPacket packet = new InitiatingPacket(buffer);
session.write(packet.createResponse());
setState(session, ProxyState.CONNECTED);
String hash = packet.getAddress();
Socks5Pair pair = connections.register(hash, session);
session.setAttribute(PAIR_KEY, pair);
} else {
Socks5Pair pair = getPair(session);
if (pair != null && pair.isActivated()) {
pair.getOther(session).write(message);
} else {
// writing before activated, close
pair.close();
}
}
} catch (IllegalArgumentException e) {
session.close(false);
log.info("SOCKS5 session closed");
}
}
/**
* {@inheritDoc}
*/
public void sessionClosed(IoSession session) throws Exception {
log.info("SOCKS5 connection closed");
Socks5Pair pair = getPair(session);
if (pair != null) {
pair.close();
}
}
/**
* {@inheritDoc}
*/
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
log.error("Exception caused in SOCKS5 proxy, probably a bug in Vysper", cause);
Socks5Pair pair = getPair(session);
if(pair != null) {
pair.close();
} else {
session.close(false);
}
}
private void setState(final IoSession session, ProxyState state) {
session.setAttribute(STATE_KEY, state);
}
private ProxyState getState(final IoSession session) {
return (ProxyState) session.getAttribute(STATE_KEY);
}
private Socks5Pair getPair(final IoSession session) {
Socks5Pair pair = (Socks5Pair) session.getAttribute(PAIR_KEY);
return pair;
}
}