2012-06-18 19 views
7

Estoy intentando escribir un túnel HTTP porque queremos poder conectarnos a una máquina remota a través de nuestra aplicación web. Si bien soy consciente de los riesgos de seguridad involucrados, es algo que nos gustaría hacer. No está alojado en Internet, sino en redes privadas, por lo que el riesgo se considera bajo.HTTP Tunnel Servlet (Java)

El requisito básico es permitir que la herramienta Java Debugging se conecte mediante un servlet a una máquina. Tenemos algunos clientes que insisten en tener cajas de desarrollo a su lado del firewall y como el puerto de retorno en el servidor de depuración java no es fijo, no podemos simplemente pedirles que abran un puerto específico.

El código no es perfecto todavía. Simplemente he estado tratando de comunicar algo de forma bidireccional.

Hay algunos componentes. Un servidor independiente al que se conecta la depuración java en Eclipse. Este servidor está configurado para saber hacia dónde se dirige en función del puerto conectado. Entonces, si se golpea el puerto 1166, sabe conectarse a un servlet en la máquina x.

es decir, Eclipse Depurador -> Depuración del servidor proxy -> Servlet Aplicación -> Aplicación JVM

Hasta ahora por mis esfuerzos, me parece ser capaz de conectar, pero las corrientes no son totalmente funcionales. Eclipse envía un JDWP-Handshake a la JVM, que se supone que responde con JDWP-Handshake. Me doy cuenta de que cuando se envía JDWP-Handshake por Eclipse, se escribe en el Debug Proxy Server y luego se transmite al Servlet, pero parece que esto se ignora en el servlet. Los registros que estoy recibiendo son los siguientes:

[INFO] Started Jetty Server 
2012-06-18 10:00:53,356 INFO ProxySocket - Connection received, forwarding to tidevwls03:1166 via http://localhost:8080/tunnel/debug-proxy 
2012-06-18 10:00:53,361 INFO ProxySocket - Connected to http://localhost:8080/tunnel/debug-proxy 
2012-06-18 10:00:53,603 INFO ProxyServlet - Received incoming http connection, attempting to forward to endpoint tidevwls03:1166 
2012-06-18 10:00:53,604 INFO ProxyServlet - Connecting to endpoint tidevwls03:1166 
2012-06-18 10:00:53,613 INFO StreamProxy - [endpoint-read -> http-write ] beginning proxy transport. 
2012-06-18 10:00:53,613 INFO StreamProxy - [http-read  -> endpoint-write] beginning proxy transport. 
2012-06-18 10:00:53,619 INFO ProxySocket - Response Header: HTTP/1.1 200 OK 
2012-06-18 10:00:53,619 INFO ProxySocket - Response Header: Content-Length: 0 
2012-06-18 10:00:53,623 INFO ProxySocket - Response Header: Server: Jetty(6.1.22) 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] beginning proxy transport. 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'J' 
2012-06-18 10:00:53,624 INFO StreamProxy - [servlet-read -> client-write ] beginning proxy transport. 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'D' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'W' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'P' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] '-' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'H' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'a' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'n' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'd' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 's' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'h' 
2012-06-18 10:00:53,624 INFO StreamProxy - [client-read -> servlet-write ] 'a' 
2012-06-18 10:00:53,625 INFO StreamProxy - [client-read -> servlet-write ] 'k' 
2012-06-18 10:00:53,625 INFO StreamProxy - [client-read -> servlet-write ] 'e' 

Me pregunto si tengo que cambiar mi forma de pensar sobre esto para que las corrientes se divide en múltiples peticiones y se utiliza una conexión basada sesión. Una solicitud se convertiría en una cadena descendente sin fin (es decir, una respuesta infinita), y luego, cuando el cliente envíe al servlet, crearía una nueva solicitud cada vez. ¿Es esta la clave para que esto funcione?

A continuación se muestra el código para el servidor proxy de depuración que puede ejecutarse independientemente o que he configurado temporalmente para ejecutarlo como un servlet en un servidor Jetty para realizar pruebas rápidas. (ProxySocket.java)

