Browse Source

add support for tunneling through http proxies

Alan Shreve 12 years ago
parent
commit
16615300f5

+ 28 - 21
src/ngrok/client/cli.go

@@ -15,16 +15,17 @@ var (
 )
 
 type Options struct {
-	server    string
-	httpAuth  string
-	hostname  string
-	localaddr string
-	protocol  string
-	url       string
-	subdomain string
-	webport   int
-	logto     string
-	authtoken string
+	serverAddr string
+	proxyAddr  string
+	httpAuth   string
+	hostname   string
+	localaddr  string
+	protocol   string
+	url        string
+	subdomain  string
+	webport    int
+	logto      string
+	authtoken  string
 }
 
 func fail(msg string, args ...interface{}) {
@@ -95,11 +96,16 @@ func parseArgs() *Options {
 		"",
 		"Authentication token for identifying a premium ngrok.com account")
 
-	server := flag.String(
-		"server",
+	serverAddr := flag.String(
+		"serverAddr",
 		"ngrokd.ngrok.com:443",
 		"Address of the remote ngrokd server")
 
+	proxyAddr := flag.String(
+		"proxyAddr",
+		"",
+		"The address of an http proxy to connect through (ex: proxy.example.org:3128)")
+
 	httpAuth := flag.String(
 		"httpauth",
 		"",
@@ -143,14 +149,15 @@ func parseArgs() *Options {
 	}
 
 	return &Options{
-		server:    *server,
-		httpAuth:  *httpAuth,
-		subdomain: *subdomain,
-		localaddr: parseLocalAddr(),
-		protocol:  parseProtocol(*protocol),
-		webport:   *webport,
-		logto:     *logto,
-		authtoken: *authtoken,
-		hostname:  *hostname,
+		serverAddr: *serverAddr,
+		proxyAddr:  *proxyAddr,
+		httpAuth:   *httpAuth,
+		subdomain:  *subdomain,
+		localaddr:  parseLocalAddr(),
+		protocol:   parseProtocol(*protocol),
+		webport:    *webport,
+		logto:      *logto,
+		authtoken:  *authtoken,
+		hostname:   *hostname,
 	}
 }

+ 1 - 1
src/ngrok/client/controller.go

@@ -177,7 +177,7 @@ func (ctl *Controller) Run(opts *Options) {
 		HttpAuth:  opts.httpAuth,
 	}
 
-	ctl.Go(func() { ctl.model.Run(opts.server, opts.authtoken, ctl, reqTunnel, opts.localaddr) })
+	ctl.Go(func() { ctl.model.Run(opts.serverAddr, opts.proxyAddr, opts.authtoken, ctl, reqTunnel, opts.localaddr) })
 
 	updates := ctl.updates.Reg()
 	defer ctl.updates.UnReg(updates)

+ 32 - 11
src/ngrok/client/model.go

@@ -41,6 +41,7 @@ type ClientModel struct {
 	protocols     []proto.Protocol
 	ctl           mvc.Controller
 	serverAddr    string
+	proxyAddr     string
 	authToken     string
 }
 
@@ -129,8 +130,9 @@ func (c *ClientModel) update() {
 	c.ctl.Update(c)
 }
 
-func (c *ClientModel) Run(serverAddr, authToken string, ctl mvc.Controller, reqTunnel *msg.ReqTunnel, localaddr string) {
+func (c *ClientModel) Run(serverAddr, proxyAddr, authToken string, ctl mvc.Controller, reqTunnel *msg.ReqTunnel, localaddr string) {
 	c.serverAddr = serverAddr
+	c.proxyAddr = proxyAddr
 	c.authToken = authToken
 	c.ctl = ctl
 	c.reconnectingControl(reqTunnel, localaddr)
@@ -167,11 +169,20 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 	}()
 
 	// establish control channel
-	conn, err := conn.Dial(c.serverAddr, "ctl", tlsConfig)
+	var (
+		ctlConn conn.Conn
+		err     error
+	)
+	if c.proxyAddr == "" {
+		// simple non-proxied case, just connect to the server
+		ctlConn, err = conn.Dial(c.serverAddr, "ctl", tlsConfig)
+	} else {
+		ctlConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "ctl", tlsConfig)
+	}
 	if err != nil {
 		panic(err)
 	}
-	defer conn.Close()
+	defer ctlConn.Close()
 
 	// authenticate with the server
 	auth := &msg.Auth{
@@ -183,13 +194,13 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 		User:      c.authToken,
 	}
 
-	if err = msg.WriteMsg(conn, auth); err != nil {
+	if err = msg.WriteMsg(ctlConn, auth); err != nil {
 		panic(err)
 	}
 
 	// wait for the server to authenticate us
 	var authResp msg.AuthResp
-	if err = msg.ReadMsgInto(conn, &authResp); err != nil {
+	if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil {
 		panic(err)
 	}
 
@@ -206,18 +217,18 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 	SaveAuthToken(c.authToken)
 
 	// register the tunnel
-	if err = msg.WriteMsg(conn, reqTunnel); err != nil {
+	if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
 		panic(err)
 	}
 
 	// start the heartbeat
 	lastPong := time.Now().UnixNano()
-	c.ctl.Go(func() { c.heartbeat(&lastPong, conn) })
+	c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) })
 
 	// main control loop
 	for {
 		var rawMsg msg.Message
-		if rawMsg, err = msg.ReadMsg(conn); err != nil {
+		if rawMsg, err = msg.ReadMsg(ctlConn); err != nil {
 			panic(err)
 		}
 
@@ -248,20 +259,30 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 			c.update()
 
 		default:
-			conn.Warn("Ignoring unknown control message %v ", m)
+			ctlConn.Warn("Ignoring unknown control message %v ", m)
 		}
 	}
 }
 
 // Establishes and manages a tunnel proxy connection with the server
 func (c *ClientModel) proxy() {
-	remoteConn, err := conn.Dial(c.serverAddr, "pxy", tlsConfig)
+	var (
+		remoteConn conn.Conn
+		err        error
+	)
+
+	if c.proxyAddr == "" {
+		remoteConn, err = conn.Dial(c.serverAddr, "pxy", tlsConfig)
+	} else {
+		remoteConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "pxy", tlsConfig)
+	}
+
 	if err != nil {
 		log.Error("Failed to establish proxy connection: %v", err)
 		return
 	}
