Browse Source

major refactoring and cleanup to support multiple tunnels

Alan Shreve 12 years ago
parent
commit
2fff31ff61

+ 6 - 1
assets/client/page.html

@@ -48,7 +48,12 @@
                 <div class="span6 offset3">
                     <div class="well" style="padding: 20px 50px;">
                         <h4>No requests to display yet</h4>
-                        <p class="lead">Make a request to <a target="_blank" href="{{ publicUrl }}">{{ publicUrl }}</a> to get started!</p>
+			<hr />
+                        <h5>To get started, make a request to one of your tunnel URLs:</h5>
+                            <ul>
+                                <li ng-repeat="t in tunnels"><p class="lead"><a target="_blank" href="{{ t.PublicUrl }}">{{ t.PublicUrl }}</a></p></li>
+                            </ul>
+                        </p>
                     </div>
                 </div>
             </div>

+ 1 - 1
assets/client/static/js/ngrok.js

@@ -303,7 +303,7 @@ ngrok.directive({
 
 ngrok.controller({
     "HttpTxns": function($scope, txnSvc) {
-        $scope.publicUrl = window.data.UiState.Url;
+        $scope.tunnels = window.data.UiState.Tunnels;
         $scope.txns = txnSvc.all();
 
         if (!!window.WebSocket) {

+ 207 - 0
src/ngrok/client/controller.go

@@ -0,0 +1,207 @@
+package client
+
+import (
+	"fmt"
+	"ngrok/client/mvc"
+	"ngrok/client/views/term"
+	"ngrok/client/views/web"
+	"ngrok/log"
+	"ngrok/msg"
+	"ngrok/proto"
+	"ngrok/util"
+	"sync"
+)
+
+type command interface{}
+
+type cmdQuit struct {
+	// display this message after quit
+	message string
+}
+
+type cmdPlayRequest struct {
+	// the tunnel to play this request over
+	tunnel mvc.Tunnel
+
+	// the bytes of the request to issue
+	payload []byte
+}
+
+// The MVC Controller
+type Controller struct {
+	// Controller logger
+	log.Logger
+
+	// the model sends updates through this broadcast channel
+	updates *util.Broadcast
+
+	// the model
+	model mvc.Model
+
+	// the views
+	views []mvc.View
+
+	// interal structure to issue commands to the controller
+	cmds chan command
+
+	// internal structure to synchronize access to State object
+	state chan mvc.State
+
+	// options
+	opts *Options
+}
+
+// public interface
+func NewController() *Controller {
+	ctl := &Controller{
+		Logger:  log.NewPrefixLogger("controller"),
+		updates: util.NewBroadcast(),
+		cmds:    make(chan command),
+		views:   make([]mvc.View, 0),
+		state:   make(chan mvc.State),
+	}
+
+	return ctl
+}
+
+func (ctl *Controller) State() mvc.State {
+	return <-ctl.state
+}
+
+func (ctl *Controller) Update(state mvc.State) {
+	ctl.updates.In() <- state
+}
+
+func (ctl *Controller) Updates() *util.Broadcast {
+	return ctl.updates
+}
+
+func (ctl *Controller) Shutdown(message string) {
+	ctl.cmds <- cmdQuit{message: message}
+}
+
+func (ctl *Controller) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
+	ctl.cmds <- cmdPlayRequest{tunnel: tunnel, payload: payload}
+}
+
+func (ctl *Controller) Go(fn func()) {
+	defer func() {
+		if r := recover(); r != nil {
+			ctl.Error("goroutine crashed: %v", r)
+			// XXX
+		}
+	}()
+
+	go fn()
+}
+
+// private functions
+func (ctl *Controller) doShutdown() {
+	ctl.Info("Shutting down")
+
+	var wg sync.WaitGroup
+
+	// wait for all of the views, plus the model
+	wg.Add(len(ctl.views) + 1)
+
+	for _, v := range ctl.views {
+		vClosure := v
+		ctl.Go(func() {
+			vClosure.Shutdown()
+			wg.Done()
+		})
+	}
+
+	ctl.Go(func() {
+		ctl.model.Shutdown()
+		wg.Done()
+	})
+
+	wg.Wait()
+}
+
+func (ctl *Controller) addView(v mvc.View) {
+	ctl.views = append(ctl.views, v)
+}
+
+func (ctl *Controller) GetWebViewPort() int {
+	return ctl.opts.webport
+}
+
+func (ctl *Controller) Run(opts *Options) {
+	// Save the options
+	ctl.opts = opts
+
+	// init the model
+	model := newClientModel(ctl)
+	ctl.model = model
+	var state mvc.State = model
+
+	// init web ui
+	var webView *web.WebView
+	if opts.webport != -1 {
+		webView = web.NewWebView(ctl, opts.webport)
+		ctl.addView(webView)
+	}
+
+	// init term ui
+	var termView *term.TermView
+	if opts.logto != "stdout" {
+		termView = term.NewTermView(ctl)
+		ctl.addView(termView)
+	}
+
+	for _, protocol := range model.GetProtocols() {
+		switch p := protocol.(type) {
+		case *proto.Http:
+			if termView != nil {
+				ctl.addView(termView.NewHttpView(p))
+			}
+
+			if webView != nil {
+				ctl.addView(webView.NewHttpView(p))
+			}
+		default:
+		}
+	}
+
+	ctl.Go(func() { autoUpdate(ctl, opts.authtoken) })
+
+	reg := &msg.RegMsg{
+		Protocol:  opts.protocol,
+		Hostname:  opts.hostname,
+		Subdomain: opts.subdomain,
+		HttpAuth:  opts.httpAuth,
+		User:      opts.authtoken,
+	}
+
+	ctl.Go(func() { ctl.model.Run(opts.server, opts.authtoken, ctl, reg, opts.localaddr) })
+
+	quitMessage := ""
+	defer func() {
+		ctl.doShutdown()
+		fmt.Println(quitMessage)
+	}()
+
+	updates := ctl.updates.Reg()
+	defer ctl.updates.UnReg(updates)
+
+	for {
+		select {
+		case obj := <-ctl.cmds:
+			switch cmd := obj.(type) {
+			case cmdQuit:
+				quitMessage = cmd.message
+				return
+
+			case cmdPlayRequest:
+				ctl.Go(func() { ctl.model.PlayRequest(cmd.tunnel, cmd.payload) })
+			}
+
+		case obj := <-updates:
+			state = obj.(mvc.State)
+
+		case ctl.state <- state:
+		}
+	}
+}

+ 0 - 150
src/ngrok/client/controller/controller.go

@@ -1,150 +0,0 @@
-package controller
-
-import (
-	"ngrok/client/mvc"
-	"ngrok/util"
-	"sync"
-)
-
-type command interface{}
-
-type cmdQuit struct {
-	// display this message after quit
-	message string
-}
-
-type cmdPlayRequest struct {
-	// the tunnel to play this request over
-	tunnel mvc.Tunnel
-
-	// the bytes of the request to issue
-	payload []byte
-}
-
-// The MVC Controller
-type Controller struct {
-	// the model sends updates through this broadcast channel
-	updates *util.Broadcast
-
-	// the model
-	model mvc.Model
-
-	// the views
-	view []mvc.View
-
-	// interal structure to issue commands to the controller
-	cmds chan Command
-}
-
-// public interface
-func NewController(model mvc.Model) *Controller {
-	ctl := &Controller{
-		updates:  util.NewBroadcast(),
-		model:	  model,
-		cmds:     make(chan command),
-		view:     make([]View),
-	}
-
-	return ctl
-}
-
-func (ctl *Controller) Update(state State) {
-	ctl.Updates.In() <- state
-}
-
-func (ctl *Controller) Shutdown(message string) {
-	ctl.cmds <- cmdQuit{message: message}
-}
-
-func (ctl *Controller) PlayRequest(tunnel *mvc.Tunnel, payload []byte) {
-	ctl.cmd <- cmdPlayRequest{tunnel: tunnel, payload: payload}
-}
-
-
-// private functions
-func (ctl *Controller) doShutdown() {
-	var wg sync.WaitGroup
-
-	// wait for all of the views, plus the model
-	wg.Add(len(ctl.views) + 1)
-
-	for v := range ctl.Views {
-		go v.Shutdown(&wg)
-	}
-	go model.Shutdown(&wg)
-
-	wg.Wait()
-}
-
-func (ctl *Controller) Go(fn func()) {
-	defer func() {
-		if r := recover(); r != nil {
-			// XXX
-		}
-	}()
-
-	go fn()
-}
-
-func (ctl *Controller) Run() {
-	// parse options
-	opts := parseArgs()
-
-	// set up logging
-	log.LogTo(opts.logto)
-
-	// seed random number generator
-	seed, err := util.RandomSeed()
-	if err != nil {
-		log.Error("Couldn't securely seed the random number generator!")
-	}
-	rand.Seed(seed)
-
-	// set up auth token
-	if opts.authtoken == "" {
-		opts.authtoken = LoadAuthToken()
-	}
-
-	// init web ui
-	if opts.webport != -1 {
-		ctl.views = append(ctl.views, web.NewWebView(ctl, ctl.model, opts.webport))
-	}
-
-	// init term ui
-	if opts.logto != "stdout" {
-		ctl.views = append(ctl.views, term.New(ctl, ctl.model))
-	}
-
-	ctl.Go(func() { autoUpdate(s, ctl, opts.authtoken) })
-
-	reg := &msg.RegMsg{
-		Protocol: opts.protocol,
-		Hostname: opts.hostname,
-		Subdomain: opts.subdomain,
-		HttpAuth: opts.httpAuth,
-		User: opts.user,
-		Password: opts.password,
-	}
-
-	ctl.Go(func() { ctl.model.Run(opts.serverAddr, opts.authtoken, ctl, tunnel) })
-
-	quitMessage = ""
-	defer func() {
-		ctl.doShutdown()
-		fmt.Printf(quitMessage)
-	}()
-
-	for {
-		select {
-		case obj := <-ctl.cmds:
-			switch cmd := obj.(type) {
-			case cmdQuit:
-				quitMessage = cmd.Message
-				return
-
-			case cmdPlayRequest:
-				ctl.Go(func() { model.PlayRequest(tunnel, cmd.Payload) })
-			}
-		}
-	}
-}

+ 22 - 3
src/ngrok/client/main.go

@@ -1,10 +1,29 @@
 package client
 
 import (
-	"ngrok/client/controller"
+	"math/rand"
+	"ngrok/log"
+	"ngrok/util"
 )
 
 func Main() {
-	controller := controller.NewController(newClientModel())
-	controller.Run()
+	// parse options
+	opts := parseArgs()
+
+	// set up logging
+	log.LogTo(opts.logto)
+
+	// set up auth token
+	if opts.authtoken == "" {
+		opts.authtoken = LoadAuthToken()
+	}
+
+	// seed random number generator
+	seed, err := util.RandomSeed()
+	if err != nil {
+		log.Error("Couldn't securely seed the random number generator!")
+	}
+	rand.Seed(seed)
+
+	NewController().Run(opts)
 }

+ 87 - 52
src/ngrok/client/model.go

@@ -2,12 +2,10 @@ package client
 
 import (
 	"fmt"
+	metrics "github.com/inconshreveable/go-metrics"
 	"io/ioutil"
 	"math"
-	"math/rand"
 	"ngrok/client/mvc"
-	"ngrok/client/views/term"
-	"ngrok/client/views/web"
 	"ngrok/conn"
 	"ngrok/log"
 	"ngrok/msg"
@@ -17,7 +15,6 @@ import (
 	"runtime"
 	"sync/atomic"
 	"time"
-	metrics "github.com/inconshreveable/go-metrics"
 )
 
 const (
@@ -32,26 +29,30 @@ const (
 `
 )
 
-type ClientModel {
+type ClientModel struct {
 	log.Logger
 
 	id            string
-	tunnels       []mvc.Tunnel
+	tunnels       map[string]mvc.Tunnel
 	serverVersion string
-	opts          *Options
 	metrics       *ClientMetrics
 	updateStatus  mvc.UpdateStatus
 	connStatus    mvc.ConnStatus
-	protoMap      map[string] *proto.Protocol
+	protoMap      map[string]proto.Protocol
+	protocols     []proto.Protocol
+	ctl           mvc.Controller
+	serverAddr    string
+	authToken     string
 }
 
-func newClient() {
-	protoMap := make(map[string] *proto.Protocol)
+func newClientModel(ctl mvc.Controller) *ClientModel {
+	protoMap := make(map[string]proto.Protocol)
 	protoMap["http"] = proto.NewHttp()
 	protoMap["https"] = protoMap["http"]
 	protoMap["tcp"] = proto.NewTcp()
+	protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]}
 
-	return &Client {
+	return &ClientModel{
 		Logger: log.NewPrefixLogger("client"),
 
 		// unique client id
@@ -63,22 +64,35 @@ func newClient() {
 		// update status
 		updateStatus: mvc.UpdateNone,
 
-		// command-line options
-		opts: opts,
-
 		// metrics
 		metrics: NewClientMetrics(),
 
 		// protocols
 		protoMap: protoMap,
+
+		// protocol list
+		protocols: protocols,
+
+		// open tunnels
+		tunnels: make(map[string]mvc.Tunnel),
+
+		// controller
+		ctl: ctl,
 	}
 }
 
 // mvc.State interface
-func (c ClientModel) GetClientVersion() string    { return version.MajorMinor() }
-func (c ClientModel) GetServerVersion() string    { return c.serverVersion }
-func (c ClientModel) GetTunnels() []mvc.Tunnel    { return c.tunnels }
-func (c ClientModel) GetConnStatus() mvc.ConnStatus           { return c.connStatus }
+func (c ClientModel) GetProtocols() []proto.Protocol { return c.protocols }
+func (c ClientModel) GetClientVersion() string       { return version.MajorMinor() }
+func (c ClientModel) GetServerVersion() string       { return c.serverVersion }
+func (c ClientModel) GetTunnels() []mvc.Tunnel {
+	tunnels := make([]mvc.Tunnel, 0)
+	for _, t := range c.tunnels {
+		tunnels = append(tunnels, t)
+	}
+	return tunnels
+}
+func (c ClientModel) GetConnStatus() mvc.ConnStatus     { return c.connStatus }
 func (c ClientModel) GetUpdateStatus() mvc.UpdateStatus { return c.updateStatus }
 
 func (c ClientModel) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
@@ -94,44 +108,43 @@ func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) {
 }
 
 // mvc.Model interface
-func (c *ClientModel) PlayRequest(tunnel *mvc.Tunnel, payload []byte) {
-	t := m.tunnels[tunnel.PublicUrl]
+func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) {
+	t := c.tunnels[tunnel.PublicUrl]
 
 	var localConn conn.Conn
-	localConn, err := conn.Dial(t.localaddr, "prv", nil)
+	localConn, err := conn.Dial(t.LocalAddr, "prv", nil)
 	if err != nil {
-		m.Warn("Failed to open private leg to %s: %v", t.localaddr, err)
+		c.Warn("Failed to open private leg to %s: %v", t.LocalAddr, err)
 		return
 	}
 	//defer localConn.Close()
-	localConn = t.protocol.WrapConn(localConn)
+	// XXX: send user context that indicates it's a replayed connection
+	localConn = t.Protocol.WrapConn(localConn, nil)
 	localConn.Write(payload)
 	ioutil.ReadAll(localConn)
 }
 
-func (c *ClientModel) Shutdown(wg *sync.WaitGroup) {
-	// there's no clean shutdown needed, do it immediately
-	wg.Done()
+func (c *ClientModel) Shutdown() {
 }
 
 func (c *ClientModel) update() {
-	c.ctl.Update(m)
+	c.ctl.Update(c)
 }
 
-func (c *ClientModel) Run(serverAddr, authToken string, ctl mvc.Controller, tunnel *mvc.Tunnel) {
+func (c *ClientModel) Run(serverAddr, authToken string, ctl mvc.Controller, reg *msg.RegMsg, localaddr string) {
 	c.serverAddr = serverAddr
 	c.authToken = authToken
 	c.ctl = ctl
-	c.reconnectingControl(tunnel)
+	c.reconnectingControl(reg, localaddr)
 }
 
-func (c *ClientModel) reconnectingControl(reg *msg.RegMsg) {
+func (c *ClientModel) reconnectingControl(reg *msg.RegMsg, localaddr string) {
 	// how long we should wait before we reconnect
 	maxWait := 30 * time.Second
 	wait := 1 * time.Second
 
 	for {
-		c.control(reg)
+		c.control(reg, localaddr)
 
 		if c.connStatus == mvc.ConnOnline {
 			wait = 1 * time.Second
@@ -148,7 +161,7 @@ func (c *ClientModel) reconnectingControl(reg *msg.RegMsg) {
 }
 
 // Establishes and manages a tunnel control connection with the server
-func (c *ClientModel) control(reg *msg.RegMsg) {
+func (c *ClientModel) control(reg *msg.RegMsg, localaddr string) {
 	defer func() {
 		if r := recover(); r != nil {
 			log.Error("control recovering from failure %v", r)
@@ -163,13 +176,13 @@ func (c *ClientModel) control(reg *msg.RegMsg) {
 	defer conn.Close()
 
 	// register with the server
-	reg["OS"] = runtime.GOOS
-	reg["ClientId"] = c.id
-	reg["Version"] = version.Proto
-	reg["MmVersion"] = version.MajorMinor()
-	reg["User"] = c.authtoken
+	reg.OS = runtime.GOOS
+	reg.ClientId = c.id
+	reg.Version = version.Proto
+	reg.MmVersion = version.MajorMinor()
+	reg.User = c.authToken
 
-	if err != nil {
+	if err = msg.WriteMsg(conn, reg); err != nil {
 		panic(err)
 	}
 
@@ -185,22 +198,22 @@ func (c *ClientModel) control(reg *msg.RegMsg) {
 		return
 	}
 
-	tunnel := &mvc.Tunnel {
+	tunnel := mvc.Tunnel{
 		PublicUrl: regAck.Url,
 		LocalAddr: localaddr,
-		Protocol: c.protoMap[reg.Protocol],
+		Protocol:  c.protoMap[reg.Protocol],
 	}
 
-	c.tunnels[tunnel.Url] = tunnel
+	c.tunnels[tunnel.PublicUrl] = tunnel
 
 	// update UI state
 	c.id = regAck.ClientId
-	conn.Info("Tunnel established at %v", tunnel.Url)
-	c.status = mvc.ConnOnline
+	c.Info("Tunnel established at %v", tunnel.PublicUrl)
+	c.connStatus = mvc.ConnOnline
 	c.serverVersion = regAck.MmVersion
 	c.update()
 
-	SaveAuthToken(c.authtoken)
+	SaveAuthToken(c.authToken)
 
 	// start the heartbeat
 	lastPong := time.Now().UnixNano()
@@ -219,6 +232,23 @@ func (c *ClientModel) control(reg *msg.RegMsg) {
 
 		case *msg.PongMsg:
 			atomic.StoreInt64(&lastPong, time.Now().UnixNano())
+
+		case *msg.RegAckMsg:
+			if m.Error != "" {
+				c.Error("Server failed to allocate tunnel: %s", regAck.Error)
+				continue
+			}
+
+			tunnel := mvc.Tunnel{
+				PublicUrl: m.Url,
+				LocalAddr: localaddr,
+				Protocol:  c.protoMap[m.Protocol],
+			}
+
+			c.tunnels[tunnel.PublicUrl] = tunnel
+			c.Info("Tunnel established at %v", tunnel.PublicUrl)
+			c.update()
+
 		default:
 			conn.Warn("Ignoring unknown control message %v ", m)
 		}
@@ -234,7 +264,7 @@ func (c *ClientModel) proxy() {
 	}
 
 	defer remoteConn.Close()
-	err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{ClientId: s.id})
+	err = msg.WriteMsg(remoteConn, &msg.RegProxyMsg{ClientId: c.id})
 	if err != nil {
 		log.Error("Failed to write RegProxyMsg: %v", err)
 		return
@@ -247,23 +277,27 @@ func (c *ClientModel) proxy() {
 		return
 	}
 
-	tunnel := tunnels[startPxyMsg.Url]
-	if tunnel == nil {
+	tunnel, ok := c.tunnels[startPxyMsg.Url]
+	if !ok {
 		c.Error("Couldn't find tunnel for proxy: %s", startPxyMsg.Url)
 		return
 	}
 
 	// start up the private connection
 	start := time.Now()
-	localConn, err := conn.Dial(tunnel.localaddr, "prv", nil)
+	localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
 	if err != nil {
-		remoteConn.Warn("Failed to open private leg %s: %v", tunnel.localaddr, err)
-		badGatewayBody := fmt.Sprintf(BadGateway, tunnel.publicUrl, tunnel.localaddr, tunnel.localaddr)
-		remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
+		remoteConn.Warn("Failed to open private leg %s: %v", tunnel.LocalAddr, err)
+
+		if tunnel.Protocol.GetName() == "http" {
+			// try to be helpful when you're in HTTP mode and a human might see the output
+			badGatewayBody := fmt.Sprintf(BadGateway, tunnel.PublicUrl, tunnel.LocalAddr, tunnel.LocalAddr)
+			remoteConn.Write([]byte(fmt.Sprintf(`HTTP/1.0 502 Bad Gateway
 Content-Type: text/html
 Content-Length: %d
 
 %s`, len(badGatewayBody), badGatewayBody)))
+		}
 		return
 	}
 	defer localConn.Close()
@@ -273,7 +307,8 @@ Content-Length: %d
 	m.connMeter.Mark(1)
 	c.update()
 	m.connTimer.Time(func() {
-		localConn := tunnel.protocol.WrapConn(localConn)
+		// XXX: wrap with connection context
+		localConn := tunnel.Protocol.WrapConn(localConn, nil)
 		bytesIn, bytesOut := conn.Join(localConn, remoteConn)
 		m.bytesIn.Update(bytesIn)
 		m.bytesOut.Update(bytesOut)

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

@@ -1,5 +1,9 @@
 package mvc
 
+import (
+	"ngrok/util"
+)
+
 type Controller interface {
 	// how the model communicates that it has changed state
 	Update(State)
@@ -8,5 +12,17 @@ type Controller interface {
 	Shutdown(message string)
 
 	// PlayRequest instructs the model to play requests
-	PlayRequest(tunnel *Tunnel, payload []byte)
+	PlayRequest(tunnel Tunnel, payload []byte)
+
+	// A channel of updates
+	Updates() *util.Broadcast
+
+	// returns the current state
+	State() State
+
+	// safe wrapper for running go-routines
+	Go(fn func())
+
+	// the port where the web interface is running
+	GetWebViewPort() int
 }

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

@@ -1,13 +1,13 @@
 package mvc
 
 import (
-	"sync"
+	"ngrok/msg"
 )
 
 type Model interface {
-	Run(opts *Options, ctl Controller)
+	Run(serverAddr, authToken string, ctl Controller, reg *msg.RegMsg, localaddr string)
 
-	Shutdown(wg *sync.WaitGroup)
+	Shutdown()
 
-	PlayRequest(tunnel *Tunnel, payload []byte)
+	PlayRequest(tunnel Tunnel, payload []byte)
 }

+ 4 - 4
src/ngrok/client/mvc/state.go

@@ -24,17 +24,17 @@ const (
 
 type Tunnel struct {
 	PublicUrl string
-	Protocol proto.Protocol
+	Protocol  proto.Protocol
 	LocalAddr string
 }
 
 type State interface {
 	GetClientVersion() string
 	GetServerVersion() string
-	GetUpdate() UpdateStatus
 	GetTunnels() []Tunnel
-	GetStatus() string
-	GetWebPort() int
+	GetProtocols() []proto.Protocol
+	GetUpdateStatus() UpdateStatus
+	GetConnStatus() ConnStatus
 	GetConnectionMetrics() (metrics.Meter, metrics.Timer)
 	GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
 	GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)

+ 1 - 5
src/ngrok/client/mvc/view.go

@@ -1,9 +1,5 @@
 package mvc
 
-import (
-	"sync"
-)
-
 type View interface {
-	Shutdown(*sync.WaitGroup)
+	Shutdown()
 }

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

@@ -7,5 +7,5 @@ import (
 )
 
 // no auto-updating in debug mode
-func autoUpdate(ctl *mvc.Controller, token string) {
+func autoUpdate(ctl mvc.Controller, token string) {
 }

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

@@ -6,9 +6,9 @@ import (
 	update "github.com/inconshreveable/go-update"
 	"net/http"
 	"net/url"
+	"ngrok/client/mvc"
 	"ngrok/log"
 	"ngrok/version"
-	"ngrok/client/mvc"
 	"runtime"
 	"time"
 )

+ 14 - 14
src/ngrok/client/views/term/http.go

@@ -2,6 +2,7 @@ package term
 
 import (
 	termbox "github.com/nsf/termbox-go"
+	"ngrok/client/mvc"
 	"ngrok/log"
 	"ngrok/proto"
 	"ngrok/util"
@@ -12,12 +13,13 @@ const (
 )
 
 type HttpView struct {
+	log.Logger
+	*area
+
 	httpProto    *proto.Http
 	HttpRequests *util.Ring
 	shutdown     chan int
-	flush        chan int
-	*area
-	log.Logger
+	termView     *TermView
 }
 
 func colorFor(status string) termbox.Attribute {
@@ -33,18 +35,15 @@ func colorFor(status string) termbox.Attribute {
 	return termbox.ColorWhite
 }
 
-func NewHttp(proto *proto.Http, flush, shutdown chan int, x, y int) *HttpView {
+func newTermHttpView(ctl mvc.Controller, termView *TermView, proto *proto.Http, x, y int) *HttpView {
 	v := &HttpView{
 		httpProto:    proto,
 		HttpRequests: util.NewRing(size),
 		area:         NewArea(x, y, 70, size+5),
-		shutdown:     shutdown,
-		flush:        flush,
-		Logger:       log.NewPrefixLogger(),
+		shutdown:     make(chan int),
+		termView:     termView,
+		Logger:       log.NewPrefixLogger("view", "term", "http"),
 	}
-	v.AddLogPrefix("view")
-	v.AddLogPrefix("term")
-	v.AddLogPrefix("http")
 	go v.Run()
 	return v
 }
@@ -54,9 +53,6 @@ func (v *HttpView) Run() {
 
 	for {
 		select {
-		case <-v.shutdown:
-			return
-
 		case txn := <-updates:
 			v.Debug("Got HTTP update")
 			if txn.(*proto.HttpTxn).Resp == nil {
@@ -78,5 +74,9 @@ func (v *HttpView) Render() {
 			v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
 		}
 	}
-	v.flush <- 1
+	v.termView.Flush()
+}
+
+func (v *HttpView) Shutdown() {
+	close(v.shutdown)
 }

+ 61 - 65
src/ngrok/client/views/term/view.go

@@ -1,6 +1,4 @@
-/*
-   interactive terminal interface for local clients
-*/
+// interactive terminal interface for local clients
 package term
 
 import (
@@ -9,6 +7,7 @@ import (
 	"ngrok/client/mvc"
 	"ngrok/log"
 	"ngrok/proto"
+	"ngrok/util"
 	"time"
 )
 
@@ -16,61 +15,50 @@ type TermView struct {
 	ctl      mvc.Controller
 	updates  chan interface{}
 	flush    chan int
+	shutdown chan int
+	redraw   *util.Broadcast
 	subviews []mvc.View
-	state    mvc.State
 	log.Logger
 	*area
 }
 
-func New(ctl mvc.Controller, state mvc.State) *TermView {
+func NewTermView(ctl mvc.Controller) *TermView {
 	// initialize terminal display
 	termbox.Init()
 
-	// make sure ngrok doesn't quit until we've cleaned up
-	ctl.Wait.Add(1)
-
 	w, _ := termbox.Size()
 
 	v := &TermView{
 		ctl:      ctl,
-		updates:  ctl.Updates.Reg(),
+		updates:  ctl.Updates().Reg(),
+		redraw:   util.NewBroadcast(),
 		flush:    make(chan int),
-		subviews: make([]mvc.View, 0),
-		state:    state,
-		Logger:   log.NewPrefixLogger(),
+		shutdown: make(chan int),
+		Logger:   log.NewPrefixLogger("view", "term"),
 		area:     NewArea(0, 0, w, 10),
 	}
 
-	v.Logger.AddLogPrefix("view")
-	v.Logger.AddLogPrefix("term")
-
-	switch p := state.GetProtocol().(type) {
-	case *proto.Http:
-		v.subviews = append(v.subviews, NewHttp(p, v.flush, ctl.Shutdown, 0, 10))
-	default:
-	}
-
-	v.Render()
-
-	go v.run()
-	go v.input()
+	ctl.Go(v.run)
+	ctl.Go(v.input)
 
 	return v
 }
 
-func colorForConn(status string) termbox.Attribute {
+func connStatusRepr(status mvc.ConnStatus) (string, termbox.Attribute) {
 	switch status {
-	case "connecting":
-		return termbox.ColorCyan
-	case "reconnecting":
-		return termbox.ColorRed
-	case "online":
-		return termbox.ColorGreen
+	case mvc.ConnConnecting:
+		return "connecting", termbox.ColorCyan
+	case mvc.ConnReconnecting:
+		return "reconnecting", termbox.ColorRed
+	case mvc.ConnOnline:
+		return "online", termbox.ColorGreen
 	}
-	return termbox.ColorWhite
+	return "unknown", termbox.ColorWhite
 }
 
-func (v *TermView) Render() {
+func (v *TermView) draw() {
+	state := v.ctl.State()
+
 	v.Clear()
 
 	// quit instructions
@@ -78,7 +66,7 @@ func (v *TermView) Render() {
 	v.Printf(v.w-len(quitMsg), 0, quitMsg)
 
 	// new version message
-	updateStatus := v.state.GetUpdate()
+	updateStatus := state.GetUpdateStatus()
 	var updateMsg string
 	switch updateStatus {
 	case mvc.UpdateNone:
@@ -111,49 +99,64 @@ func (v *TermView) Render() {
 	}
 
 	v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok")
+	statusStr, statusColor := connStatusRepr(state.GetConnStatus())
+	v.APrintf(statusColor, 0, 2, "%-30s%s", "Tunnel Status", statusStr)
+
+	v.Printf(0, 3, "%-30s%s/%s", "Version", state.GetClientVersion(), state.GetServerVersion())
+	var i int = 4
+	for _, t := range state.GetTunnels() {
+		v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
+		i++
+	}
+	webAddr := fmt.Sprintf("http://localhost:%d", v.ctl.GetWebViewPort())
+	v.Printf(0, i+0, "%-30s%s", "Web Interface", webAddr)
 
-	status := v.state.GetStatus()
-	v.APrintf(colorForConn(status), 0, 2, "%-30s%s", "Tunnel Status", status)
-
-	v.Printf(0, 3, "%-30s%s/%s", "Version", v.state.GetClientVersion(), v.state.GetServerVersion())
-	v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
-	v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
-	webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
-	v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
-
-	connMeter, connTimer := v.state.GetConnectionMetrics()
-	v.Printf(0, 7, "%-30s%d", "# Conn", connMeter.Count())
+	connMeter, connTimer := state.GetConnectionMetrics()
+	v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())
 
 	msec := float64(time.Millisecond)
-	v.Printf(0, 8, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
+	v.Printf(0, i+2, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
 
 	termbox.Flush()
 }
 
 func (v *TermView) run() {
-	defer v.ctl.Wait.Done()
+	defer close(v.shutdown)
 	defer termbox.Close()
 
+	redraw := v.redraw.Reg()
+	defer v.redraw.UnReg(redraw)
+
+	v.draw()
 	for {
 		v.Debug("Waiting for update")
 		select {
 		case <-v.flush:
 			termbox.Flush()
 
-		case obj := <-v.updates:
-			if obj != nil {
-				v.state = obj.(mvc.State)
-			}
-			v.Render()
+		case <-v.updates:
+			v.draw()
+
+		case <-redraw:
+			v.draw()
 
-		case <-v.ctl.Shutdown:
+		case <-v.shutdown:
 			return
 		}
 	}
 }
 
-func (v *TermView) Shutdown(wg *sync.WaitGroup) {
-	wg.Done()
+func (v *TermView) Shutdown() {
+	v.shutdown <- 1
+	<-v.shutdown
+}
+
+func (v *TermView) Flush() {
+	v.flush <- 1
+}
+
+func (v *TermView) NewHttpView(p *proto.Http) *HttpView {
+	return newTermHttpView(v.ctl, v, p, 0, 12)
 }
 
 func (v *TermView) input() {
@@ -164,21 +167,14 @@ func (v *TermView) input() {
 			switch ev.Key {
 			case termbox.KeyCtrlC:
 				v.Info("Got quit command")
-				ctl.Shutdown()
+				v.ctl.Shutdown("")
 			}
 
 		case termbox.EventResize:
 			v.Info("Resize event, redrawing")
-			// send nil to update channel to force re-rendering
-			v.updates <- nil
-			for _, sv := range v.subviews {
-				sv.Render()
-			}
+			v.redraw.In() <- 1
 
 		case termbox.EventError:
-			if v.ctl.IsShuttingDown() {
-				return
-			}
 			panic(ev.Err)
 		}
 	}

+ 25 - 32
src/ngrok/client/views/web/http.go

@@ -55,17 +55,18 @@ type SerializedResponse struct {
 }
 
 type WebHttpView struct {
+	log.Logger
+
 	webview      *WebView
-	ctl          *ui.Controller
+	ctl          mvc.Controller
 	httpProto    *proto.Http
-	updates      chan interface{}
 	state        chan SerializedUiState
 	HttpRequests *util.Ring
 	idToTxn      map[string]*SerializedTxn
 }
 
 type SerializedUiState struct {
-	Url string
+	Tunnels []mvc.Tunnel
 }
 
 type SerializedPayload struct {
@@ -73,20 +74,18 @@ type SerializedPayload struct {
 	UiState SerializedUiState
 }
 
-func NewWebHttpView(wv *WebView, ctl *ui.Controller, proto *proto.Http) *WebHttpView {
-	w := &WebHttpView{
+func newWebHttpView(ctl mvc.Controller, wv *WebView, proto *proto.Http) *WebHttpView {
+	whv := &WebHttpView{
+		Logger:       log.NewPrefixLogger("view", "web", "http"),
 		webview:      wv,
 		ctl:          ctl,
 		httpProto:    proto,
 		idToTxn:      make(map[string]*SerializedTxn),
-		updates:      ctl.Updates.Reg(),
-		state:        make(chan SerializedUiState),
 		HttpRequests: util.NewRing(20),
 	}
-	go w.updateHttp()
-	go w.updateUiState()
-	w.register()
-	return w
+	ctl.Go(whv.updateHttp)
+	whv.register()
+	return whv
 }
 
 type XMLDoc struct {
@@ -156,13 +155,13 @@ func (whv *WebHttpView) updateHttp() {
 		if htxn.UserData == nil {
 			id, err := util.RandId(8)
 			if err != nil {
-				log.Error("Failed to generate txn identifier for web storage: %v", err)
+				whv.Error("Failed to generate txn identifier for web storage: %v", err)
 				continue
 			}
 
 			rawReq, err := httputil.DumpRequest(htxn.Req.Request, true)
 			if err != nil {
-				log.Error("Failed to dump request: %v", err)
+				whv.Error("Failed to dump request: %v", err)
 				continue
 			}
 
@@ -189,7 +188,7 @@ func (whv *WebHttpView) updateHttp() {
 		} else {
 			rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true)
 			if err != nil {
-				log.Error("Failed to dump response: %v", err)
+				whv.Error("Failed to dump response: %v", err)
 				continue
 			}
 
@@ -206,35 +205,26 @@ func (whv *WebHttpView) updateHttp() {
 
 			payload, err := json.Marshal(txn)
 			if err != nil {
-				log.Error("Failed to serialized txn payload for websocket: %v", err)
+				whv.Error("Failed to serialized txn payload for websocket: %v", err)
 			}
 			whv.webview.wsMessages.In() <- payload
 		}
 	}
 }
 
-func (v *WebHttpView) updateUiState() {
-	var s SerializedUiState
-	for {
-		select {
-		case obj := <-v.updates:
-			uiState := obj.(ui.State)
-			s.Url = uiState.GetPublicUrl()
-		case v.state <- s:
-		}
-	}
-}
-
-func (h *WebHttpView) register() {
+func (whv *WebHttpView) register() {
 	http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) {
 		r.ParseForm()
 		txnid := r.Form.Get("txnid")
-		if txn, ok := h.idToTxn[txnid]; ok {
+		if txn, ok := whv.idToTxn[txnid]; ok {
 			bodyBytes, err := httputil.DumpRequestOut(txn.HttpTxn.Req.Request, true)
 			if err != nil {
 				panic(err)
 			}
-			h.ctl.Cmds <- ui.CmdRequest{Payload: bodyBytes}
+			// XXX: pull the tunnel out of the transaction's user context
+			//h.ctl.PlayRequest(tunnel, bodyBytes)
+			var t mvc.Tunnel
+			whv.ctl.PlayRequest(t, bodyBytes)
 			w.Write([]byte(http.StatusText(200)))
 		} else {
 			http.Error(w, http.StatusText(400), 400)
@@ -250,8 +240,8 @@ func (h *WebHttpView) register() {
 		tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl)))
 
 		payloadData := SerializedPayload{
-			Txns:    h.HttpRequests.Slice(),
-			UiState: <-h.state,
+			Txns:    whv.HttpRequests.Slice(),
+			UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()},
 		}
 
 		payload, err := json.Marshal(payloadData)
@@ -265,3 +255,6 @@ func (h *WebHttpView) register() {
 		}
 	})
 }
+
+func (whv *WebHttpView) Shutdown() {
+}

+ 26 - 13
src/ngrok/client/views/web/view.go

@@ -14,34 +14,39 @@ import (
 )
 
 type WebView struct {
+	log.Logger
+
+	// saved only for creating subviews
+	ctl mvc.Controller
+
+	// messages sent over this broadcast are sent too all websocket connections
 	wsMessages *util.Broadcast
 }
 
-func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
-	v := &WebView{
+func NewWebView(ctl mvc.Controller, port int) *WebView {
+	wv := &WebView{
+		Logger:     log.NewPrefixLogger("view", "web"),
 		wsMessages: util.NewBroadcast(),
+		ctl:        ctl,
 	}
 
-	switch p := state.GetProtocol().(type) {
-	case *proto.Http:
-		NewWebHttpView(v, ctl, p)
-	}
-
+	// for now, always redirect to the http view
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, "/http/in", 302)
 	})
 
+	// handle web socket connections
 	http.HandleFunc("/_ws", func(w http.ResponseWriter, r *http.Request) {
 		conn, err := websocket.Upgrade(w, r.Header, nil, 1024, 1024)
 
 		if err != nil {
 			http.Error(w, "Failed websocket upgrade", 400)
-			log.Warn("Failed websocket upgrade: %v", err)
+			wv.Warn("Failed websocket upgrade: %v", err)
 			return
 		}
 
-		msgs := v.wsMessages.Reg()
-		defer v.wsMessages.UnReg(msgs)
+		msgs := wv.wsMessages.Reg()
+		defer wv.wsMessages.UnReg(msgs)
 		for m := range msgs {
 			err := conn.WriteMessage(websocket.OpText, m.([]byte))
 			if err != nil {
@@ -51,17 +56,25 @@ func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
 		}
 	})
 
+	// serve static assets
 	http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
 		buf, err := assets.ReadAsset(path.Join("assets", "client", r.URL.Path[1:]))
 		if err != nil {
-			log.Warn("Error serving static file: %s", err.Error())
+			wv.Warn("Error serving static file: %s", err.Error())
 			http.NotFound(w, r)
 			return
 		}
 		w.Write(buf)
 	})
 
-	log.Info("Serving web interface on localhost:%d", port)
+	wv.Info("Serving web interface on localhost:%d", port)
 	go http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
-	return v
+	return wv
+}
+
+func (wv *WebView) NewHttpView(proto *proto.Http) *WebHttpView {
+	return newWebHttpView(wv.ctl, wv, proto)
+}
+
+func (wv *WebView) Shutdown() {
 }

+ 1 - 1
src/ngrok/msg/msg.go

@@ -47,7 +47,7 @@ type RegAckMsg struct {
 	Version   string
 	MmVersion string
 	Url       string
-	ProxyAddr string
+	Protocol  string
 	Error     string
 	ClientId  string
 }

+ 13 - 10
src/ngrok/proto/http.go

@@ -23,18 +23,21 @@ type HttpResponse struct {
 }
 
 type HttpTxn struct {
-	Req      *HttpRequest
-	Resp     *HttpResponse
-	Start    time.Time
-	Duration time.Duration
-	UserData interface{}
+	Req          *HttpRequest
+	Resp         *HttpResponse
+	Start        time.Time
+	Duration     time.Duration
+	UserData     interface{}
+	ConnUserData interface{}
 }
 
 type Http struct {
-	Txns     *util.Broadcast
-	reqGauge metrics.Gauge
-	reqMeter metrics.Meter
-	reqTimer metrics.Timer
+	Txns         *util.Broadcast
+	reqGauge     metrics.Gauge
+	reqMeter     metrics.Meter
+	reqTimer     metrics.Timer
+	UserData     interface{}
+	ConnUserData interface{}
 }
 
 func NewHttp() *Http {
@@ -54,7 +57,7 @@ func extractBody(r io.Reader) ([]byte, io.ReadCloser, error) {
 
 func (h *Http) GetName() string { return "http" }
 
-func (h *Http) WrapConn(c conn.Conn) conn.Conn {
+func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
 	tee := conn.NewTee(c)
 	lastTxn := make(chan *HttpTxn)
 	go h.readRequests(tee, lastTxn)

+ 1 - 1
src/ngrok/proto/interface.go

@@ -6,5 +6,5 @@ import (
 
 type Protocol interface {
 	GetName() string
-	WrapConn(conn.Conn) conn.Conn
+	WrapConn(conn.Conn, interface{}) conn.Conn
 }

+ 1 - 1
src/ngrok/proto/tcp.go

@@ -12,6 +12,6 @@ func NewTcp() *Tcp {
 
 func (h *Tcp) GetName() string { return "tcp" }
 
-func (h *Tcp) WrapConn(c conn.Conn) conn.Conn {
+func (h *Tcp) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
 	return c
 }

+ 1 - 1
src/ngrok/server/control.go

@@ -113,7 +113,7 @@ func (c *Control) registerTunnel(regMsg *msg.RegMsg) {
 	// acknowledge success
 	c.out <- &msg.RegAckMsg{
 		Url:       t.url,
-		ProxyAddr: fmt.Sprintf("%s:%d", opts.domain, opts.tunnelPort),
+		Protocol:  regMsg.Protocol,
 		Version:   version.Proto,
 		MmVersion: version.MajorMinor(),
 		ClientId:  c.id,