import java.io.BufferedReader; 
import java.io.IOException; 
import java.io.InputStreamReader; 
import java.io.OutputStream; 
import java.net.ServerSocket; 
import java.net.Socket; 
import java.net.URL; 
import java.util.List; 

import javax.net.SocketFactory; 
import javax.servlet.ServletException; 
import javax.servlet.http.HttpServlet; 

import org.apache.commons.io.IOUtils; 
import org.apache.commons.lang.StringUtils; 
import org.apache.log4j.Logger; 
import org.springframework.context.ApplicationContext; 
import org.springframework.context.support.ClassPathXmlApplicationContext; 

public class ProxySocket extends HttpServlet { 
    private static final Logger logger = Logger.getLogger(ProxySocket.class); 
    private static final ApplicationContext springContext = new ClassPathXmlApplicationContext("env-spring/applicationContext*.xml"); 

    @Override 
    public void init() throws ServletException { 
    List<HttpDebugConfig> configs = (List<HttpDebugConfig>) springContext.getBean("DebugProxyHosts"); 
    for (HttpDebugConfig config : configs) { 
     ProxyServer proxyServer = new ProxyServer(config); 
     proxyServer.start(); 
    } 
    } 

    class ProxyServer extends Thread { 
    private HttpDebugConfig config; 

    public ProxyServer(HttpDebugConfig config) { 
     this.config = config; 
    } 

    public void run() { 
     ServerSocket ss = null; 
     StreamProxy streamToTunnel = null; 
     StreamProxy streamToClient = null; 

     try { 
     ss = new ServerSocket(config.getLocalPort()); 
     Socket inbound = null; 
     Socket outbound = null; 
     logger.info(String.format("Listening for connections on port %d. Proxying to %s:%d", config.getLocalPort(), config.getRemoteHost(), config.getRemotePort())); 
     while ((inbound = ss.accept()) != null) { 
      try { 
      logger.info(String.format("Connection received, forwarding to %s:%d via %s", config.getRemoteHost(), config.getRemotePort(), config.getProxyUrl())); 

      URL proxy = new URL(config.getProxyUrl()); 

      outbound = SocketFactory.getDefault().createSocket(proxy.getHost(), proxy.getPort()); 
      logger.info(String.format("Connected to %s", config.getProxyUrl())); 

      OutputStream out = outbound.getOutputStream(); 
      BufferedReader in = new BufferedReader(new InputStreamReader(outbound.getInputStream())); 

      writeLine(out, String.format("POST %s HTTP/1.1", config.getProxyUrl())); 
      writeLine(out, String.format("Host: http://%s:%s", proxy.getHost(), proxy.getPort())); 
      writeLine(out, "Connection: keep-alive"); 
      writeLine(out, String.format("tunnel_host: %s", config.getRemoteHost())); 
      writeLine(out, String.format("tunnel_port: %s", String.valueOf(config.getRemotePort()))); 
      writeLine(out, ""); 

      // read the http response and then we can start tunnelling. 
      for (String line = ""; StringUtils.isNotBlank(line = in.readLine());) { 
       logger.info(String.format("Response Header: %s", line)); 
      } 

      streamToTunnel = new StreamProxy("[client-read -> servlet-write ]", inbound.getInputStream(), outbound.getOutputStream()); 
      streamToClient = new StreamProxy("[servlet-read -> client-write ]", outbound.getInputStream(), inbound.getOutputStream()); 
      streamToTunnel.start(); 
      streamToClient.start(); 

      while (streamToClient.isAlive() || streamToTunnel.isAlive()) { 
       try { Thread.sleep(100); } catch (InterruptedException e) { } 
      } 

      logger.info(String.format("Shutting down socket-to-%s.", config.getProxyUrl())); 
      } finally { 
      IOUtils.closeQuietly(inbound); 
      IOUtils.closeQuietly(outbound); 
      } 
     } 
     } catch (IOException e) { 
     logger.error(String.format("No longer listening for connections on port %d. Proxying to %s:%d", config.getLocalPort(), config.getRemoteHost(), config.getRemotePort()), e); 
     } finally { 
     if (ss != null) { 
      try { ss.close(); } catch (Exception e) { } 
     } 
     } 
    } 