-
 	defer remoteConn.Close()
+
 	err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
 	if err != nil {
 		log.Error("Failed to write RegProxy: %v", err)

+ 1 - 1
src/ngrok/client/mvc/model.go

@@ -5,7 +5,7 @@ import (
 )
 
 type Model interface {
-	Run(serverAddr, authToken string, ctl Controller, reqTunnel *msg.ReqTunnel, localaddr string)
+	Run(serverAddr, proxyAddr, authToken string, ctl Controller, reqTunnel *msg.ReqTunnel, localaddr string)
 
 	Shutdown()
 

+ 40 - 3
src/ngrok/conn/conn.go

@@ -78,15 +78,52 @@ func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
 		return
 	}
 
+	conn = wrapConn(rawConn, typ)
+	conn.Debug("New connection to: %v", rawConn.RemoteAddr())
+
 	if tlsCfg != nil {
-		rawConn = tls.Client(rawConn, tlsCfg)
+		conn.StartTLS(tlsCfg)
 	}
 
-	conn = wrapConn(rawConn, typ)
-	conn.Debug("New connection to: %v", rawConn.RemoteAddr())
 	return
 }
 
+func DialHttpProxy(proxyAddr, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
+	// dial the proxy
+	if conn, err = Dial(proxyAddr, typ, nil); err != nil {
+		return
+	}
+
+	// send an HTTP proxy CONNECT message
+	req, err := http.NewRequest("CONNECT", "http://"+addr, nil)
+	if err != nil {
+		return
+	}
+	req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ngrok)")
+	req.Write(conn)
+
+	// read the proxy's response
+	resp, err := http.ReadResponse(bufio.NewReader(conn), req)
+	if err != nil {
+		return
+	}
+	resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		err = fmt.Errorf("Non-200 response from proxy server: %s", resp.Status)
+		return
+	}
+
+	// upgrade to TLS
+	conn.StartTLS(tlsCfg)
+
+	return
+}
+
+func (c *loggedConn) StartTLS(tlsCfg *tls.Config) {
+	c.Conn = tls.Client(c.Conn, tlsCfg)
+}
+
 func (c *loggedConn) Close() error {
 	c.Debug("Closing")
 	return c.Conn.Close()