/*
* 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();
}
}
}