    private void writeLine(OutputStream out, String msg) throws IOException { 
     out.write(String.format("%s\n", StringUtils.defaultString(msg)).getBytes()); 
    } 
    } 
} 

La siguiente sección de código es la configuración de resorte (/env-spring/applicationContext.xml).

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:util="http://www.springframework.org/schema/util" 
    xsi:schemaLocation=" 
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
     http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd 
    "> 
    <util:list id="DebugProxyHosts" list-class="java.util.ArrayList"> 
     <bean class="HttpDebugConfig"> 
      <property name="localPort" value="1166" /> 
      <property name="proxyUrl" value="http://localhost:8080/tunnel/debug-proxy" /> 
      <property name="remoteHost" value="tidevwls03" /> 
      <property name="remotePort" value="1166" /> 
     </bean> 
    </util:list> 
</beans> 

La configuración bean (HttpDebugConfig.java).

public class HttpDebugConfig { 
    private int localPort; 
    private String remoteHost; 
    private int remotePort; 
    private String proxyUrl; 

    public int getLocalPort() { 
    return localPort; 
    } 

    public void setLocalPort(int localPort) { 
    this.localPort = localPort; 
    } 

    public String getRemoteHost() { 
    return remoteHost; 
    } 

    public void setRemoteHost(String remoteHost) { 
    this.remoteHost = remoteHost; 
    } 

    public int getRemotePort() { 
    return remotePort; 
    } 

    public void setRemotePort(int remotePort) { 
    this.remotePort = remotePort; 
    } 

    public String getProxyUrl() { 
    return proxyUrl; 
    } 

    public void setProxyUrl(String proxyUrl) { 
    this.proxyUrl = proxyUrl; 
    } 
} 

el flujo de entrada a la copiadora flujo de salida (StreamProxy.java)

import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream; 

import org.apache.commons.io.IOUtils; 
import org.apache.log4j.Logger; 

public class StreamProxy extends Thread { 
    private static final Logger logger = Logger.getLogger(StreamProxy.class); 

    private InputStream in; 
    private OutputStream out; 

    private boolean kill = false; 

    public StreamProxy(String name, InputStream in, OutputStream out) { 
    this.in = in; 
    this.out = out; 
    setName(name); 
    } 

    @Override 
    public void interrupt() { 
    this.kill = true; 
    super.interrupt(); 
    } 

    @Override 
    public void run() { 
    try { 
     logger.info(String.format("%s beginning proxy transport.", getName())); 
     do { 
     int n = 0; 
     while (-1 != (n = in.read())) { 
      logger.info(getName() + " '" + (char) n + "'"); 
      out.write(n); 
      // out.flush(); 
     } 
     try { Thread.sleep(1); } catch (Exception e) { } 
     } while (! kill); 
     logger.info(String.format("%s completed proxy transport.", getName())); 
    } catch (IOException e) { 
     logger.error(String.format("%s Failed to copy from input stream to output stream. Aborting thread.", getName()), e); 
     kill = true; 
    } finally { 
     IOUtils.closeQuietly(in); 
     IOUtils.closeQuietly(out); 
    } 
    } 
} 

Esta sección es el túnel Servlet (ProxyServlet.java)

import java.io.IOException; 
import java.net.Socket; 

import javax.net.SocketFactory; 
import javax.servlet.ServletException; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

import org.apache.commons.io.IOUtils; 
import org.apache.commons.lang.StringUtils; 
import org.apache.commons.lang.math.NumberUtils; 
import org.apache.log4j.Logger; 

public class ProxyServlet extends HttpServlet { 
    private static final Logger logger = Logger.getLogger(ProxyServlet.class); 
    private static final long serialVersionUID = -686421490573011755L; 

