Last active
August 29, 2015 14:16
-
-
Save subchen/27cf1711812e4d041077 to your computer and use it in GitHub Desktop.
HTTP proxy servlet: http://edwardstx.net/2010/06/http-proxy-servlet/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// package de.spieleck.servlets; | |
// ProxyServlet - serving pages from foreign servers.... | |
// | |
import java.io.*; | |
import java.net.*; | |
import java.lang.Integer; | |
import java.util.StringTokenizer; | |
import javax.servlet.*; | |
import javax.servlet.http.*; | |
/** | |
* Serves pages which are fetched from another HTTP-Server | |
* useful for going thru firewalls and other trickery... | |
* <P> | |
* The communication is somewhat this way: | |
* <UL> | |
* <LI>Client requests data from servlet | |
* <LI>Servlet interprets path and requests data from remote server | |
* <LI>Servlet obtains answer from remote server and forwards it to client | |
* <LI>Client obtains answer | |
* </UL> | |
* <P> | |
* XXX There is a problem with If-Modified and If-None-Match requests: | |
* the 304 Not Modified answer does not go thru the servelet in the | |
* backward direction. It could be that the HttpServletResponse does hava | |
* some sideeffects which are not helpfull in this special situation. | |
* This type of request is currently avoided by removing all "If-" requests. | |
* <br /> | |
* <b>Note:</b> This servlet is actually buggy. It is buggy since it does | |
* not solve all problems, it only solves the problems I needed to solve. | |
* Many thanks to Thorsten Gast the creator of dirjack | |
* for pointing at least some bugs. | |
* @author <a href="mailto:frank -at- spieleck.de">Frank Nestel</a>. | |
*/ | |
public class ProxyServlet extends HttpServlet | |
{ | |
/** | |
* "Official" HTTP line end | |
*/ | |
public final static String CRLF = "\r\n"; | |
public final static String LF = "\n"; | |
/** | |
* remote path | |
*/ | |
protected String remotePath; | |
/** | |
* remote server | |
*/ | |
protected String remoteServer; | |
/** | |
* Port at remote server | |
*/ | |
protected int remotePort; | |
/** | |
* Debug mode? | |
*/ | |
protected boolean debugFlag; | |
/** Init | |
*/ | |
public void init(ServletConfig config) | |
throws ServletException | |
{ | |
super.init(config); | |
remotePath = getInitParameter("remotePath"); | |
remoteServer = getInitParameter("remoteServer"); | |
String remotePortStr= getInitParameter("remotePort"); | |
if ( remotePath == null || remoteServer == null ) | |
throw new ServletException( | |
"Servlet needs remotePath & remoteServer."); | |
if ( remotePortStr != null ) | |
{ | |
try | |
{ | |
remotePort = Integer.parseInt(remotePortStr); | |
} | |
catch ( Exception e ) | |
{ | |
throw new ServletException("Port must be a number!"); | |
} | |
} | |
else | |
remotePort = 80; | |
if ( "".equals(remotePath) ) | |
remotePath = ""; // XXX ??? "/" | |
else if ( remotePath.charAt(0) != '/' ) | |
remotePath = "/"+remotePath; | |
debugFlag = "true".equals(getInitParameter("debug")); | |
// | |
log("remote="+remoteServer+" "+remotePort+" "+remotePath); | |
} | |
/// Returns a string containing information about the author, version, and | |
// copyright of the servlet. | |
public String getServletInfo() | |
{ | |
return "Online redirecting content."; | |
} | |
/// Services a single request from the client. | |
// @param req the servlet request | |
// @param req the servlet response | |
// @exception ServletException when an exception has occurred | |
public void service( HttpServletRequest req, HttpServletResponse res ) | |
throws ServletException, IOException | |
{ | |
// | |
// Connect to "remote" server: | |
Socket sock; | |
OutputStream out; | |
InputStream in; | |
// | |
try | |
{ | |
sock = new Socket(remoteServer, remotePort); // !!!!!!!! | |
out = new BufferedOutputStream(sock.getOutputStream()); | |
in = new BufferedInputStream(sock.getInputStream()); | |
} | |
catch (IOException e) | |
{ | |
res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, | |
"Socket opening: "+remoteServer+" "+remotePort); | |
return; | |
} | |
try | |
{ | |
// | |
// Build up a HTTP request from pure strings: | |
StringBuffer sb = new StringBuffer(200); | |
sb.append(req.getMethod()); | |
sb.append(' '); | |
String pi = req.getPathInfo(); | |
sb.append(remotePath); | |
if ( pi != null ) | |
{ | |
appendCleaned(sb, pi); | |
} | |
else | |
sb.append("/"); | |
if ( req.getQueryString() != null ) | |
{ | |
sb.append('?'); | |
appendCleaned(sb, req.getQueryString()); | |
} | |
sb.append(' '); | |
sb.append("HTTP/1.0"); | |
sb.append(CRLF); | |
log(sb.toString()); | |
out.write(sb.toString().getBytes()); | |
java.util.Enumeration en = req.getHeaderNames(); | |
while ( en.hasMoreElements() ) | |
{ | |
String k = (String) en.nextElement(); | |
// Filter incoming headers: | |
if ( "Host".equalsIgnoreCase(k) ) | |
{ | |
sb.setLength(0); | |
sb.append(k); | |
sb.append(": "); | |
sb.append(remoteServer); | |
sb.append(":"); | |
sb.append(remotePort); | |
sb.append(CRLF); | |
log("c["+k+"]: "+sb+" "+req.getHeader(k)); | |
out.write(sb.toString().getBytes()); | |
} | |
// | |
// Throw away persistant connections between servers | |
// Throw away request potentially causing a 304 response. | |
else if ( | |
! "Connection".equalsIgnoreCase(k) | |
&& ! "If-Modified-Since".equalsIgnoreCase(k) | |
&& ! "If-None-Match".equalsIgnoreCase(k) | |
) | |
{ | |
sb.setLength(0); | |
sb.append(k); | |
sb.append(": "); | |
sb.append(req.getHeader(k)); | |
sb.append(CRLF); | |
log("=["+k+"]: "+req.getHeader(k)); | |
out.write(sb.toString().getBytes()); | |
} | |
else | |
{ | |
log("*["+k+"]: "+req.getHeader(k)); | |
} | |
} | |
// Finish request header by an empty line | |
out.write(CRLF.getBytes()); | |
// Copy post data | |
InputStream inr = req.getInputStream(); | |
copyStream(inr, out); | |
out.flush(); | |
log("Remote request finished. Reading answer."); | |
// Now we have finished the outgoing request. | |
// We'll now see, what is coming back from remote: | |
// Get the answer, treat its header and copy the stream data: | |
if ( treatHeader(in, req, res) ) | |
{ | |
log("+ copyStream"); | |
// if ( debugFlag ) res.setContentType("text/plain"); | |
out = res.getOutputStream(); | |
copyStream(in, out); | |
} | |
else | |
log("- copyStream"); | |
} | |
catch (IOException e) | |
{ | |
log("out-in.open!"); | |
// res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, | |
// "out-in open!"); | |
return; | |
} | |
try | |
{ | |
// out.close(); | |
in.close(); | |
sock.close(); | |
} | |
catch (IOException ignore) | |
{ | |
log("Exception "+ignore); | |
} | |
} | |
public static void appendCleaned(StringBuffer sb, String str) | |
{ | |
for(int i = 0; i < str.length(); i++) | |
{ | |
char ch = str.charAt(i); | |
if ( ch == ' ' ) | |
sb.append("%20"); | |
else | |
sb.append(ch); | |
} | |
} | |
/** | |
* Forward and filter header from backend Request. | |
*/ | |
private boolean treatHeader(InputStream in, | |
HttpServletRequest req, | |
HttpServletResponse res) | |
throws ServletException | |
{ | |
boolean retval = true; | |
byte[] lineBytes = new byte[4096]; | |
int len; | |
String line; | |
try | |
{ | |
// Read the first line of the request. | |
len = readLine(in, lineBytes ); | |
if ( len == -1 || len == 0 ) | |
throw new ServletException( "No Request found in Data." ); | |
{ | |
String line2 = new String( lineBytes, 0, len ); | |
log("head: "+line2+" "+len); | |
} | |
// We mainly skip the header by the foreign server | |
// assuming, that we can handle protocoll mismatch or so! | |
res.setHeader("viaJTTP","JTTP"); | |
// Some more headers require special care .... | |
boolean firstline = true; | |
// Shortcut evaluation skips the read on first time! | |
while ( firstline || ((len=readLine(in,lineBytes)) > 0) ) | |
{ | |
line = new String( lineBytes, 0, len ); | |
int colonPos = line.indexOf( ":" ); | |
if ( firstline && colonPos == -1 ) | |
{ | |
// Special first line considerations ... | |
String headl[] = wordStr(line); | |
log("head: "+line+" "+headl.length); | |
try | |
{ | |
res.setStatus(Integer.parseInt(headl[1])); | |
} | |
catch ( NumberFormatException ignore ) | |
{ | |
log("ID exception: "+headl); | |
} | |
catch ( Exception panik ) | |
{ | |
log("First line invalid!"); | |
return true; | |
} | |
} | |
else if ( colonPos != -1 ) | |
{ | |
String head = line.substring(0,colonPos); | |
// XXX Skip LWS (what is LWS) | |
int i = colonPos + 1; | |
while ( isLWS(line.charAt(i)) ) i++; | |
String value= line.substring(i); | |
log("<"+head+">=<"+ value+">"); | |
if ( head.equalsIgnoreCase("Location") ) | |
{ | |
// res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); | |
// res.setHeader(head, value ); | |
log("Location cutted: "+value); | |
} | |
else if ( head.equalsIgnoreCase( "Content-type" ) ) | |
res.setContentType( value ); | |
else if ( head.equalsIgnoreCase( "Content-length" ) ) | |
{ | |
try | |
{ | |
int cLen = Integer.parseInt( value ); | |
retval = ( cLen > 0 ); | |
res.setContentLength(cLen); | |
} | |
catch ( NumberFormatException ignore ) {} | |
} | |
// Generically treat unknown headers | |
else | |
{ | |
log("^- generic."); | |
res.setHeader(head, value ); | |
} | |
} | |
// XXX We do not treat multiline continuation Headers here | |
// which have not occured anywhere yet. | |
firstline = false; | |
} | |
} | |
catch ( IOException e ) | |
{ | |
log("Header skip problem:"); | |
throw new ServletException("Header skip problem: "+e.getMessage()); | |
} | |
log( "--------------" ); | |
return retval; | |
} | |
/** | |
* Read a RFC2616 line from an InputStream: | |
*/ | |
public int readLine(InputStream in, byte[] b ) | |
throws IOException | |
{ | |
int off2 = 0; | |
while ( off2 < b.length ) | |
{ | |
int r = in.read(); | |
if ( r == -1 ) | |
{ | |
if (off2 == 0 ) | |
return -1; | |
break; | |
} | |
if ( r == 13 ) | |
continue; | |
if ( r == 10 ) | |
break; | |
b[off2] = (byte) r; | |
++off2; | |
} | |
return off2; | |
} | |
/** Copy a file from in to out. | |
* Sub-classes can override this in order to do filtering of some sort. | |
*/ | |
public void copyStream( InputStream in, OutputStream out ) | |
throws IOException | |
{ | |
BufferedInputStream bin = new BufferedInputStream(in); | |
int b; | |
while ( ( b = bin.read() ) != -1 ) | |
out.write(b); | |
} | |
/** | |
* Split a blank separated string into | |
*/ | |
public String[] wordStr( String inp ) | |
{ | |
StringTokenizer tok = new StringTokenizer(inp, " "); | |
int i, n = tok.countTokens(); | |
String[] res = new String[n]; | |
for(i = 0; i < n; i++ ) | |
res[i] = tok.nextToken(); | |
return res; | |
} | |
/** | |
* XXX Should identify RFC2616 LWS | |
*/ | |
protected boolean isLWS(char c) | |
{ | |
return c == ' '; | |
} | |
/** | |
* Capture awaay the standard servlet log .. | |
*/ | |
public void log(String msg) | |
{ | |
if ( debugFlag ) | |
System.err.println("## "+msg); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.edwardstx; | |
import java.io.BufferedInputStream; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.util.ArrayList; | |
import java.util.Enumeration; | |
import java.util.List; | |
import java.util.Map; | |
import javax.servlet.ServletConfig; | |
import javax.servlet.ServletException; | |
import javax.servlet.http.HttpServlet; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import org.apache.commons.fileupload.FileItem; | |
import org.apache.commons.fileupload.FileUploadException; | |
import org.apache.commons.fileupload.disk.DiskFileItemFactory; | |
import org.apache.commons.fileupload.servlet.ServletFileUpload; | |
import org.apache.commons.httpclient.Header; | |
import org.apache.commons.httpclient.HttpClient; | |
import org.apache.commons.httpclient.HttpMethod; | |
import org.apache.commons.httpclient.NameValuePair; | |
import org.apache.commons.httpclient.methods.GetMethod; | |
import org.apache.commons.httpclient.methods.PostMethod; | |
import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource; | |
import org.apache.commons.httpclient.methods.multipart.FilePart; | |
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; | |
import org.apache.commons.httpclient.methods.multipart.Part; | |
import org.apache.commons.httpclient.methods.multipart.StringPart; | |
public class ProxyServlet extends HttpServlet { | |
/** | |
* Serialization UID. | |
*/ | |
private static final long serialVersionUID = 1L; | |
/** | |
* Key for redirect location header. | |
*/ | |
private static final String STRING_LOCATION_HEADER = "Location"; | |
/** | |
* Key for content type header. | |
*/ | |
private static final String STRING_CONTENT_TYPE_HEADER_NAME = "Content-Type"; | |
/** | |
* Key for content length header. | |
*/ | |
private static final String STRING_CONTENT_LENGTH_HEADER_NAME = "Content-Length"; | |
/** | |
* Key for host header | |
*/ | |
private static final String STRING_HOST_HEADER_NAME = "Host"; | |
/** | |
* The directory to use to temporarily store uploaded files | |
*/ | |
private static final File FILE_UPLOAD_TEMP_DIRECTORY = new File(System.getProperty("java.io.tmpdir")); | |
// Proxy host params | |
/** | |
* The host to which we are proxying requests | |
*/ | |
private String stringProxyHost; | |
/** | |
* The port on the proxy host to wihch we are proxying requests. Default value is 80. | |
*/ | |
private int intProxyPort = 80; | |
/** | |
* The (optional) path on the proxy host to wihch we are proxying requests. Default value is "". | |
*/ | |
private String stringProxyPath = ""; | |
/** | |
* The maximum size for uploaded files in bytes. Default value is 5MB. | |
*/ | |
private int intMaxFileUploadSize = 5 * 1024 * 1024; | |
/** | |
* Initialize the <code>ProxyServlet</code> | |
* @param servletConfig The Servlet configuration passed in by the servlet conatiner | |
*/ | |
public void init(ServletConfig servletConfig) { | |
// Get the proxy host | |
String stringProxyHostNew = servletConfig.getInitParameter("proxyHost"); | |
if(stringProxyHostNew == null || stringProxyHostNew.length() == 0) { | |
throw new IllegalArgumentException("Proxy host not set, please set init-param 'proxyHost' in web.xml"); | |
} | |
this.setProxyHost(stringProxyHostNew); | |
// Get the proxy port if specified | |
String stringProxyPortNew = servletConfig.getInitParameter("proxyPort"); | |
if(stringProxyPortNew != null && stringProxyPortNew.length() > 0) { | |
this.setProxyPort(Integer.parseInt(stringProxyPortNew)); | |
} | |
// Get the proxy path if specified | |
String stringProxyPathNew = servletConfig.getInitParameter("proxyPath"); | |
if(stringProxyPathNew != null && stringProxyPathNew.length() > 0) { | |
this.setProxyPath(stringProxyPathNew); | |
} | |
// Get the maximum file upload size if specified | |
String stringMaxFileUploadSize = servletConfig.getInitParameter("maxFileUploadSize"); | |
if(stringMaxFileUploadSize != null && stringMaxFileUploadSize.length() > 0) { | |
this.setMaxFileUploadSize(Integer.parseInt(stringMaxFileUploadSize)); | |
} | |
} | |
/** | |
* Performs an HTTP GET request | |
* @param httpServletRequest The {@link HttpServletRequest} object passed | |
* in by the servlet engine representing the | |
* client request to be proxied | |
* @param httpServletResponse The {@link HttpServletResponse} object by which | |
* we can send a proxied response to the client | |
*/ | |
public void doGet (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) | |
throws IOException, ServletException { | |
// Create a GET request | |
GetMethod getMethodProxyRequest = new GetMethod(this.getProxyURL(httpServletRequest)); | |
// Forward the request headers | |
setProxyRequestHeaders(httpServletRequest, getMethodProxyRequest); | |
// Execute the proxy request | |
this.executeProxyRequest(getMethodProxyRequest, httpServletRequest, httpServletResponse); | |
} | |
/** | |
* Performs an HTTP POST request | |
* @param httpServletRequest The {@link HttpServletRequest} object passed | |
* in by the servlet engine representing the | |
* client request to be proxied | |
* @param httpServletResponse The {@link HttpServletResponse} object by which | |
* we can send a proxied response to the client | |
*/ | |
public void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) | |
throws IOException, ServletException { | |
// Create a standard POST request | |
PostMethod postMethodProxyRequest = new PostMethod(this.getProxyURL(httpServletRequest)); | |
// Forward the request headers | |
setProxyRequestHeaders(httpServletRequest, postMethodProxyRequest); | |
// Check if this is a mulitpart (file upload) POST | |
if(ServletFileUpload.isMultipartContent(httpServletRequest)) { | |
this.handleMultipartPost(postMethodProxyRequest, httpServletRequest); | |
} else { | |
this.handleStandardPost(postMethodProxyRequest, httpServletRequest); | |
} | |
// Execute the proxy request | |
this.executeProxyRequest(postMethodProxyRequest, httpServletRequest, httpServletResponse); | |
} | |
/** | |
* Sets up the given {@link PostMethod} to send the same multipart POST | |
* data as was sent in the given {@link HttpServletRequest} | |
* @param postMethodProxyRequest The {@link PostMethod} that we are | |
* configuring to send a multipart POST request | |
* @param httpServletRequest The {@link HttpServletRequest} that contains | |
* the mutlipart POST data to be sent via the {@link PostMethod} | |
*/ | |
@SuppressWarnings("unchecked") | |
private void handleMultipartPost(PostMethod postMethodProxyRequest, HttpServletRequest httpServletRequest) | |
throws ServletException { | |
// Create a factory for disk-based file items | |
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory(); | |
// Set factory constraints | |
diskFileItemFactory.setSizeThreshold(this.getMaxFileUploadSize()); | |
diskFileItemFactory.setRepository(FILE_UPLOAD_TEMP_DIRECTORY); | |
// Create a new file upload handler | |
ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory); | |
// Parse the request | |
try { | |
// Get the multipart items as a list | |
List<FileItem> listFileItems = (List<FileItem>) servletFileUpload.parseRequest(httpServletRequest); | |
// Create a list to hold all of the parts | |
List<Part> listParts = new ArrayList<Part>(); | |
// Iterate the multipart items list | |
for(FileItem fileItemCurrent : listFileItems) { | |
// If the current item is a form field, then create a string part | |
if (fileItemCurrent.isFormField()) { | |
StringPart stringPart = new StringPart( | |
fileItemCurrent.getFieldName(), // The field name | |
fileItemCurrent.getString() // The field value | |
); | |
// Add the part to the list | |
listParts.add(stringPart); | |
} else { | |
// The item is a file upload, so we create a FilePart | |
FilePart filePart = new FilePart( | |
fileItemCurrent.getFieldName(), // The field name | |
new ByteArrayPartSource( | |
fileItemCurrent.getName(), // The uploaded file name | |
fileItemCurrent.get() // The uploaded file contents | |
) | |
); | |
// Add the part to the list | |
listParts.add(filePart); | |
} | |
} | |
MultipartRequestEntity multipartRequestEntity = new MultipartRequestEntity( | |
listParts.toArray(new Part[] {}), | |
postMethodProxyRequest.getParams() | |
); | |
postMethodProxyRequest.setRequestEntity(multipartRequestEntity); | |
// The current content-type header (received from the client) IS of | |
// type "multipart/form-data", but the content-type header also | |
// contains the chunk boundary string of the chunks. Currently, this | |
// header is using the boundary of the client request, since we | |
// blindly copied all headers from the client request to the proxy | |
// request. However, we are creating a new request with a new chunk | |
// boundary string, so it is necessary that we re-set the | |
// content-type string to reflect the new chunk boundary string | |
postMethodProxyRequest.setRequestHeader(STRING_CONTENT_TYPE_HEADER_NAME, multipartRequestEntity.getContentType()); | |
} catch (FileUploadException fileUploadException) { | |
throw new ServletException(fileUploadException); | |
} | |
} | |
/** | |
* Sets up the given {@link PostMethod} to send the same standard POST | |
* data as was sent in the given {@link HttpServletRequest} | |
* @param postMethodProxyRequest The {@link PostMethod} that we are | |
* configuring to send a standard POST request | |
* @param httpServletRequest The {@link HttpServletRequest} that contains | |
* the POST data to be sent via the {@link PostMethod} | |
*/ | |
@SuppressWarnings("unchecked") | |
private void handleStandardPost(PostMethod postMethodProxyRequest, HttpServletRequest httpServletRequest) { | |
// Get the client POST data as a Map | |
Map<String, String[]> mapPostParameters = (Map<String,String[]>) httpServletRequest.getParameterMap(); | |
// Create a List to hold the NameValuePairs to be passed to the PostMethod | |
List<NameValuePair> listNameValuePairs = new ArrayList<NameValuePair>(); | |
// Iterate the parameter names | |
for(String stringParameterName : mapPostParameters.keySet()) { | |
// Iterate the values for each parameter name | |
String[] stringArrayParameterValues = mapPostParameters.get(stringParameterName); | |
for(String stringParamterValue : stringArrayParameterValues) { | |
// Create a NameValuePair and store in list | |
NameValuePair nameValuePair = new NameValuePair(stringParameterName, stringParamterValue); | |
listNameValuePairs.add(nameValuePair); | |
} | |
} | |
// Set the proxy request POST data | |
postMethodProxyRequest.setRequestBody(listNameValuePairs.toArray(new NameValuePair[] { })); | |
} | |
/** | |
* Executes the {@link HttpMethod} passed in and sends the proxy response | |
* back to the client via the given {@link HttpServletResponse} | |
* @param httpMethodProxyRequest An object representing the proxy request to be made | |
* @param httpServletResponse An object by which we can send the proxied | |
* response back to the client | |
* @throws IOException Can be thrown by the {@link HttpClient}.executeMethod | |
* @throws ServletException Can be thrown to indicate that another error has occurred | |
*/ | |
private void executeProxyRequest( | |
HttpMethod httpMethodProxyRequest, | |
HttpServletRequest httpServletRequest, | |
HttpServletResponse httpServletResponse) | |
throws IOException, ServletException { | |
// Create a default HttpClient | |
HttpClient httpClient = new HttpClient(); | |
httpMethodProxyRequest.setFollowRedirects(false); | |
// Execute the request | |
int intProxyResponseCode = httpClient.executeMethod(httpMethodProxyRequest); | |
// Check if the proxy response is a redirect | |
// The following code is adapted from org.tigris.noodle.filters.CheckForRedirect | |
// Hooray for open source software | |
if(intProxyResponseCode >= HttpServletResponse.SC_MULTIPLE_CHOICES /* 300 */ | |
&& intProxyResponseCode < HttpServletResponse.SC_NOT_MODIFIED /* 304 */) { | |
String stringStatusCode = Integer.toString(intProxyResponseCode); | |
String stringLocation = httpMethodProxyRequest.getResponseHeader(STRING_LOCATION_HEADER).getValue(); | |
if(stringLocation == null) { | |
throw new ServletException("Recieved status code: " + stringStatusCode | |
+ " but no " + STRING_LOCATION_HEADER + " header was found in the response"); | |
} | |
// Modify the redirect to go to this proxy servlet rather that the proxied host | |
String stringMyHostName = httpServletRequest.getServerName(); | |
if(httpServletRequest.getServerPort() != 80) { | |
stringMyHostName += ":" + httpServletRequest.getServerPort(); | |
} | |
stringMyHostName += httpServletRequest.getContextPath(); | |
httpServletResponse.sendRedirect(stringLocation.replace(getProxyHostAndPort() + this.getProxyPath(), stringMyHostName)); | |
return; | |
} else if(intProxyResponseCode == HttpServletResponse.SC_NOT_MODIFIED) { | |
// 304 needs special handling. See: | |
// http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304 | |
// We get a 304 whenever passed an 'If-Modified-Since' | |
// header and the data on disk has not changed; server | |
// responds w/ a 304 saying I'm not going to send the | |
// body because the file has not changed. | |
httpServletResponse.setIntHeader(STRING_CONTENT_LENGTH_HEADER_NAME, 0); | |
httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); | |
return; | |
} | |
// Pass the response code back to the client | |
httpServletResponse.setStatus(intProxyResponseCode); | |
// Pass response headers back to the client | |
Header[] headerArrayResponse = httpMethodProxyRequest.getResponseHeaders(); | |
for(Header header : headerArrayResponse) { | |
httpServletResponse.setHeader(header.getName(), header.getValue()); | |
} | |
// Send the content to the client | |
InputStream inputStreamProxyResponse = httpMethodProxyRequest.getResponseBodyAsStream(); | |
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStreamProxyResponse); | |
OutputStream outputStreamClientResponse = httpServletResponse.getOutputStream(); | |
int intNextByte; | |
while ( ( intNextByte = bufferedInputStream.read() ) != -1 ) { | |
outputStreamClientResponse.write(intNextByte); | |
} | |
} | |
public String getServletInfo() { | |
return "Jason's Proxy Servlet"; | |
} | |
/** | |
* Retreives all of the headers from the servlet request and sets them on | |
* the proxy request | |
* | |
* @param httpServletRequest The request object representing the client's | |
* request to the servlet engine | |
* @param httpMethodProxyRequest The request that we are about to send to | |
* the proxy host | |
*/ | |
@SuppressWarnings("unchecked") | |
private void setProxyRequestHeaders(HttpServletRequest httpServletRequest, HttpMethod httpMethodProxyRequest) { | |
// Get an Enumeration of all of the header names sent by the client | |
Enumeration enumerationOfHeaderNames = httpServletRequest.getHeaderNames(); | |
while(enumerationOfHeaderNames.hasMoreElements()) { | |
String stringHeaderName = (String) enumerationOfHeaderNames.nextElement(); | |
if(stringHeaderName.equalsIgnoreCase(STRING_CONTENT_LENGTH_HEADER_NAME)) | |
continue; | |
// As per the Java Servlet API 2.5 documentation: | |
// Some headers, such as Accept-Language can be sent by clients | |
// as several headers each with a different value rather than | |
// sending the header as a comma separated list. | |
// Thus, we get an Enumeration of the header values sent by the client | |
Enumeration enumerationOfHeaderValues = httpServletRequest.getHeaders(stringHeaderName); | |
while(enumerationOfHeaderValues.hasMoreElements()) { | |
String stringHeaderValue = (String) enumerationOfHeaderValues.nextElement(); | |
// In case the proxy host is running multiple virtual servers, | |
// rewrite the Host header to ensure that we get content from | |
// the correct virtual server | |
if(stringHeaderName.equalsIgnoreCase(STRING_HOST_HEADER_NAME)){ | |
stringHeaderValue = getProxyHostAndPort(); | |
} | |
Header header = new Header(stringHeaderName, stringHeaderValue); | |
// Set the same header on the proxy request | |
httpMethodProxyRequest.setRequestHeader(header); | |
} | |
} | |
} | |
// Accessors | |
private String getProxyURL(HttpServletRequest httpServletRequest) { | |
// Set the protocol to HTTP | |
String stringProxyURL = "http://" + this.getProxyHostAndPort(); | |
// Check if we are proxying to a path other that the document root | |
if(!this.getProxyPath().equalsIgnoreCase("")){ | |
stringProxyURL += this.getProxyPath(); | |
} | |
// Handle the path given to the servlet | |
stringProxyURL += httpServletRequest.getPathInfo(); | |
// Handle the query string | |
if(httpServletRequest.getQueryString() != null) { | |
stringProxyURL += "?" + httpServletRequest.getQueryString(); | |
} | |
return stringProxyURL; | |
} | |
private String getProxyHostAndPort() { | |
if(this.getProxyPort() == 80) { | |
return this.getProxyHost(); | |
} else { | |
return this.getProxyHost() + ":" + this.getProxyPort(); | |
} | |
} | |
private String getProxyHost() { | |
return this.stringProxyHost; | |
} | |
private void setProxyHost(String stringProxyHostNew) { | |
this.stringProxyHost = stringProxyHostNew; | |
} | |
private int getProxyPort() { | |
return this.intProxyPort; | |
} | |
private void setProxyPort(int intProxyPortNew) { | |
this.intProxyPort = intProxyPortNew; | |
} | |
private String getProxyPath() { | |
return this.stringProxyPath; | |
} | |
private void setProxyPath(String stringProxyPathNew) { | |
this.stringProxyPath = stringProxyPathNew; | |
} | |
private int getMaxFileUploadSize() { | |
return this.intMaxFileUploadSize; | |
} | |
private void setMaxFileUploadSize(int intMaxFileUploadSizeNew) { | |
this.intMaxFileUploadSize = intMaxFileUploadSizeNew; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.mitre.dsmiley.httpproxy; | |
/** | |
* Copyright MITRE | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
import org.apache.http.Header; | |
import org.apache.http.HttpEntity; | |
import org.apache.http.HttpEntityEnclosingRequest; | |
import org.apache.http.HttpHeaders; | |
import org.apache.http.HttpHost; | |
import org.apache.http.HttpRequest; | |
import org.apache.http.HttpResponse; | |
import org.apache.http.client.HttpClient; | |
import org.apache.http.client.methods.AbortableHttpRequest; | |
import org.apache.http.client.params.ClientPNames; | |
import org.apache.http.client.params.CookiePolicy; | |
import org.apache.http.client.utils.URIUtils; | |
import org.apache.http.entity.InputStreamEntity; | |
import org.apache.http.impl.client.DefaultHttpClient; | |
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; | |
import org.apache.http.message.BasicHeader; | |
import org.apache.http.message.BasicHttpEntityEnclosingRequest; | |
import org.apache.http.message.BasicHttpRequest; | |
import org.apache.http.message.HeaderGroup; | |
import org.apache.http.params.BasicHttpParams; | |
import org.apache.http.params.HttpParams; | |
import org.apache.http.util.EntityUtils; | |
import javax.servlet.ServletException; | |
import javax.servlet.http.Cookie; | |
import javax.servlet.http.HttpServlet; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import java.io.Closeable; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
import java.lang.reflect.Constructor; | |
import java.net.HttpCookie; | |
import java.net.URI; | |
import java.util.BitSet; | |
import java.util.Enumeration; | |
import java.util.Formatter; | |
import java.util.List; | |
/** | |
* An HTTP reverse proxy/gateway servlet. It is designed to be extended for customization | |
* if desired. Most of the work is handled by | |
* <a href="http://hc.apache.org/httpcomponents-client-ga/">Apache HttpClient</a>. | |
* <p> | |
* There are alternatives to a servlet based proxy such as Apache mod_proxy if that is available to you. However | |
* this servlet is easily customizable by Java, secure-able by your web application's security (e.g. spring-security), | |
* portable across servlet engines, and is embeddable into another web application. | |
* </p> | |
* <p> | |
* Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html | |
* </p> | |
* | |
* @author David Smiley dsmiley@mitre.org | |
*/ | |
public class ProxyServlet extends HttpServlet { | |
/* INIT PARAMETER NAME CONSTANTS */ | |
/** A boolean parameter name to enable logging of input and target URLs to the servlet log. */ | |
public static final String P_LOG = "log"; | |
/** A boolean parameter name to enable forwarding of the client IP */ | |
public static final String P_FORWARDEDFOR = "forwardip"; | |
/** The parameter name for the target (destination) URI to proxy to. */ | |
protected static final String P_TARGET_URI = "targetUri"; | |
protected static final String ATTR_TARGET_URI = | |
ProxyServlet.class.getSimpleName() + ".targetUri"; | |
protected static final String ATTR_TARGET_HOST = | |
ProxyServlet.class.getSimpleName() + ".targetHost"; | |
/* MISC */ | |
protected boolean doLog = false; | |
protected boolean doForwardIP = true; | |
/** User agents shouldn't send the url fragment but what if it does? */ | |
protected boolean doSendUrlFragment = true; | |
//These next 3 are cached here, and should only be referred to in initialization logic. See the | |
// ATTR_* parameters. | |
/** From the configured parameter "targetUri". */ | |
protected String targetUri; | |
protected URI targetUriObj;//new URI(targetUri) | |
protected HttpHost targetHost;//URIUtils.extractHost(targetUriObj); | |
private HttpClient proxyClient; | |
@Override | |
public String getServletInfo() { | |
return "A proxy servlet by David Smiley, dsmiley@apache.org"; | |
} | |
protected String getTargetUri(HttpServletRequest servletRequest) { | |
return (String) servletRequest.getAttribute(ATTR_TARGET_URI); | |
} | |
private HttpHost getTargetHost(HttpServletRequest servletRequest) { | |
return (HttpHost) servletRequest.getAttribute(ATTR_TARGET_HOST); | |
} | |
/** | |
* Reads a configuration parameter. By default it reads servlet init parameters but | |
* it can be overridden. | |
*/ | |
protected String getConfigParam(String key) { | |
return getServletConfig().getInitParameter(key); | |
} | |
@Override | |
public void init() throws ServletException { | |
String doLogStr = getConfigParam(P_LOG); | |
if (doLogStr != null) { | |
this.doLog = Boolean.parseBoolean(doLogStr); | |
} | |
String doForwardIPString = getConfigParam(P_FORWARDEDFOR); | |
if (doForwardIPString != null) { | |
this.doForwardIP = Boolean.parseBoolean(doForwardIPString); | |
} | |
initTarget();//sets target* | |
HttpParams hcParams = new BasicHttpParams(); | |
hcParams.setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES); | |
readConfigParam(hcParams, ClientPNames.HANDLE_REDIRECTS, Boolean.class); | |
proxyClient = createHttpClient(hcParams); | |
} | |
protected void initTarget() throws ServletException { | |
targetUri = getConfigParam(P_TARGET_URI); | |
if (targetUri == null) | |
throw new ServletException(P_TARGET_URI+" is required."); | |
//test it's valid | |
try { | |
targetUriObj = new URI(targetUri); | |
} catch (Exception e) { | |
throw new ServletException("Trying to process targetUri init parameter: "+e,e); | |
} | |
targetHost = URIUtils.extractHost(targetUriObj); | |
} | |
/** Called from {@link #init(javax.servlet.ServletConfig)}. HttpClient offers many opportunities | |
* for customization. By default, | |
* <a href="http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/SystemDefaultHttpClient.html"> | |
* SystemDefaultHttpClient</a> is used if available, otherwise it falls | |
* back to: | |
* <pre>new DefaultHttpClient(new ThreadSafeClientConnManager(),hcParams)</pre> | |
* SystemDefaultHttpClient uses PoolingClientConnectionManager. In any case, it should be thread-safe. */ | |
@SuppressWarnings({"unchecked", "deprecation"}) | |
protected HttpClient createHttpClient(HttpParams hcParams) { | |
try { | |
//as of HttpComponents v4.2, this class is better since it uses System | |
// Properties: | |
Class clientClazz = Class.forName("org.apache.http.impl.client.SystemDefaultHttpClient"); | |
Constructor constructor = clientClazz.getConstructor(HttpParams.class); | |
return (HttpClient) constructor.newInstance(hcParams); | |
} catch (ClassNotFoundException e) { | |
//no problem; use v4.1 below | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
//Fallback on using older client: | |
return new DefaultHttpClient(new ThreadSafeClientConnManager(), hcParams); | |
} | |
/** The http client used. | |
* @see #createHttpClient(HttpParams) */ | |
protected HttpClient getProxyClient() { | |
return proxyClient; | |
} | |
/** Reads a servlet config parameter by the name {@code hcParamName} of type {@code type}, and | |
* set it in {@code hcParams}. | |
*/ | |
protected void readConfigParam(HttpParams hcParams, String hcParamName, Class type) { | |
String val_str = getConfigParam(hcParamName); | |
if (val_str == null) | |
return; | |
Object val_obj; | |
if (type == String.class) { | |
val_obj = val_str; | |
} else { | |
try { | |
//noinspection unchecked | |
val_obj = type.getMethod("valueOf",String.class).invoke(type,val_str); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
hcParams.setParameter(hcParamName,val_obj); | |
} | |
@Override | |
public void destroy() { | |
//As of HttpComponents v4.3, clients implement closeable | |
if (proxyClient instanceof Closeable) {//TODO AutoCloseable in Java 1.6 | |
try { | |
((Closeable) proxyClient).close(); | |
} catch (IOException e) { | |
log("While destroying servlet, shutting down HttpClient: "+e, e); | |
} | |
} else { | |
//Older releases require we do this: | |
if (proxyClient != null) | |
proxyClient.getConnectionManager().shutdown(); | |
} | |
super.destroy(); | |
} | |
@Override | |
protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) | |
throws ServletException, IOException { | |
//initialize request attributes from caches if unset by a subclass by this point | |
if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) { | |
servletRequest.setAttribute(ATTR_TARGET_URI, targetUri); | |
} | |
if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) { | |
servletRequest.setAttribute(ATTR_TARGET_HOST, targetHost); | |
} | |
// Make the Request | |
//note: we won't transfer the protocol version because I'm not sure it would truly be compatible | |
String method = servletRequest.getMethod(); | |
String proxyRequestUri = rewriteUrlFromRequest(servletRequest); | |
HttpRequest proxyRequest; | |
//spec: RFC 2616, sec 4.3: either of these two headers signal that there is a message body. | |
if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null || | |
servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null) { | |
HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, proxyRequestUri); | |
// Add the input entity (streamed) | |
// note: we don't bother ensuring we close the servletInputStream since the container handles it | |
eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), servletRequest.getContentLength())); | |
proxyRequest = eProxyRequest; | |
} else | |
proxyRequest = new BasicHttpRequest(method, proxyRequestUri); | |
copyRequestHeaders(servletRequest, proxyRequest); | |
setXForwardedForHeader(servletRequest, proxyRequest); | |
HttpResponse proxyResponse = null; | |
try { | |
// Execute the request | |
if (doLog) { | |
log("proxy " + method + " uri: " + servletRequest.getRequestURI() + " -- " + proxyRequest.getRequestLine().getUri()); | |
} | |
proxyResponse = proxyClient.execute(getTargetHost(servletRequest), proxyRequest); | |
// Process the response | |
int statusCode = proxyResponse.getStatusLine().getStatusCode(); | |
if (doResponseRedirectOrNotModifiedLogic(servletRequest, servletResponse, proxyResponse, statusCode)) { | |
//the response is already "committed" now without any body to send | |
//TODO copy response headers? | |
return; | |
} | |
// Pass the response code. This method with the "reason phrase" is deprecated but it's the only way to pass the | |
// reason along too. | |
//noinspection deprecation | |
servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase()); | |
copyResponseHeaders(proxyResponse, servletRequest, servletResponse); | |
// Send the content to the client | |
copyResponseEntity(proxyResponse, servletResponse); | |
} catch (Exception e) { | |
//abort request, according to best practice with HttpClient | |
if (proxyRequest instanceof AbortableHttpRequest) { | |
AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest; | |
abortableHttpRequest.abort(); | |
} | |
if (e instanceof RuntimeException) | |
throw (RuntimeException)e; | |
if (e instanceof ServletException) | |
throw (ServletException)e; | |
//noinspection ConstantConditions | |
if (e instanceof IOException) | |
throw (IOException) e; | |
throw new RuntimeException(e); | |
} finally { | |
// make sure the entire entity was consumed, so the connection is released | |
if (proxyResponse != null) | |
consumeQuietly(proxyResponse.getEntity()); | |
//Note: Don't need to close servlet outputStream: | |
// http://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter | |
} | |
} | |
protected boolean doResponseRedirectOrNotModifiedLogic( | |
HttpServletRequest servletRequest, HttpServletResponse servletResponse, | |
HttpResponse proxyResponse, int statusCode) | |
throws ServletException, IOException { | |
// Check if the proxy response is a redirect | |
// The following code is adapted from org.tigris.noodle.filters.CheckForRedirect | |
if (statusCode >= HttpServletResponse.SC_MULTIPLE_CHOICES /* 300 */ | |
&& statusCode < HttpServletResponse.SC_NOT_MODIFIED /* 304 */) { | |
Header locationHeader = proxyResponse.getLastHeader(HttpHeaders.LOCATION); | |
if (locationHeader == null) { | |
throw new ServletException("Received status code: " + statusCode | |
+ " but no " + HttpHeaders.LOCATION + " header was found in the response"); | |
} | |
// Modify the redirect to go to this proxy servlet rather that the proxied host | |
String locStr = rewriteUrlFromResponse(servletRequest, locationHeader.getValue()); | |
servletResponse.sendRedirect(locStr); | |
return true; | |
} | |
// 304 needs special handling. See: | |
// http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304 | |
// We get a 304 whenever passed an 'If-Modified-Since' | |
// header and the data on disk has not changed; server | |
// responds w/ a 304 saying I'm not going to send the | |
// body because the file has not changed. | |
if (statusCode == HttpServletResponse.SC_NOT_MODIFIED) { | |
servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0); | |
servletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); | |
return true; | |
} | |
return false; | |
} | |
protected void closeQuietly(Closeable closeable) { | |
try { | |
closeable.close(); | |
} catch (IOException e) { | |
log(e.getMessage(), e); | |
} | |
} | |
/** HttpClient v4.1 doesn't have the | |
* {@link org.apache.http.util.EntityUtils#consumeQuietly(org.apache.http.HttpEntity)} method. */ | |
protected void consumeQuietly(HttpEntity entity) { | |
try { | |
EntityUtils.consume(entity); | |
} catch (IOException e) {//ignore | |
log(e.getMessage(), e); | |
} | |
} | |
/** These are the "hop-by-hop" headers that should not be copied. | |
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html | |
* I use an HttpClient HeaderGroup class instead of Set<String> because this | |
* approach does case insensitive lookup faster. | |
*/ | |
protected static final HeaderGroup hopByHopHeaders; | |
static { | |
hopByHopHeaders = new HeaderGroup(); | |
String[] headers = new String[] { | |
"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", | |
"TE", "Trailers", "Transfer-Encoding", "Upgrade" }; | |
for (String header : headers) { | |
hopByHopHeaders.addHeader(new BasicHeader(header, null)); | |
} | |
} | |
/** Copy request headers from the servlet client to the proxy request. */ | |
protected void copyRequestHeaders(HttpServletRequest servletRequest, HttpRequest proxyRequest) { | |
// Get an Enumeration of all of the header names sent by the client | |
Enumeration enumerationOfHeaderNames = servletRequest.getHeaderNames(); | |
while (enumerationOfHeaderNames.hasMoreElements()) { | |
String headerName = (String) enumerationOfHeaderNames.nextElement(); | |
//Instead the content-length is effectively set via InputStreamEntity | |
if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) | |
continue; | |
if (hopByHopHeaders.containsHeader(headerName)) | |
continue; | |
Enumeration headers = servletRequest.getHeaders(headerName); | |
while (headers.hasMoreElements()) {//sometimes more than one value | |
String headerValue = (String) headers.nextElement(); | |
// In case the proxy host is running multiple virtual servers, | |
// rewrite the Host header to ensure that we get content from | |
// the correct virtual server | |
if (headerName.equalsIgnoreCase(HttpHeaders.HOST)) { | |
HttpHost host = getTargetHost(servletRequest); | |
headerValue = host.getHostName(); | |
if (host.getPort() != -1) | |
headerValue += ":"+host.getPort(); | |
} else if (headerName.equalsIgnoreCase(org.apache.http.cookie.SM.COOKIE)) { | |
headerValue = getRealCookie(headerValue); | |
} | |
proxyRequest.addHeader(headerName, headerValue); | |
} | |
} | |
} | |
private void setXForwardedForHeader(HttpServletRequest servletRequest, | |
HttpRequest proxyRequest) { | |
String headerName = "X-Forwarded-For"; | |
if (doForwardIP) { | |
String newHeader = servletRequest.getRemoteAddr(); | |
String existingHeader = servletRequest.getHeader(headerName); | |
if (existingHeader != null) { | |
newHeader = existingHeader + ", " + newHeader; | |
} | |
proxyRequest.setHeader(headerName, newHeader); | |
} | |
} | |
/** Copy proxied response headers back to the servlet client. */ | |
protected void copyResponseHeaders(HttpResponse proxyResponse, HttpServletRequest servletRequest, | |
HttpServletResponse servletResponse) { | |
for (Header header : proxyResponse.getAllHeaders()) { | |
if (hopByHopHeaders.containsHeader(header.getName())) | |
continue; | |
if (header.getName().equalsIgnoreCase(org.apache.http.cookie.SM.SET_COOKIE) || | |
header.getName().equalsIgnoreCase(org.apache.http.cookie.SM.SET_COOKIE2)) { | |
copyProxyCookie(servletRequest, servletResponse, header); | |
} else { | |
servletResponse.addHeader(header.getName(), header.getValue()); | |
} | |
} | |
} | |
/** Copy cookie from the proxy to the servlet client. | |
* Replaces cookie path to local path and renames cookie to avoid collisions. | |
*/ | |
protected void copyProxyCookie(HttpServletRequest servletRequest, | |
HttpServletResponse servletResponse, Header header) { | |
List<HttpCookie> cookies = HttpCookie.parse(header.getValue()); | |
String path = servletRequest.getContextPath(); // path starts with / or is empty string | |
path += servletRequest.getServletPath(); // servlet path starts with / or is empty string | |
for (HttpCookie cookie : cookies) { | |
//set cookie name prefixed w/ a proxy value so it won't collide w/ other cookies | |
String proxyCookieName = getCookieNamePrefix() + cookie.getName(); | |
Cookie servletCookie = new Cookie(proxyCookieName, cookie.getValue()); | |
servletCookie.setComment(cookie.getComment()); | |
servletCookie.setMaxAge((int) cookie.getMaxAge()); | |
servletCookie.setPath(path); //set to the path of the proxy servlet | |
// don't set cookie domain | |
servletCookie.setSecure(cookie.getSecure()); | |
servletCookie.setVersion(cookie.getVersion()); | |
servletResponse.addCookie(servletCookie); | |
} | |
} | |
/** Take any client cookies that were originally from the proxy and prepare them to send to the | |
* proxy. This relies on cookie headers being set correctly according to RFC 6265 Sec 5.4. | |
* This also blocks any local cookies from being sent to the proxy. | |
*/ | |
protected String getRealCookie(String cookieValue) { | |
StringBuilder escapedCookie = new StringBuilder(); | |
String cookies[] = cookieValue.split("; "); | |
for (String cookie : cookies) { | |
String cookieSplit[] = cookie.split("="); | |
if (cookieSplit.length == 2) { | |
String cookieName = cookieSplit[0]; | |
if (cookieName.startsWith(getCookieNamePrefix())) { | |
cookieName = cookieName.substring(getCookieNamePrefix().length()); | |
if (escapedCookie.length() > 0) { | |
escapedCookie.append("; "); | |
} | |
escapedCookie.append(cookieName).append("=").append(cookieSplit[1]); | |
} | |
} | |
cookieValue = escapedCookie.toString(); | |
} | |
return cookieValue; | |
} | |
/** The string prefixing rewritten cookies. */ | |
protected String getCookieNamePrefix() { | |
return "!Proxy!" + getServletConfig().getServletName(); | |
} | |
/** Copy response body data (the entity) from the proxy to the servlet client. */ | |
protected void copyResponseEntity(HttpResponse proxyResponse, HttpServletResponse servletResponse) throws IOException { | |
HttpEntity entity = proxyResponse.getEntity(); | |
if (entity != null) { | |
OutputStream servletOutputStream = servletResponse.getOutputStream(); | |
entity.writeTo(servletOutputStream); | |
} | |
} | |
/** Reads the request URI from {@code servletRequest} and rewrites it, considering targetUri. | |
* It's used to make the new request. | |
*/ | |
protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) { | |
StringBuilder uri = new StringBuilder(500); | |
uri.append(getTargetUri(servletRequest)); | |
// Handle the path given to the servlet | |
if (servletRequest.getPathInfo() != null) {//ex: /my/path.html | |
uri.append(encodeUriQuery(servletRequest.getPathInfo())); | |
} | |
// Handle the query string & fragment | |
String queryString = servletRequest.getQueryString();//ex:(following '?'): name=value&foo=bar#fragment | |
String fragment = null; | |
//split off fragment from queryString, updating queryString if found | |
if (queryString != null) { | |
int fragIdx = queryString.indexOf('#'); | |
if (fragIdx >= 0) { | |
fragment = queryString.substring(fragIdx + 1); | |
queryString = queryString.substring(0,fragIdx); | |
} | |
} | |
queryString = rewriteQueryStringFromRequest(servletRequest, queryString); | |
if (queryString != null && queryString.length() > 0) { | |
uri.append('?'); | |
uri.append(encodeUriQuery(queryString)); | |
} | |
if (doSendUrlFragment && fragment != null) { | |
uri.append('#'); | |
uri.append(encodeUriQuery(fragment)); | |
} | |
return uri.toString(); | |
} | |
protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) { | |
return queryString; | |
} | |
/** For a redirect response from the target server, this translates {@code theUrl} to redirect to | |
* and translates it to one the original client can use. */ | |
protected String rewriteUrlFromResponse(HttpServletRequest servletRequest, String theUrl) { | |
//TODO document example paths | |
final String targetUri = getTargetUri(servletRequest); | |
if (theUrl.startsWith(targetUri)) { | |
String curUrl = servletRequest.getRequestURL().toString();//no query | |
String pathInfo = servletRequest.getPathInfo(); | |
if (pathInfo != null) { | |
assert curUrl.endsWith(pathInfo); | |
curUrl = curUrl.substring(0,curUrl.length()-pathInfo.length());//take pathInfo off | |
} | |
theUrl = curUrl+theUrl.substring(targetUri.length()); | |
} | |
return theUrl; | |
} | |
/** The target URI as configured. Not null. */ | |
public String getTargetUri() { return targetUri; } | |
/** | |
* Encodes characters in the query or fragment part of the URI. | |
* | |
* <p>Unfortunately, an incoming URI sometimes has characters disallowed by the spec. HttpClient | |
* insists that the outgoing proxied request has a valid URI because it uses Java's {@link URI}. | |
* To be more forgiving, we must escape the problematic characters. See the URI class for the | |
* spec. | |
* | |
* @param in example: name=value&foo=bar#fragment | |
*/ | |
protected static CharSequence encodeUriQuery(CharSequence in) { | |
//Note that I can't simply use URI.java to encode because it will escape pre-existing escaped things. | |
StringBuilder outBuf = null; | |
Formatter formatter = null; | |
for(int i = 0; i < in.length(); i++) { | |
char c = in.charAt(i); | |
boolean escape = true; | |
if (c < 128) { | |
if (asciiQueryChars.get((int)c)) { | |
escape = false; | |
} | |
} else if (!Character.isISOControl(c) && !Character.isSpaceChar(c)) {//not-ascii | |
escape = false; | |
} | |
if (!escape) { | |
if (outBuf != null) | |
outBuf.append(c); | |
} else { | |
//escape | |
if (outBuf == null) { | |
outBuf = new StringBuilder(in.length() + 5*3); | |
outBuf.append(in,0,i); | |
formatter = new Formatter(outBuf); | |
} | |
//leading %, 0 padded, width 2, capital hex | |
formatter.format("%%%02X",(int)c);//TODO | |
} | |
} | |
return outBuf != null ? outBuf : in; | |
} | |
protected static final BitSet asciiQueryChars; | |
static { | |
char[] c_unreserved = "_-!.~'()*".toCharArray();//plus alphanum | |
char[] c_punct = ",;:$&+=".toCharArray(); | |
char[] c_reserved = "?/[]@".toCharArray();//plus punct | |
asciiQueryChars = new BitSet(128); | |
for(char c = 'a'; c <= 'z'; c++) asciiQueryChars.set((int)c); | |
for(char c = 'A'; c <= 'Z'; c++) asciiQueryChars.set((int)c); | |
for(char c = '0'; c <= '9'; c++) asciiQueryChars.set((int)c); | |
for(char c : c_unreserved) asciiQueryChars.set((int)c); | |
for(char c : c_punct) asciiQueryChars.set((int)c); | |
for(char c : c_reserved) asciiQueryChars.set((int)c); | |
asciiQueryChars.set((int)'%');//leave existing percent escapes in place | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment