package net.i2p.util; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import gnu.getopt.Getopt; import net.i2p.I2PAppContext; /** * This is a quick hack to get a working EepHead, primarily for the following usage: * <pre> * EepHead foo = new EepHead(...); * if (foo.fetch()) { * String lastmod = foo.getLastModified(); * if (lastmod != null) { * parse the string... * ... * } * } * </pre> * Other use cases (command line, listeners, etc...) lightly- or un-tested. * Note that this follows redirects! This may not be what you want or expect. * * Writing from scratch rather than extending EepGet would maybe have been less bloated memory-wise. * This way gets us redirect handling, among other benefits. * * @since 0.7.7 * @author zzz */ public class EepHead extends EepGet { /** EepGet needs either a non-null file or a stream... shouldn't actually be written to... */ static final OutputStream _dummyStream = new ByteArrayOutputStream(0); public EepHead(I2PAppContext ctx, String proxyHost, int proxyPort, int numRetries, 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, true, proxyHost, proxyPort, numRetries, -1, -1, null, _dummyStream, url, true, null, null); } /** * EepHead [-p 127.0.0.1:4444] [-n #retries] url * * This doesn't really do much since it doesn't register a listener. * EepGet doesn't have a method to store and return all the headers, so just print * out the ones we have methods for. * Turn on logging to use it for a decent test. */ public static void main(String args[]) { String proxyHost = "127.0.0.1"; int proxyPort = 4444; int numRetries = 0; int inactivityTimeout = 60*1000; String username = null; String password = null; boolean error = false; Getopt g = new Getopt("eephead", args, "p:cn:t:u:x:"); try { int c; while ((c = g.getopt()) != -1) { switch (c) { case 'p': String s = g.getOptarg(); int colon = s.indexOf(':'); if (colon >= 0) { // Todo IPv6 [a:b:c]:4444 proxyHost = s.substring(0, colon); String port = s.substring(colon + 1); proxyPort = Integer.parseInt(port); } else { proxyHost = s; // proxyPort remains default } break; case 'c': // no proxy, same as -p :0 proxyHost = ""; proxyPort = 0; break; case 'n': numRetries = Integer.parseInt(g.getOptarg()); break; case 't': inactivityTimeout = 1000 * Integer.parseInt(g.getOptarg()); break; case 'u': username = g.getOptarg(); break; case 'x': password = g.getOptarg(); break; case '?': case ':': default: error = true; break; } // switch } // while } catch (RuntimeException e) { e.printStackTrace(); error = true; } if (error || args.length - g.getOptind() != 1) { usage(); System.exit(1); } String url = args[g.getOptind()]; EepHead get = new EepHead(I2PAppContext.getGlobalContext(), proxyHost, proxyPort, numRetries, url); if (username != null) { if (password == null) { try { BufferedReader r = new BufferedReader(new InputStreamReader(System.in)); do { System.err.print("Proxy password: "); password = r.readLine(); if (password == null) throw new IOException(); password = password.trim(); } while (password.length() <= 0); } catch (IOException ioe) { System.exit(1); } } get.addAuthorization(username, password); } if (get.fetch(45*1000, -1, inactivityTimeout)) { System.err.println("Content-Type: " + get.getContentType()); System.err.println("Content-Length: " + get.getContentLength()); System.err.println("Last-Modified: " + get.getLastModified()); System.err.println("Etag: " + get.getETag()); } else { System.err.println("Failed " + url); System.exit(1); } } private static void usage() { System.err.println("EepHead [-p 127.0.0.1[:4444]] [-c]\n" + " [-n #retries] (default 0)\n" + " [-t timeout] (default 60 sec)\n" + " [-u username] [-x password] url\n" + " (use -c or -p :0 for no proxy)"); } /** return true if the URL was completely retrieved */ @Override protected void doFetch(SocketTimeout timeout) throws IOException { _headersRead = false; _aborted = false; try { readHeaders(); } finally { _headersRead = true; } if (_aborted) throw new IOException("Timed out reading the HTTP headers"); timeout.resetTimer(); if (_fetchInactivityTimeout > 0) timeout.setInactivityTimeout(_fetchInactivityTimeout); else timeout.setInactivityTimeout(60*1000); // Should we even follow redirects for HEAD? if (_redirectLocation != null) { try { if (_redirectLocation.startsWith("http://")) { _actualURL = _redirectLocation; } else { // the Location: field has been required to be an absolute URI at least since // RFC 1945 (HTTP/1.0 1996), so it isn't clear what the point of this is. // This oddly adds a ":" even if no port, but that seems to work. URI url = new URI(_actualURL); String host = url.getHost(); if (host == null) throw new MalformedURLException("Redirected to invalid URL"); int port = url.getPort(); if (port < 0) port = 80; if (_redirectLocation.startsWith("/")) _actualURL = "http://" + host + ":" + port + _redirectLocation; else // this blows up completely on a redirect to https://, for example _actualURL = "http://" + host+ ":" + port + "/" + _redirectLocation; } } catch (URISyntaxException use) { IOException ioe = new MalformedURLException("Redirected to invalid URL"); ioe.initCause(use); throw ioe; } AuthState as = _authState; if (_responseCode == 407) { if (!_shouldProxy) throw new IOException("Proxy auth response from non-proxy"); if (as == null) throw new IOException("Proxy requires authentication"); if (as.authSent) throw new IOException("Proxy authentication failed"); // ignore stale if (_log.shouldLog(Log.INFO)) _log.info("Adding auth"); // actually happens in getRequest() } else { _redirects++; if (_redirects > 5) throw new IOException("Too many redirects: to " + _redirectLocation); if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation); if (as != null) as.authSent = false; } // reset some important variables, we don't want to save the values from the redirect _bytesRemaining = -1; _redirectLocation = null; _etag = null; _lastModified = null; _contentType = null; _encodingChunked = false; sendRequest(timeout); doFetch(timeout); return; } if (_log.shouldLog(Log.DEBUG)) _log.debug("Headers read completely"); if (_out != null) _out.close(); _out = null; if (_aborted) throw new IOException("Timed out reading the HTTP data"); timeout.cancel(); if (_transferFailed) { // 404, etc - transferFailed is called after all attempts fail, by fetch() above for (int i = 0; i < _listeners.size(); i++) _listeners.get(i).attemptFailed(_url, 0, 0, _currentAttempt, _numRetries, new Exception("Attempt failed")); } else { for (int i = 0; i < _listeners.size(); i++) _listeners.get(i).transferComplete( 0, 0, 0, _url, "dummy", false); } } @Override protected String getRequest() throws IOException { StringBuilder buf = new StringBuilder(512); 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(); if (host == null) throw new MalformedURLException("Bad URL"); int port = url.getPort(); String path = url.getRawPath(); String query = url.getRawQuery(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Requesting " + _actualURL); // RFC 2616 sec 5.1.2 - full URL if proxied, absolute path only if not proxied String urlToSend; if (_shouldProxy) { urlToSend = _actualURL; if ((path == null || path.length()<= 0) && (query == null || query.length()<= 0)) urlToSend += "/"; } else { urlToSend = path; if (urlToSend == null || urlToSend.length()<= 0) urlToSend = "/"; if (query != null) urlToSend += '?' + query; } buf.append("HEAD ").append(urlToSend).append(" HTTP/1.1\r\n"); // RFC 2616 sec 5.1.2 - host + port (NOT authority, which includes userinfo) buf.append("Host: ").append(host); if (port >= 0) buf.append(':').append(port); buf.append("\r\n"); buf.append("Accept-Encoding: \r\n"); // This will be replaced if we are going through I2PTunnelHTTPClient buf.append("User-Agent: " + USER_AGENT + "\r\n"); if (_authState != null && _shouldProxy && _authState.authMode != AUTH_MODE.NONE) { buf.append("Proxy-Authorization: "); buf.append(_authState.getAuthHeader("HEAD", urlToSend)); buf.append("\r\n"); } buf.append("Connection: close\r\n\r\n"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Request: [" + buf.toString() + "]"); return buf.toString(); } /** We don't decrement the variable (unlike in EepGet), so this is valid */ public long getContentLength() { return _bytesRemaining; } }