    @Override 
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 
    new Runner(request, response).start(); 
    } 

    class Runner extends Thread { 
    private HttpServletRequest request; 
    private HttpServletResponse response; 

    public Runner(HttpServletRequest request, HttpServletResponse response) { 
     this.request = request; 
     this.response = response; 
    } 

    @Override 
    public void run() { 
     Socket endpoint = null; 
     StreamProxy streamToHttp = null; 
     StreamProxy streamToEndpoint = null; 

     String host = StringUtils.defaultIfEmpty(request.getHeader("tunnel_host"), "localhost"); 
     int port = NumberUtils.toInt(request.getHeader("tunnel_port"), 8000); 

     try { 
     logger.info(String.format("Received incoming http connection, attempting to forward to endpoint %s:%d", host, port)); 

     logger.info(String.format("Connecting to endpoint %s:%d", host, port)); 
     endpoint = SocketFactory.getDefault().createSocket(host, port); 

     streamToHttp = new StreamProxy("[endpoint-read -> http-write ]", endpoint.getInputStream(), response.getOutputStream()); 
     streamToEndpoint = new StreamProxy("[http-read  -> endpoint-write]", request.getInputStream(), endpoint.getOutputStream()); 
     streamToHttp.start(); 
     streamToEndpoint.start(); 

     while (streamToEndpoint.isAlive() || streamToHttp.isAlive()) { 
      try { Thread.sleep(100); } catch (InterruptedException e) { } 
     } 

     logger.info(String.format("Safely shut down servlet-to-%s:%d proxy.", host, port)); 
     } catch (IOException e) { 
     logger.error(String.format("Shutting down servlet-to-%s:%d proxy.", host, port), e); 
     } finally { 
     if (streamToHttp != null) { 
      streamToHttp.interrupt(); 
     } 
     if (streamToEndpoint != null) { 
      streamToEndpoint.interrupt(); 
     } 
     IOUtils.closeQuietly(endpoint); 
     } 
    } 
    } 
} 

La configuración contenedor de aplicaciones (web .xml)

<?xml version="1.0" encoding="ISO-8859-1"?> 
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" 
    version="2.4"> 

    <display-name>tunnel</display-name> 

    <context-param> 
     <param-name>log4jConfigLocation</param-name> 
     <param-value>classpath:log4j.properties 
     </param-value> 
    </context-param> 

    <context-param> 
     <param-name>contextConfigLocation</param-name> 
     <param-value>classpath:env-spring/applicationContext*.xml</param-value> 
    </context-param> 

    <listener> 
     <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> 
    </listener> 

    <listener> 
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 
    </listener> 

    <servlet> 
     <servlet-name>Debug Proxy</servlet-name> 
     <servlet-class>ProxyServlet</servlet-class> 
     <load-on-startup>1</load-on-startup> 
    </servlet> 

    <servlet-mapping> 
     <servlet-name>Debug Proxy</servlet-name> 
     <url-pattern>/debug-proxy</url-pattern> 
    </servlet-mapping> 

    <servlet> 
     <servlet-name>Debug Socket</servlet-name> 
     <servlet-class>ProxySocket</servlet-class> 
     <load-on-startup>1</load-on-startup> 
    </servlet> 

    <servlet-mapping> 
     <servlet-name>Debug Socket</servlet-name> 
     <url-pattern>/debug-socket</url-pattern> 
    </servlet-mapping> 

    <welcome-file-list> 
     <welcome-file>index.jsp</welcome-file> 
    </welcome-file-list> 
</web-app> 

