
com.twelvemonkeys.servlet.ProxyServlet Maven / Gradle / Ivy
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Enumeration;
/**
* A simple proxy servlet implementation. Supports HTTP and HTTPS.
*
* Note: The servlet is not a true HTTP proxy as described in
* RFC 2616,
* instead it passes on all incoming HTTP requests to the configured remote
* server.
* Useful for bypassing firewalls or to avoid exposing internal network
* infrastructure to external clients.
*
* At the moment, no caching of content is implemented.
*
* If the {@code remoteServer} init parameter is not set, the servlet will
* respond by sending a {@code 500 Internal Server Error} response to the client.
* If the configured remote server is down, or unreachable, the servlet will
* respond by sending a {@code 502 Bad Gateway} response to the client.
* Otherwise, the response from the remote server will be tunneled unmodified
* to the client.
*
* @author Harald Kuhr
* @author last modified by $Author: haku $
*
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java#1 $
*/
public class ProxyServlet extends GenericServlet {
/** Remote server host name or IP address */
protected String mRemoteServer = null;
/** Remote server port */
protected int mRemotePort = 80;
/** Remote server "mount" path */
protected String mRemotePath = "";
private static final String HTTP_REQUEST_HEADER_HOST = "host";
private static final String HTTP_RESPONSE_HEADER_SERVER = "server";
private static final String MESSAGE_REMOTE_SERVER_NOT_CONFIGURED = "Remote server not configured.";
/**
* Called by {@code init} to set the remote server. Must be a valid host
* name or IP address. No default.
*
* @param pRemoteServer
*/
public void setRemoteServer(String pRemoteServer) {
mRemoteServer = pRemoteServer;
}
/**
* Called by {@code init} to set the remote port. Must be a number.
* Default is {@code 80}.
*
* @param pRemotePort
*/
public void setRemotePort(String pRemotePort) {
try {
mRemotePort = Integer.parseInt(pRemotePort);
}
catch (NumberFormatException e) {
log("RemotePort must be a number!", e);
}
}
/**
* Called by {@code init} to set the remote path. May be an empty string
* for the root path, or any other valid path on the remote server.
* Default is {@code ""}.
*
* @param pRemotePath
*/
public void setRemotePath(String pRemotePath) {
if (StringUtil.isEmpty(pRemotePath)) {
pRemotePath = "";
}
else if (pRemotePath.charAt(0) != '/') {
pRemotePath = "/" + pRemotePath;
}
mRemotePath = pRemotePath;
}
/**
* Override {@code service} to use HTTP specifics.
*
* @param pRequest
* @param pResponse
*
* @throws ServletException
* @throws IOException
*
* @see #service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException {
service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse);
}
/**
* Services a single request.
* Supports HTTP and HTTPS.
*
* @param pRequest
* @param pResponse
*
* @throws ServletException
* @throws IOException
*
* @see ProxyServlet Class descrition
*/
protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException {
// Sanity check configuration
if (mRemoteServer == null) {
log(MESSAGE_REMOTE_SERVER_NOT_CONFIGURED);
pResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
MESSAGE_REMOTE_SERVER_NOT_CONFIGURED);
return;
}
HttpURLConnection remoteConnection = null;
try {
// Recreate request URI for remote request
String requestURI = createRemoteRequestURI(pRequest);
URL remoteURL = new URL(pRequest.getScheme(), mRemoteServer, mRemotePort, requestURI);
// Get connection, with method from original request
// NOTE: The actual connection is not done before we ask for streams...
// NOTE: The HttpURLConnection is supposed to handle multiple
// requests to the same server internally
String method = pRequest.getMethod();
remoteConnection = (HttpURLConnection) remoteURL.openConnection();
remoteConnection.setRequestMethod(method);
// Copy header fields
copyHeadersFromClient(pRequest, remoteConnection);
// Do proxy specifc stuff?
// TODO: Read up the specs from RFC 2616 (HTTP) on proxy behaviour
// TODO: RFC 2616 says "[a] proxy server MUST NOT establish an HTTP/1.1
// persistent connection with an HTTP/1.0 client"
// Copy message body from client to remote server
copyBodyFromClient(pRequest, remoteConnection);
// Set response status code from remote server to client
int responseCode = remoteConnection.getResponseCode();
pResponse.setStatus(responseCode);
//System.out.println("Response is: " + responseCode + " " + remoteConnection.getResponseMessage());
// Copy header fields back
copyHeadersToClient(remoteConnection, pResponse);
// More proxy specific stuff?
// Copy message body from remote server to client
copyBodyToClient(remoteConnection, pResponse);
}
catch (ConnectException e) {
// In case we could not connecto to the remote server
log("Could not connect to remote server.", e);
pResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, e.getMessage());
}
finally {
// Disconnect from server
// TODO: Should we actually do this?
if (remoteConnection != null) {
remoteConnection.disconnect();
}
}
}
/**
* Copies the message body from the remote server to the client (outgoing
* {@code HttpServletResponse}).
*
* @param pRemoteConnection
* @param pResponse
*
* @throws IOException
*/
private void copyBodyToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) throws IOException {
InputStream fromRemote = null;
OutputStream toClient = null;
try {
// Get either input or error stream
try {
fromRemote = pRemoteConnection.getInputStream();
}
catch (IOException e) {
// If exception, use errorStream instead
fromRemote = pRemoteConnection.getErrorStream();
}
// I guess the stream might be null if there is no response other
// than headers (Continue, No Content, etc).
if (fromRemote != null) {
toClient = pResponse.getOutputStream();
FileUtil.copy(fromRemote, toClient);
}
}
finally {
if (fromRemote != null) {
try {
fromRemote.close();
}
catch (IOException e) {
log("Stream from remote could not be closed.", e);
}
}
if (toClient != null) {
try {
toClient.close();
}
catch (IOException e) {
log("Stream to client could not be closed.", e);
}
}
}
}
/**
* Copies the message body from the client (incomming
* {@code HttpServletRequest}) to the remote server if the request method
* is {@code POST} or PUT.
* Otherwise this method does nothing.
*
* @param pRequest
* @param pRemoteConnection
*
* @throws java.io.IOException
*/
private void copyBodyFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) throws IOException {
// If this is a POST or PUT, copy message body from client remote server
if (!("POST".equals(pRequest.getMethod()) || "PUT".equals(pRequest.getMethod()))) {
return;
}
// NOTE: Setting doOutput to true, will make it a POST request (why?)...
pRemoteConnection.setDoOutput(true);
// Get streams and do the copying
InputStream fromClient = null;
OutputStream toRemote = null;
try {
fromClient = pRequest.getInputStream();
toRemote = pRemoteConnection.getOutputStream();
FileUtil.copy(fromClient, toRemote);
}
finally {
if (fromClient != null) {
try {
fromClient.close();
}
catch (IOException e) {
log("Stream from client could not be closed.", e);
}
}
if (toRemote != null) {
try {
toRemote.close();
}
catch (IOException e) {
log("Stream to remote could not be closed.", e);
}
}
}
}
/**
* Creates the remote request URI based on the incoming request.
* The URI will include any query strings etc.
*
* @param pRequest
*
* @return a {@code String} representing the remote request URI
*/
private String createRemoteRequestURI(HttpServletRequest pRequest) {
StringBuilder requestURI = new StringBuilder(mRemotePath);
requestURI.append(pRequest.getPathInfo());
if (!StringUtil.isEmpty(pRequest.getQueryString())) {
requestURI.append("?");
requestURI.append(pRequest.getQueryString());
}
return requestURI.toString();
}
/**
* Copies headers from the remote connection back to the client
* (the outgoing HttpServletResponse). All headers except the "Server"
* header are copied.
*
* @param pRemoteConnection
* @param pResponse
*/
private void copyHeadersToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) {
// NOTE: There is no getHeaderFieldCount method or similar...
// Also, the getHeaderFields() method was introduced in J2SE 1.4, and
// we want to be 1.2 compatible.
// So, just try to loop until there are no more headers.
int i = 0;
while (true) {
String key = pRemoteConnection.getHeaderFieldKey(i);
// NOTE: getHeaderField(String) returns only the last value
String value = pRemoteConnection.getHeaderField(i);
// If the key is not null, life is simple, and Sun is shining
// However, the default implementations includes the HTTP response
// code ("HTTP/1.1 200 Ok" or similar) as a header field with
// key "null" (why..?)...
// In addition, we want to skip the original "Server" header
if (key != null && !HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) {
//System.out.println("client <<<-- remote: " + key + ": " + value);
pResponse.setHeader(key, value);
}
else if (value == null) {
// If BOTH key and value is null, there are no more header fields
break;
}
i++;
}
/* 1.4+ version below....
Map headers = pRemoteConnection.getHeaderFields();
for (Iterator iterator = headers.entrySet().iterator(); iterator.hasNext();) {
Map.Entry header = (Map.Entry) iterator.next();
List values = (List) header.getValue();
for (Iterator valueIter = values.iterator(); valueIter.hasNext();) {
String value = (String) valueIter.next();
String key = (String) header.getKey();
// Skip the server header
if (HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) {
key = null;
}
// The default implementations includes the HTTP response code
// ("HTTP/1.1 200 Ok" or similar) as a header field with
// key "null" (why..?)...
if (key != null) {
//System.out.println("client <<<-- remote: " + key + ": " + value);
pResponse.setHeader(key, value);
}
}
}
*/
}
/**
* Copies headers from the client (the incoming {@code HttpServletRequest})
* to the outgoing connection.
* All headers except the "Host" header are copied.
*
* @param pRequest
* @param pRemoteConnection
*/
private void copyHeadersFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) {
Enumeration headerNames = pRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
Enumeration headerValues = pRequest.getHeaders(headerName);
// Skip the "host" header, as we want something else
if (HTTP_REQUEST_HEADER_HOST.equalsIgnoreCase(headerName)) {
// Skip this header
headerName = null;
}
// Set the the header to the remoteConnection
if (headerName != null) {
// Convert from multiple line to single line, comma separated, as
// there seems to be a shortcoming in the URLConneciton API...
StringBuilder headerValue = new StringBuilder();
while (headerValues.hasMoreElements()) {
String value = (String) headerValues.nextElement();
headerValue.append(value);
if (headerValues.hasMoreElements()) {
headerValue.append(", ");
}
}
//System.out.println("client -->>> remote: " + headerName + ": " + headerValue);
pRemoteConnection.setRequestProperty(headerName, headerValue.toString());
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy