package net.i2p.client.streaming;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.EepGet;
import net.i2p.util.SocketTimeout;
/**
* Fetch a URL using a socket from the supplied I2PSocketManager.
* Hostname must resolve to an i2p destination - no routing to an outproxy.
* Does not support response gzip decompression (unlike I2PTunnelHTTPProxy) (yet),
* but of course there is still gzip at the I2CP layer.
*
* This is designed for Java apps such as bittorrent clients that wish to
* do HTTP fetches and use other protocols on a single set of tunnels.
* This may provide anonymity benefits over using the shared clients HTTP proxy,
* preventing inadvertent outproxy usage, reduce resource usage by eliminating
* a second set of tunnels, and eliminate the requirement to
* to separately configure the proxy host and port.
*
* For additional documentation see the superclass.
*
* Supports http://example.i2p/blah
* Supports http://B32KEY.b32.i2p/blah
* Supports http://i2p/B64KEY/blah for compatibility with the eepproxy
* Supports http://B64KEY/blah for compatibility with the eepproxy
* Warning - does not support /eepproxy/blah, address helpers, http://B64KEY.i2p/blah,
* or other odd things that may be found in the HTTP proxy.
*
* @author zzz
*/
public class I2PSocketEepGet extends EepGet {
private final I2PSocketManager _socketManager;
/** this replaces _proxy in the superclass. Sadly, I2PSocket does not extend Socket. */
private I2PSocket _socket;
/** from ConnectionOptions */
private static final String PROP_CONNECT_DELAY = "i2p.streaming.connectDelay";
private static final String CONNECT_DELAY = "500";
public I2PSocketEepGet(I2PAppContext ctx, I2PSocketManager mgr, int numRetries, String outputFile, String url) {
this(ctx, mgr, numRetries, -1, -1, outputFile, null, url);
}
public I2PSocketEepGet(I2PAppContext ctx, I2PSocketManager mgr, int numRetries, long minSize, long maxSize,
String outputFile, OutputStream outputStream, String url) {
// we're using this constructor:
// public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) {
super(ctx, false, null, -1, numRetries, minSize, maxSize, outputFile, outputStream, url, true, null, null);
_socketManager = mgr;
}
/**
* We have to override this to close _socket, since we can't use _proxy in super as the I2PSocket.
*/
@Override
public boolean fetch(long fetchHeaderTimeout, long totalTimeout, long inactivityTimeout) {
boolean rv = super.fetch(fetchHeaderTimeout, totalTimeout, inactivityTimeout);
if (_socket != null) {
try {
_socket.close();
_socket = null;
} catch (IOException ioe) {}
}
return rv;
}
/**
* Overridden to disable inline gunzipping
* @since 0.8.10
*/
@Override
protected void readHeaders() throws IOException {
try {
super.readHeaders();
} finally {
_isGzippedResponse = false;
}
}
/**
* Look up the address, get a socket from the I2PSocketManager supplied in the constructor,
* and send the request.
*
* @param timeout ignored
*/
@Override
protected void sendRequest(SocketTimeout timeout) throws IOException {
if (_outputStream == null) {
File outFile = new File(_outputFile);
if (outFile.exists())
_alreadyTransferred = outFile.length();
}
if (_proxyIn != null) try { _proxyIn.close(); } catch (IOException ioe) {}
if (_proxyOut != null) try { _proxyOut.close(); } catch (IOException ioe) {}
if (_socket != null) try { _socket.close(); } catch (IOException ioe) {}
try {
URI url = new URI(_actualURL);
if ("http".equals(url.getScheme())) {
String host = url.getHost();
if (host == null)
throw new MalformedURLException("no hostname: " + _actualURL);
int port = url.getPort();
if (port <= 0 || port > 65535)
port = 80;
// HTTP Proxy compatibility http://i2p/B64KEY/blah
// Rewrite the url to strip out the /i2p/,
// as the naming service accepts B64KEY (but not B64KEY.i2p atm)
if ("i2p".equals(host)) {
String file = url.getRawPath();
try {
int slash = 1 + file.substring(1).indexOf('/');
host = file.substring(1, slash);
_actualURL = "http://" + host + file.substring(slash);
String query = url.getRawQuery();
if (query != null)
_actualURL = _actualURL + '?' + query;
} catch (IndexOutOfBoundsException ioobe) {
throw new MalformedURLException("Bad /i2p/ format: " + _actualURL);
}
}
// Use existing I2PSession for lookups.
// This is much more efficient than using the naming service
Destination dest;
I2PSession sess = _socketManager.getSession();
if (sess != null && !sess.isClosed()) {
try {
if (host.length() == 60 && host.endsWith(".b32.i2p")) {
byte[] b = Base32.decode(host.substring(0, 52));
if (b != null) {
Hash h = Hash.create(b);
dest = sess.lookupDest(h, 20*1000);
} else {
dest = null;
}
} else {
dest = sess.lookupDest(host, 20*1000);
}
} catch (I2PSessionException ise) {
dest = null;
}
} else {
dest = _context.namingService().lookup(host);
}
if (dest == null)
throw new UnknownHostException("Unknown or non-i2p host: " + host);
// Set the timeouts, using the other existing options in the socket manager
// This currently duplicates what SocketTimeout is doing in EepGet,
// but when that's ripped out of EepGet to use setsotimeout, we'll need this.
Properties props = new Properties();
props.setProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT, "" + CONNECT_TIMEOUT);
props.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, "" + INACTIVITY_TIMEOUT);
// This is important - even if the underlying socket doesn't have a connect delay,
// we want to set it for this connection, so the request headers will go out
// in the SYN packet, saving one RTT.
props.setProperty(PROP_CONNECT_DELAY, CONNECT_DELAY);
I2PSocketOptions opts = _socketManager.buildOptions(props);
opts.setPort(port);
_socket = _socketManager.connect(dest, opts);
} else {
throw new MalformedURLException("Unsupported protocol: " + _actualURL);
}
} catch (URISyntaxException use) {
IOException ioe = new MalformedURLException("Bad URL");
ioe.initCause(use);
throw ioe;
} catch (I2PException ie) {
throw new IOException("I2P error", ie);
}
_proxyIn = _socket.getInputStream();
_proxyOut = _socket.getOutputStream();
// SocketTimeout doesn't take an I2PSocket, but no matter, because we
// always close our socket in fetch() above.
//timeout.setSocket(_socket);
String req = getRequest();
_proxyOut.write(DataHelper.getUTF8(req));
_proxyOut.flush();
}
/**
* Guess we have to override this since
* super doesn't strip the http://host from the GET line
* which hoses some servers (opentracker)
* HTTP proxy was kind enough to do this for us
*/
@Override
protected String getRequest() throws IOException {
StringBuilder buf = new StringBuilder(2048);
URI url;
try {
url = new URI(_actualURL);
} catch (URISyntaxException use) {
IOException ioe = new MalformedURLException("Bad URL");
ioe.initCause(use);
throw ioe;
}
//String host = url.getHost();
String path = url.getRawPath();
String query = url.getRawQuery();
if (query != null)
path = path + '?' + query;
if (!path.startsWith("/"))
path = '/' + path;
buf.append("GET ").append(path).append(" HTTP/1.1\r\n" +
"Host: ").append(url.getHost()).append("\r\n");
if (_alreadyTransferred > 0) {
buf.append("Range: bytes=");
buf.append(_alreadyTransferred);
buf.append("-\r\n");
}
buf.append("Accept-Encoding: \r\n" +
"Cache-control: no-cache\r\n" +
"Pragma: no-cache\r\n" +
"Connection: close\r\n");
boolean uaOverridden = false;
if (_extraHeaders != null) {
for (String hdr : _extraHeaders) {
if (hdr.toLowerCase(Locale.US).startsWith("user-agent: "))
uaOverridden = true;
buf.append(hdr).append("\r\n");
}
}
if(!uaOverridden)
buf.append("User-Agent: " + USER_AGENT + "\r\n");
buf.append("\r\n");
if (_log.shouldDebug())
_log.debug("Request: [" + buf.toString() + "]");
return buf.toString();
}
/**
* I2PSocketEepGet [-n #retries] [-t timeout] url
* Uses I2CP at localhost:7654 with a single 1-hop tunnel each direction.
* Tunnel build time not included in the timeout.
*
* This is just for testing, it will be commented out someday.
* Real command line apps should use EepGet.main(),
* which has more options, and you don't have to wait for tunnels to be built.
*/
/****
public static void main(String args[]) {
int numRetries = 0;
long inactivityTimeout = INACTIVITY_TIMEOUT;
String url = null;
try {
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-n")) {
numRetries = Integer.parseInt(args[i+1]);
i++;
} else if (args[i].equals("-t")) {
inactivityTimeout = 1000 * Integer.parseInt(args[i+1]);
i++;
} else if (args[i].startsWith("-")) {
usage();
return;
} else {
url = args[i];
}
}
} catch (RuntimeException e) {
e.printStackTrace();
usage();
return;
}
if (url == null) {
usage();
return;
}
Properties opts = new Properties();
opts.setProperty("i2cp.dontPublishLeaseSet", "true");
opts.setProperty("inbound.quantity", "1");
opts.setProperty("outbound.quantity", "1");
opts.setProperty("inbound.length", "1");
opts.setProperty("outbound.length", "1");
opts.setProperty("inbound.nickname", "I2PSocketEepGet");
I2PSocketManager mgr = I2PSocketManagerFactory.createManager(opts);
if (mgr == null) {
System.err.println("Error creating the socket manager");
return;
}
I2PSocketEepGet get = new I2PSocketEepGet(I2PAppContext.getGlobalContext(),
mgr, numRetries, suggestName(url), url);
get.addStatusListener(get.new CLIStatusListener(1024, 40));
get.fetch(inactivityTimeout, -1, inactivityTimeout);
mgr.destroySocketManager();
}
private static void usage() {
System.err.println("I2PSocketEepGet [-n #retries] [-t timeout] url");
}
****/
}