Finalmente, mi pom.xml como estoy construyendo con maven.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 
    <groupId>tunnel</groupId> 
    <artifactId>tunnel</artifactId> 
    <version>0.0.1-SNAPSHOT</version> 
    <packaging>war</packaging> 

    <properties> 
     <version.spring>3.1.1.RELEASE</version.spring> 
    </properties> 

    <build> 
     <plugins> 
      <plugin> 
       <groupId>org.mortbay.jetty</groupId> 
       <artifactId>maven-jetty-plugin</artifactId> 
       <version>6.1.22</version> 
       <configuration> 
        <webApp>${project.build.directory}/${project.build.finalName}.${project.packaging}</webApp> 
        <stopPort>9966</stopPort> 
        <stopKey>foo</stopKey> 
       </configuration> 
      </plugin> 
     </plugins> 
    </build> 

    <dependencies> 
     <dependency> 
      <groupId>javax.servlet</groupId> 
      <artifactId>servlet-api</artifactId> 
      <version>2.5</version> 
     </dependency> 

     <!-- Utilities --> 
     <dependency> 
      <groupId>commons-lang</groupId> 
      <artifactId>commons-lang</artifactId> 
      <version>2.6</version> 
     </dependency> 
     <dependency> 
      <groupId>commons-io</groupId> 
      <artifactId>commons-io</artifactId> 
      <version>2.0.1</version> 
     </dependency> 
     <dependency> 
      <groupId>commons-collections</groupId> 
      <artifactId>commons-collections</artifactId> 
      <version>3.2.1</version> 
     </dependency> 
     <dependency> 
      <groupId>commons-logging</groupId> 
      <artifactId>commons-logging</artifactId> 
      <version>1.1.1</version> 
     </dependency> 
     <dependency> 
      <groupId>commons-httpclient</groupId> 
      <artifactId>commons-httpclient</artifactId> 
      <version>3.0.1</version> 
      <exclusions> 
       <exclusion> 
        <artifactId>junit</artifactId> 
        <groupId>junit</groupId> 
       </exclusion> 
      </exclusions> 
     </dependency> 
     <dependency> 
      <groupId>commons-codec</groupId> 
      <artifactId>commons-codec</artifactId> 
      <version>1.2</version> 
     </dependency> 

     <!-- Logging --> 
     <dependency> 
      <groupId>log4j</groupId> 
      <artifactId>log4j</artifactId> 
      <version>1.2.16</version> 
     </dependency> 

     <!-- Spring Framework --> 
     <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-core</artifactId> 
      <version>${version.spring}</version> 
     </dependency> 
    <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-context</artifactId> 
      <version>${version.spring}</version> 
     </dependency> 
    <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-web</artifactId> 
      <version>${version.spring}</version> 
     </dependency> 
    <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-webmvc</artifactId> 
      <version>${version.spring}</version> 
     </dependency> 
    </dependencies> 
</project> 

corro servidor Embarcadero con los siguientes objetivos Maven

jetty:stop clean install jetty:run-war 

Esperamos que este pequeño proyecto interesante! Espero escuchar sus ideas y comentarios.

Gracias, Stuart

+0

No puede especificar el puerto de depuración remota en Jetty? Pensé que podías hacerlo en cualquier servidor ... – Thihara

+0

Puedes, pero solo estoy probando en Jetty durante el desarrollo de este servlet de túnel. La implementación real irá a un servidor WebLogic detrás de un firewall. Esto significa que no puedo conectarme al puerto ya que el cortafuegos bloquea el puerto y también porque el puerto de retorno de la JVM es aleatorio y no se puede configurar, por lo tanto, incluso si abrimos el puerto desde el cliente de depuración, no podemos decir qué puerto JVM lo usará cuando regrese al cliente. – mrswadge

+0

+1 por el problema interesante, no sabía sobre el puerto aleatorio de JVM ... – Thihara

Respuesta

0

SSH túnel - mi elección http://en.wikipedia.org/wiki/Tunneling_protocol

ssh -L [bind_address:]port:sshserverhostname:targetmachinehostname port 

-L Especifica que ese puerto, en el host local (cliente) debe ser remitido a la acogida dada y el puerto en el lado remoto. Esto funciona asignando un socket para escuchar el puerto en el lado local, opcionalmente vinculado a la bind_address especificada. Cada vez que se realiza una conexión a este puerto , la conexión se reenvía a través del canal seguro y se establece una conexión para alojar el hostport puerto desde la máquina remota. Los reenvíos de puertos también se pueden especificar en el archivo de configuración . Las direcciones IPv6 se pueden especificar al encerrar la dirección entre corchetes. Solo el superusuario puede reenviar puertos con privilegios. De forma predeterminada, el puerto local está vinculado de acuerdo con dance con la configuración de GatewayPorts. Sin embargo, una bind_address explícita se puede usar para vincular la conexión a una dirección específica. La bind_address de `localhost'' indicates that the listen- ing port be bound for local use only, while an empty address or * 'indica que el puerto debe estar disponible desde todas las interfaces.

Cuestiones relacionadas