/* * * Copyright (C) 2007-2015 Licensed to the Comunes Association (CA) under * one or more contributor license agreements (see COPYRIGHT for details). * The CA licenses this file to you under the GNU Affero General Public * License version 3, (the "License"); you may not use this file except in * compliance with the License. This file is part of kune. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package cc.kune.chat.server; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.Enumeration; import java.util.StringTokenizer; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 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 { private static final long serialVersionUID = 1L; /** * "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 */ @Override public void init(final ServletConfig config) throws ServletException { super.init(config); remotePath = getInitParameter("remotePath"); remoteServer = getInitParameter("remoteServer"); final 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 (final 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. @Override 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 @Override public void service(final HttpServletRequest req, final 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 (final IOException e) { res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Socket opening: " + remoteServer + " " + remotePort); return; } try { // // Build up a HTTP request from pure strings: final StringBuffer sb = new StringBuffer(200); sb.append(req.getMethod()); sb.append(' '); final 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()); final Enumeration<?> en = req.getHeaderNames(); while (en.hasMoreElements()) { final 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 final 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 (final 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 (final IOException ignore) { log("Exception " + ignore); } } public static void appendCleaned(final StringBuffer sb, final String str) { for (int i = 0; i < str.length(); i++) { final char ch = str.charAt(i); if (ch == ' ') { sb.append("%20"); } else { sb.append(ch); } } } /** * Forward and filter header from backend Request. */ private boolean treatHeader(final InputStream in, final HttpServletRequest req, final HttpServletResponse res) throws ServletException { boolean retval = true; final 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."); { final 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); final int colonPos = line.indexOf(":"); if (firstline && colonPos == -1) { // Special first line considerations ... final String headl[] = wordStr(line); log("head: " + line + " " + headl.length); try { res.setStatus(Integer.parseInt(headl[1])); } catch (final NumberFormatException ignore) { log("ID exception: " + headl); } catch (final Exception panik) { log("First line invalid!"); return true; } } else if (colonPos != -1) { final String head = line.substring(0, colonPos); // XXX Skip LWS (what is LWS) int i = colonPos + 1; while (isLWS(line.charAt(i))) { i++; } final 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 { final int cLen = Integer.parseInt(value); retval = cLen > 0; res.setContentLength(cLen); } catch (final 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 (final 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(final InputStream in, final byte[] b) throws IOException { int off2 = 0; while (off2 < b.length) { final 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(final InputStream in, final OutputStream out) throws IOException { final BufferedInputStream bin = new BufferedInputStream(in); int b; while ((b = bin.read()) != -1) { out.write(b); } } /** * Split a blank separated string into */ public String[] wordStr(final String inp) { final StringTokenizer tok = new StringTokenizer(inp, " "); int i; final int n = tok.countTokens(); final 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(final char c) { return c == ' '; } /** * Capture awaay the standard servlet log .. */ @Override public void log(final String msg) { if (debugFlag) { System.err.println("## " + msg); } } }