/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
* (c) 2003 - 2004 mihi
*/
package net.i2p.i2ptunnel;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.app.ClientApp;
import net.i2p.app.ClientAppManager;
import net.i2p.app.Outproxy;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketOptions;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.EventDispatcher;
import net.i2p.util.Log;
import net.i2p.util.PortMapper;
/**
* Supports the following:
*<pre>
* (where protocol is generally HTTP/1.1 but is ignored)
* (where host is one of:
* example.i2p
* 52chars.b32.i2p
* 516+charsbase64
* example.com (sent to one of the configured proxies)
* )
*
* (protocol is ignored for i2p destinations)
* CONNECT host
* CONNECT host protocol
* CONNECT host:port
* CONNECT host:port protocol (this is the standard)
*</pre>
*
* Additional lines after the CONNECT line but before the blank line are ignored and stripped.
* The CONNECT line is removed for .i2p accesses
* but passed along for outproxy accesses.
*
* Ref:
*<pre>
* INTERNET-DRAFT Ari Luotonen
* Expires: September 26, 1997 Netscape Communications Corporation
* draft-luotonen-ssl-tunneling-03.txt March 26, 1997
* Tunneling SSL Through a WWW Proxy
*</pre>
*
* @author zzz a stripped-down I2PTunnelHTTPClient
*/
public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable {
public static final String AUTH_REALM = "I2P SSL Proxy";
private final static String ERR_BAD_PROTOCOL =
"HTTP/1.1 405 Bad Method\r\n"+
"Content-Type: text/html; charset=iso-8859-1\r\n"+
"Cache-control: no-cache\r\n"+
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"\r\n"+
"<html><body><H1>I2P ERROR: METHOD NOT ALLOWED</H1>"+
"The request uses a bad protocol. "+
"The Connect Proxy supports CONNECT requests ONLY. Other methods such as GET are not allowed - Maybe you wanted the HTTP Proxy?.<BR>";
private final static String ERR_LOCALHOST =
"HTTP/1.1 403 Access Denied\r\n"+
"Content-Type: text/html; charset=iso-8859-1\r\n"+
"Cache-control: no-cache\r\n"+
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"\r\n"+
"<html><body><H1>I2P ERROR: REQUEST DENIED</H1>"+
"Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.<BR>";
/**
* As of 0.9.20 this is fast, and does NOT connect the manager to the router,
* or open the local socket. You MUST call startRunning() for that.
*
* @throws IllegalArgumentException if the I2PTunnel does not contain
* valid config to contact the router
*/
public I2PTunnelConnectClient(int localPort, Logging l, boolean ownDest,
String wwwProxy, EventDispatcher notifyThis,
I2PTunnel tunnel) throws IllegalArgumentException {
super(localPort, ownDest, l, notifyThis, "HTTPS Proxy on " + tunnel.listenHost + ':' + localPort, tunnel);
if (wwwProxy != null) {
StringTokenizer tok = new StringTokenizer(wwwProxy, ", ");
while (tok.hasMoreTokens())
_proxyList.add(tok.nextToken().trim());
}
setName("HTTPS Proxy on " + tunnel.listenHost + ':' + localPort);
}
/**
* Create the default options (using the default timeout, etc).
* Warning, this does not make a copy of I2PTunnel's client options,
* it modifies them directly.
*/
@Override
protected I2PSocketOptions getDefaultOptions() {
Properties defaultOpts = getTunnel().getClientOptions();
if (!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT))
defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT);
if (!defaultOpts.contains("i2p.streaming.inactivityTimeout"))
defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT);
// delayed start
verifySocketManager();
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
return opts;
}
@Override
public void startRunning() {
super.startRunning();
if (open)
_context.portMapper().register(PortMapper.SVC_HTTPS_PROXY, getTunnel().listenHost, getLocalPort());
}
@Override
public boolean close(boolean forced) {
int reg = _context.portMapper().getPort(PortMapper.SVC_HTTPS_PROXY);
if (reg == getLocalPort())
_context.portMapper().unregister(PortMapper.SVC_HTTPS_PROXY);
return super.close(forced);
}
/** @since 0.9.4 */
protected String getRealm() {
return AUTH_REALM;
}
protected void clientConnectionRun(Socket s) {
InputStream in = null;
OutputStream out = null;
String targetRequest = null;
boolean usingWWWProxy = false;
String currentProxy = null;
// local outproxy plugin
boolean usingInternalOutproxy = false;
Outproxy outproxy = null;
long requestId = __requestId.incrementAndGet();
try {
out = s.getOutputStream();
in = s.getInputStream();
String line, method = null, host = null, destination = null, restofline = null;
StringBuilder newRequest = new StringBuilder();
String authorization = null;
int remotePort = 443;
while (true) {
// Use this rather than BufferedReader because we can't have readahead,
// since we are passing the stream on to I2PTunnelRunner
line = DataHelper.readLine(in);
if(line == null) {
break;
}
line = line.trim();
if (_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix(requestId) + "Line=[" + line + "]");
if (method == null) { // first line CONNECT blah.i2p:80 HTTP/1.1
int pos = line.indexOf(' ');
if (pos == -1) break; // empty first line
method = line.substring(0, pos);
String request = line.substring(pos + 1);
pos = request.indexOf(':');
if (pos == -1) {
pos = request.indexOf(' ');
} else {
int spos = request.indexOf(' ');
if (spos > 0) {
try {
remotePort = Integer.parseInt(request.substring(pos + 1, spos));
} catch (NumberFormatException nfe) {
break;
} catch (IndexOutOfBoundsException ioobe) {
break;
}
}
}
if (pos == -1) {
host = request;
restofline = "";
} else {
host = request.substring(0, pos);
restofline = request.substring(pos); // ":80 HTTP/1.1" or " HTTP/1.1"
}
if (host.toLowerCase(Locale.US).endsWith(".i2p")) {
// Destination gets the host name
destination = host;
} else if (host.contains(".") || host.startsWith("[")) {
if (Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_USE_OUTPROXY_PLUGIN, "true"))) {
ClientAppManager mgr = _context.clientAppManager();
if (mgr != null) {
ClientApp op = mgr.getRegisteredApp(Outproxy.NAME);
if (op != null) {
outproxy = (Outproxy) op;
usingInternalOutproxy = true;
if (host.startsWith("[")) {
host = host.substring(1);
if (host.endsWith("]"))
host = host.substring(0, host.length() - 1);
}
}
}
}
if (!usingInternalOutproxy) {
// The request must be forwarded to a outproxy
currentProxy = selectProxy();
if (currentProxy == null) {
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!");
writeErrorMessage(ERR_NO_OUTPROXY, out);
s.close();
return;
}
destination = currentProxy;
usingWWWProxy = true;
newRequest.append("CONNECT ").append(host).append(restofline).append("\r\n"); // HTTP spec
}
} else if (host.toLowerCase(Locale.US).equals("localhost")) {
writeErrorMessage(ERR_LOCALHOST, out);
s.close();
return;
} else { // full b64 address (hopefully)
destination = host;
}
targetRequest = host;
if (_log.shouldLog(Log.DEBUG)) {
_log.debug(getPrefix(requestId) + "METHOD:" + method + ":\n" +
"HOST :" + host + ":\n" +
"PORT :" + remotePort + ":\n" +
"REST :" + restofline + ":\n" +
"DEST :" + destination + ":\n" +
"www proxy? " + usingWWWProxy + " internal proxy? " + usingInternalOutproxy);
}
} else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: ")) {
// strip Proxy-Authenticate from the response in HTTPResponseOutputStream
// save for auth check below
authorization = line.substring(21); // "proxy-authorization: ".length()
line = null;
} else if (line.length() > 0) {
// Additional lines - shouldn't be too many. Firefox sends:
// User-Agent: blabla
// Proxy-Connection: keep-alive
// Host: blabla.i2p
//
// We could send these (filtered like in HTTPClient) on to the outproxy,
// but for now just chomp them all.
line = null;
} else {
// Add Proxy-Authentication header for next hop (outproxy)
if (usingWWWProxy && Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_AUTH))) {
// specific for this proxy
String user = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_USER_PREFIX + currentProxy);
String pw = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_PW_PREFIX + currentProxy);
if (user == null || pw == null) {
// if not, look at default user and pw
user = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_USER);
pw = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_PW);
}
if (user != null && pw != null) {
newRequest.append("Proxy-Authorization: Basic ")
.append(Base64.encode(DataHelper.getUTF8(user + ':' + pw), true)) // true = use standard alphabet
.append("\r\n");
}
}
newRequest.append("\r\n"); // HTTP spec
// do it
break;
}
}
if (method == null || !"CONNECT".equals(method.toUpperCase(Locale.US))) {
writeErrorMessage(ERR_BAD_PROTOCOL, out);
s.close();
return;
}
// no destination, going to outproxy plugin
if (usingInternalOutproxy) {
Socket outSocket = outproxy.connect(host, remotePort);
OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
byte[] response = SUCCESS_RESPONSE.getBytes("UTF-8");
Thread t = new I2PTunnelOutproxyRunner(s, outSocket, sockLock, null, response, onTimeout);
// we are called from an unlimited thread pool, so run inline
t.run();
return;
}
if (destination == null) {
writeErrorMessage(ERR_BAD_PROTOCOL, out);
s.close();
return;
}
// Authorization
AuthResult result = authorize(s, requestId, method, authorization);
if (result != AuthResult.AUTH_GOOD) {
if (_log.shouldLog(Log.WARN)) {
if (authorization != null)
_log.warn(getPrefix(requestId) + "Auth failed, sending 407 again");
else
_log.warn(getPrefix(requestId) + "Auth required, sending 407");
}
out.write(DataHelper.getASCII(getAuthError(result == AuthResult.AUTH_STALE)));
s.close();
return;
}
Destination clientDest = _context.namingService().lookup(destination);
if (clientDest == null) {
String header;
if (usingWWWProxy)
header = getErrorPage("dnfp", ERR_DESTINATION_UNKNOWN);
else
header = getErrorPage("dnfh", ERR_DESTINATION_UNKNOWN);
writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination);
s.close();
return;
}
I2PSocketOptions sktOpts = getDefaultOptions();
if (!usingWWWProxy && remotePort > 0)
sktOpts.setPort(remotePort);
I2PSocket i2ps = createI2PSocket(clientDest, sktOpts);
byte[] data = null;
byte[] response = null;
if (usingWWWProxy)
data = newRequest.toString().getBytes("ISO-8859-1");
else
response = SUCCESS_RESPONSE.getBytes("UTF-8");
OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
Thread t = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout);
// we are called from an unlimited thread pool, so run inline
//t.start();
t.run();
} catch (IOException ex) {
_log.info(getPrefix(requestId) + "Error trying to connect", ex);
handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
closeSocket(s);
} catch (I2PException ex) {
_log.info("getPrefix(requestId) + Error trying to connect", ex);
handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
closeSocket(s);
} catch (OutOfMemoryError oom) {
IOException ex = new IOException("OOM");
_log.info("getPrefix(requestId) + Error trying to connect", ex);
handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
closeSocket(s);
}
}
private static void writeErrorMessage(String errMessage, OutputStream out) throws IOException {
if (out == null)
return;
out.write(errMessage.getBytes("UTF-8"));
writeFooter(out);
}
}