/* * GenericProxyHandler.java * * Brazil project web application toolkit, * export version: 2.3 * Copyright (c) 1998-2009 Sun Microsystems, Inc. * * Sun Public License Notice * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is included as the file "license.terms", * and also available at http://www.sun.com/ * * The Original Code is from: * Brazil project web application toolkit release 2.3. * The Initial Developer of the Original Code is: suhler. * Portions created by suhler are Copyright (C) Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): cstevens, rinaldo, suhler. * * Version: 2.10 * Created by suhler on 98/09/14 * Last modified by suhler on 09/01/20 16:07:19 * * Version Histories: * * 2.10 09/01/20-16:07:19 (suhler) * add support for Https * * 2.9 08/12/19-11:13:46 (suhler) * lint * * 2.8 08/03/17-09:44:22 (suhler) * use httpRequest factory * * 2.7 08/03/04-10:55:22 (suhler) * javadoc fixes * * 2.6 08/02/04-13:40:00 (suhler) * Properly rewrite cookie domains when proxying content * * 2.5 07/03/21-15:53:25 (suhler) * add call to hand over our pageMapper * * 2.4 07/01/29-15:34:58 (suhler) * - added passHost flag to pass the origin http "host" header instead of the * target one * - added noErrorReturn flag. If set, the handler returns false instead of * returning an error code to the client * - made MapPage an instance variable (WARNING: this effects sub-classes) * . * * 2.3 07/01/14-15:14:18 (suhler) * diagniostic fixes * * 2.2 06/11/13-15:03:29 (suhler) * - pass response status from target to client * - change system..println to Log() * * 2.1 02/10/01-16:36:22 (suhler) * version change * * 1.32 02/08/01-13:45:55 (suhler) * change "requestHeaders" to "headers", and use the same syntax/semantics as * the PollHandler and IncludeTemplate. * Make sure the host ehader is set and nut duplicated * * 1.31 02/07/24-10:45:44 (suhler) * doc updates * * 1.30 01/09/23-17:35:44 (suhler) * Added "requestheaders" properties, allowing arbitrary http headers * to be sent to the up-stream server * * 1.29 01/01/16-14:25:54 (suhler) * don't return from "finally" clauses * * 1.28 00/12/11-13:26:44 (suhler) * add class=props for automatic property extraction * * 1.27 00/05/31-13:45:28 (suhler) * doc cleanup * * 1.26 00/04/20-11:48:04 (cstevens) * copyright. * * 1.25 00/03/29-14:34:26 (cstevens) * unused method * * 1.24 00/03/10-17:02:08 (cstevens) * Removing unused member variables * * 1.23 00/02/25-09:02:47 (suhler) * temporary patches to fix redirection and "host" problems. * * 1.22 00/02/11-12:42:23 (suhler) * * 1.21 00/02/08-10:00:32 (suhler) * Missing else, caused crashes * * 1.20 00/02/03-14:24:20 (suhler) * better diagnostics * * 1.19 00/02/02-14:44:57 (suhler) * turn on url mapping diagnostics if log>5 * * 1.18 99/10/26-18:50:42 (cstevens) * case * * 1.17 99/10/26-17:06:48 (cstevens) * Get rid of public variables Request.server and Request.sock * In all cases, Request.server was not necessary in the Handler. * Request.sock was changed to Request.getSock(); it is still rarely used for * diagnostics and loggin (e.g., ChainSawHandler). * Change GenericProxyHandler (which actually filters data from other sites, and * is not a generic proxy) to use Request.sendResponse(InputStream, ...) to * stream the data, instead of fetching the entire contents into a big byte array * and then sending the byte array to Request.sendResponse(byte[], ...). * * 1.16 99/10/14-14:57:02 (cstevens) * resolve wilcard imports. * * 1.15 99/10/06-12:21:09 (suhler) * Merged changes between child workspace "/home/suhler/brazil/naws" and * parent workspace "/net/mack.eng/export/ws/brazil/naws". * * 1.13.1.1 99/10/06-12:16:39 (suhler) * rewrite to use caching proxy (not complete) * * 1.14 99/10/01-11:25:46 (cstevens) * Change logging to show prefix of Handler generating the log message. * * 1.13 99/09/15-14:38:36 (cstevens) * Rewriting http server. * * 1.12 99/09/01-12:15:12 (suhler) * add a flush() to make compatible with TailHandler * * 1.11 99/06/28-10:48:07 (suhler) * re-written to separate out link rewriting stuff * * 1.10 99/05/25-09:19:28 (suhler) * revert to previous version. HtmlMunge was fixed accordingly * * 1.9 99/05/24-19:10:36 (suhler) * chenged to use HtmlMunge semantics * * 1.8 99/03/30-09:29:01 (suhler) * documentation update * * 1.7 98/11/18-14:53:34 (suhler) * Lost this in the re-write. Allow subclasses to return "false" by setting * the content to null in modifyContent() * * 1.6 98/11/18-13:57:03 (suhler) * Re-wrote proxy code to fixe suspected (but not confirmed) * thread race conditions by eliminating all instance variable that * are "writable" by respond() * * 1.5 98/11/15-15:30:44 (rinaldo) * Create new Distribution * * 1.4 98/11/08-10:27:58 (suhler) * Don't pass connection header through proxy. * Somewhere there must be a list of headers that should not be * proxied. Some day * * 1.3 98/09/21-14:53:50 (suhler) * * 1.2 98/09/20-15:37:32 (suhler) * - make sure url prefix has leading / * - elide unused variable * . * * 1.2 98/09/14-18:03:05 (Codemgr) * SunPro Code Manager data about conflicts, renames, etc... * Name history : 2 1 handlers/GenericProxyHandler.java * Name history : 1 0 GenericProxyHandler.java * * 1.1 98/09/14-18:03:04 (suhler) * date and time created 98/09/14 18:03:04 by suhler * */ package sunlabs.brazil.handler; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.ConnectException; import java.net.UnknownHostException; import java.util.StringTokenizer; import sunlabs.brazil.server.Handler; import sunlabs.brazil.server.Request; import sunlabs.brazil.server.Server; import sunlabs.brazil.util.http.HttpRequest; import sunlabs.brazil.util.http.MimeHeaders; import sunlabs.brazil.util.StringMap; import sunlabs.brazil.util.Format; /** * Handler for implementing a virtual web site. * This causes another web site to "appear" inside our document root. * This classes is intended to be sub-classed, so some of the methods * in this implementation don't do too much. * * All of the appropriate links in HTML documents on the virtual * site are rewritten, so they appear to be local references. * This can be used on a firewall in conjunction with * {@link AclSwitchHandler} * to provide authenticated access to selected web sites. *

* Properties: *

*
prefix
URL prefix must match *
host
name of host site to proxy to. *
protocol *
defaultPort *
Hormally all requests are fetched via "http", using the default * port of 80. The "protocol" and "defaultPort" settings may be used * to get other protocols. For example: *
 *  protocol=https
 *  defaultPort=443
 * 
*
port
Host port to proxy to (defaults to defaultPort). *
proxyHost
Which proxy host to use (if any) * to contact "host". *
proxyPort
The proxy's port (defaults to defaultPort) *
headers
A list of white space delimited tokens that refer to * additional HTTP headers that are added onto the polled * request. For each token the server properties * [token].name and [token].value * define a new http header. *
passHost
If true, the original browser host string is passed to the * target, otherwise the mapped hostname is used, in which case the * http header "X-Host-Orig" will contain the original host name. *
noErrorReturn *
If true, then if the proxy request fails, the response method * returns "false", and places the reason for failure in the "errorCode" * and "errorMsg" request properties. * Otherwise, and error response is generated. The default is (erroneously) * false for historical reasons. *
* * @author Stephen Uhler * @version 2.10, 09/01/20 */ public class GenericProxyHandler implements Handler { // protected Server server; protected String prefix; protected MapPage mapper = null; // our url rewriter. protected String host; // The host containing the actual page protected String protocol="http"; protected int defaultPort = 80; protected int port; // The port for above (defaults to defaultPort) protected String proxyHost; // The proxy server (if any) protected int proxyPort; // The proxy server's port (defaults to defaultPort) protected String urlPrefix; // The url prefix that triggers this handler protected String requestPrefix; // The host/port prefix protected String tokens; // our tokens in server.props protected boolean passHost; // if true, pass-thru the original http host header protected boolean noErrorReturn=false; // if true. /** * Handler configuration property prefix. * Only URL's that begin with this string are considered by this handler. * The default is (/). */ public static final String PREFIX = "prefix"; // URL prefix for proxy /** * Handler configuration property host. * The actual host site to appear on our site (required) */ public static final String HOST = "host"; // The host to proxy these requests to /** * Handler configuration property port. * The actual port on the host site (defaults to defaultPort). */ public static final String PORT = "port"; // The host port to proxy these requests to /** * Handler configuration property proxyHost. * The name of a proxy to use (if any) to get to the host. */ public static final String PROXY_HOST = "proxyHost"; // The proxy server (if any) /** * Handler configuration property proxyPort. * The proxy port to use to get to the host. defaults to defaultPort. */ public static final String PROXY_PORT = "proxyPort"; // The proxy server port (if any) public static final String NL = "\r\n"; // line terminator /** * Do one-time setup. * get and process the handler properties. * we can contact the server identified by the host parameter. */ public boolean init(Server server, String prefix) { // this.server = server; this.prefix = prefix; if (server.logLevel > server.LOG_DIAGNOSTIC) { MapPage.log = true; HttpRequest.displayAllHeaders = true; } passHost=Format.isTrue(server.props.getProperty(prefix + "passHost")); noErrorReturn=Format.isTrue(server.props.getProperty(prefix + "noErrorReturn")); System.out.println(prefix + ": passHost=" + passHost + " noErrorReturn=" + noErrorReturn); /* XXX * Need to add tag map entries here for a specific host */ host = server.props.getProperty(prefix + HOST); if (host == null) { server.log(Server.LOG_WARNING, prefix, "no host to proxy to"); return false; } protocol = server.props.getProperty(prefix + PREFIX, "http"); try { String str = server.props.getProperty(prefix + PROXY_PORT); defaultPort = Integer.decode(str).intValue(); } catch (Exception e) {} urlPrefix = server.props.getProperty(prefix + PREFIX, "/"); if (urlPrefix.indexOf('/') != 0) { urlPrefix = "/" + urlPrefix; } if (!urlPrefix.endsWith("/")) { urlPrefix += "/"; } port = defaultPort; try { String str = server.props.getProperty(prefix + PORT); port = Integer.decode(str).intValue(); } catch (Exception e) {} proxyHost = server.props.getProperty(prefix + PROXY_HOST); proxyPort = defaultPort; try { String str = server.props.getProperty(prefix + PROXY_PORT); proxyPort = Integer.decode(str).intValue(); } catch (Exception e) {} if (port == defaultPort) { requestPrefix = protocol + "://" + host; } else { requestPrefix = protocol + "://" + host + ":" + port; } tokens = server.props.getProperty(prefix + "headers"); mapper = new MapPage(urlPrefix); return true; } /** * If this is one of "our" url's, fetch the document from * the destination server, and return it as if it was local. */ public boolean respond(Request request) throws IOException { if (!isMine(request)) { return false; } String url = request.url.substring(urlPrefix.length()); if (!url.startsWith("/")) { url = "/" + url; } if (!request.query.equals("")) { url += "?" + request.query; } request.log(Server.LOG_DIAGNOSTIC, " Request headers: " + request.headers); HttpRequest target = HttpRequest.getRequest(requestPrefix + url); // System.out.println("Fetching: " + requestPrefix + url); if (proxyHost != null) { target.setProxy(proxyHost, proxyPort); } target.setMethod(request.method); HttpRequest.removePointToPointHeaders(request.headers, false); request.headers.remove("if-modified-since"); // wrong spot XXX request.headers.copyTo(target.requestHeaders); /* XXX This doesn't belong here! - the proxy should do it */ if (!passHost) { String orig = target.requestHeaders.get("host"); if (orig != null) { target.requestHeaders.remove("host"); target.requestHeaders.put("X-Host-Orig", orig); } } if (tokens != null) { target.addHeaders(tokens, request.props); } target.requestHeaders.putIfNotPresent("Host",host); boolean code=true; try { if (request.postData != null) { OutputStream out = target.getOutputStream(); out.write(request.postData); out.close(); } // Connect to target and read the response headers request.log(Server.LOG_DIAGNOSTIC, prefix, "fetching " + target.url + "..."); target.connect(); request.log(Server.LOG_DIAGNOSTIC, prefix, "... Got response code: " + target.status); HttpRequest.removePointToPointHeaders(target.responseHeaders, true); target.responseHeaders.copyTo(request.responseHeaders); /* * Fix the domain and path specifiers on set-cookie requests. */ for (int i = 0; i < request.responseHeaders.size(); i++) { String key = request.responseHeaders.getKey(i); if (key.equals("set-cookie")) { String cookie = request.responseHeaders.get(i); String newCookie = mapCookie(cookie); System.out.println("ProxyHandler Mapping cookie: (" + cookie + ") to (" + newCookie + ")"); request.responseHeaders.put("set-cookie", newCookie); } } // Now filter the output, writing the header and content if true request.log(Server.LOG_DIAGNOSTIC, " Response headers: " + target.responseHeaders); if (shouldFilter(request.responseHeaders)) { ByteArrayOutputStream out = new ByteArrayOutputStream(); target.getInputStream().copyTo(out); request.log(Server.LOG_DIAGNOSTIC, " parsing/modifying " + out.size() + " bytes"); byte[] content = modifyContent(request, out.toByteArray()); if (content == null) { // This is wrong!! request.log(Server.LOG_DIAGNOSTIC, " null content, returning false"); code=false; } else { // String type = request.responseHeaders.get("Content-Type"); request.setStatus(target.getResponseCode()); request.sendResponse(content, null); } } else { request.log(Server.LOG_DIAGNOSTIC, "Delivering normal content"); request.sendResponse(target.getInputStream(), target.getContentLength(), null, target.getResponseCode()); } } catch (InterruptedIOException e) { /* * Read timeout while reading from the remote side. We use a * read timeout in case the target never responds. */ code = doError(request, 408, "Timeout / No response"); } catch (UnknownHostException e) { code = doError(request, 503, urlPrefix + " Not reachable"); } catch (ConnectException e) { code = doError(request, 500, "Connection refused"); } catch (IOException e) { code = doError(request,500, "Error retrieving response: " + e); e.printStackTrace(); } finally { target.close(); // System.out.println("Finally (proxy): " + code); } return code; } /* * Either send an error response, set the properties */ boolean doError(Request request, int code, String msg) { if (noErrorReturn) { request.props.put(prefix + "errorCode", "" + code); request.props.put(prefix + "errorMsg", "" + msg); return false; } else { request.sendError(code, msg); return true; } } /** * See if the content needs to be filtered. * Return "true" if "modifyContent" should be called * @param headers Vector of mime headers for data to proxy */ protected boolean shouldFilter(MimeHeaders headers) { String type = headers.get("Content-Type"); // System.out.println("Modify?? " + type); return (headers.get("location") != null || (type != null && type.indexOf("text/html") >= 0)); } /** * See if this is one of my requests. * This method can be overridden to do more sophisticated mappings. * @param request The standard request object */ public boolean isMine(Request request) { return request.url.startsWith(urlPrefix); } /** * Return a reference to our page mapper, to allow futzing with the page * maps from the outside */ public MapPage getMapper() { return mapper; } /** * Rewrite the links in an html file so they resolve correctly * in proxy mode. * * @param request The original request to this "proxy" * @param content The content that needs to be rewritten. * @return true if the headers and content should be sent to the client, false otherwise * Modifies "headers" as a side effect */ public byte[] modifyContent(Request request, byte[] content) { byte[] result; result = mapper.convertHtml(new String(content)).getBytes(); /* * Now fix the content length */ request.responseHeaders.put("Content-Length", result.length); /* * Rewrite the location header, if any */ String location = request.responseHeaders.get("location"); if (location != null) { request.setStatus(302); // XXX doesn't belong here, should copy // targets status String fixed = mapper.convertString(location); if (fixed != null) { String newLocation = request.serverUrl() + fixed; request.responseHeaders.put("Location", newLocation); } } return result; } /* * We need to fix the path and domain. */ String mapCookie(String line) { Cookie cookie = new Cookie(line); String path = cookie.get("path"); if (path == null) { cookie.put("path", urlPrefix); } else { cookie.put("path", urlPrefix + path.substring(1)); } cookie.remove("domain"); // XXX broken! return cookie.toString(); } /* This should be combined with the CookieFilter */ public static class Cookie { StringMap map; // cookie parameters /** * Turn cookie parameters into a String Map * @param cookie The cookie value */ public Cookie(String cookie) { StringTokenizer st = new StringTokenizer(cookie, ";"); map = new StringMap(); map.put("value", st.nextToken().trim()); while (st.hasMoreTokens()) { String param = st.nextToken(); int index = param.indexOf('='); if (index > 0) { map.put(param.substring(0,index).trim().toLowerCase(), param.substring(index+1).trim()); } else { map.put(param.trim(),""); } } } public String get(String key) { return map.get(key); } public void put(String key, String value) { map.put(key, value); } public void remove(String key) { map.remove(key); } public String toString() { StringBuffer sb = new StringBuffer(map.get("value")); for (int i = 1; i < map.size(); i++) { sb.append("; ").append(map.getKey(i)); sb.append("=").append(map.get(i)); } return sb.toString(); } } }