Browse Source

add support for a yaml-based ngrok configuration file. add full support for running multiple configured tunnels from the configuration file. modify CLI to support ngrok commands. remove snakeoil CA from release builds of ngrok client. add support for dialing to an HTTP proxy via HTTPS. Use http_proxy environment variable when none is specified. ngrokd now honors the new protocol and proplery populates ReqId in NewTunnel messages. ngrokd TLS configuration options are now command line switches instead of environment variables.

Alan Shreve 12 years ago
parent
commit
824a4b10ab

+ 1 - 0
.gitignore

@@ -4,5 +4,6 @@ pkg/
 src/code.google.com
 src/github.com
 src/bitbucket.org
+src/launchpad.net
 src/ngrok/client/assets/assets_release.go
 src/ngrok/server/assets/assets_release.go

+ 45 - 0
docs/SELFHOSTING.md

@@ -0,0 +1,45 @@
+# How to run your own ngrokd server
+
+Running your own ngrok server is really easy.
+
+## Compile it
+You can compile an ngrokd server with the following command:
+
+	make release-server
+
+## Run the server
+You'll run the server with the following command. There's more information on the
+
+
+	./ngrokd -tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt" -domain="example.com"
+
+### Specifying your TLS certificate and key
+ngrok only makes TLS-encrypted connections. To run your own ngrokd server, you'll need your own
+TLS certificates for your domain. You need a *wild card* certificate for *.example.com.
+When you run ngrokd, you'll need to instruct it where to find your TLS certificate and private
+key. Specify the paths with the following switches:
+
+	-tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt"
+
+### Setting the server's domain
+When you run your own ngrokd server, you need to tell ngrokd the domain it's running on so that it
+knows what URLs to issue to clients.
+
+	-domain="example.com"
+
+### Modifying your DNS
+You need to use the DNS management tools given to you by your provider to create an A
+record which points *.example.com to the IP address of the server running ngrokd.
+
+## Connect with a client
+In order to connect with a client, you'll need to set two options in ngrok's configuration file.
+The ngrok configuration file is a simple YAML file that is read from ~/.ngrok by default. You may specify
+a custom configuration file path with the -config switch. Your config file must contain the following two
+options.
+
+	server_addr: example.com:4443
+	trust_host_root_certs: true
+
+Then, just run ngrok as usual to connect securely to your own ngrokd server!
+
+	ngrok 80

+ 0 - 64
src/ngrok/client/auth.go

@@ -1,64 +0,0 @@
-package client
-
-/*
-   Functions for reading and writing the auth token from the user's
-   home directory.
-*/
-
-import (
-	"io/ioutil"
-	"ngrok/log"
-	"os"
-	"os/user"
-	"path"
-	"sync"
-)
-
-var (
-	once             sync.Once
-	currentAuthToken string
-	authTokenFile    string
-)
-
-func initAuth() {
-	user, err := user.Current()
-
-	// user.Current() does not work on linux when cross compilling because
-	// it requires CGO; use os.Getenv("HOME") hack until we compile natively
-	homeDir := os.Getenv("HOME")
-	if err != nil {
-		log.Warn("Failed to get user's home directory: %s", err.Error())
-	} else {
-		homeDir = user.HomeDir
-	}
-
-	authTokenFile = path.Join(homeDir, ".ngrok")
-
-	log.Debug("Reading auth token from file %s", authTokenFile)
-	tokenBytes, err := ioutil.ReadFile(authTokenFile)
-
-	if err == nil {
-		currentAuthToken = string(tokenBytes)
-	} else {
-		log.Warn("Failed to read ~/.ngrok for auth token: %s", err.Error())
-	}
-}
-
-// Load the auth token from file
-func LoadAuthToken() string {
-	once.Do(initAuth)
-	return currentAuthToken
-}
-
-// Save the auth token to file
-func SaveAuthToken(token string) {
-	if token == "" || token == LoadAuthToken() || authTokenFile == "" {
-		return
-	}
-
-	perms := os.FileMode(0644)
-	err := ioutil.WriteFile(authTokenFile, []byte(token), perms)
-	if err != nil {
-		log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
-	}
-}

+ 77 - 114
src/ngrok/client/cli.go

@@ -1,112 +1,72 @@
 package client
 
 import (
-	"errors"
 	"flag"
 	"fmt"
-	"net"
 	"ngrok/version"
 	"os"
-	"strconv"
 )
 
-var (
-	PORT_OUT_OF_RANGE error = errors.New("Port number must be between 1 and 65535")
-)
-
-type Options struct {
-	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{}) {
-	fmt.Printf(msg+"\n", args...)
-	flag.PrintDefaults()
-	os.Exit(1)
-}
+const usage1 string = `Usage: %s [OPTIONS] <local port or address>
+Options:
+`
 
-func parsePort(portString string) (err error) {
-	var port int
-	if port, err = strconv.Atoi(portString); err != nil {
-		return err
-	}
+const usage2 string = `
+Examples:
+	ngrok 80
+	ngrok -subdomain=example 8080
+	ngrok -proto=tcp 22
+	ngrok -hostname="example.com" -httpauth="user:password" 10.0.0.1
 
-	if port < 1 || port > 65535 {
-		return PORT_OUT_OF_RANGE
-	}
 
-	return
-}
+Advanced usage: ngrok [OPTIONS] <command> [command args] [...]
+Commands:
+	ngrok start [tunnel] [...]    Start tunnels by name from config file
+	ngrok help                    Print help
+	ngrok version                 Print ngrok version
 
-// Local address could be a port of a host:port string
-// we always return a host:port string from this function or fail
-func parseLocalAddr() string {
-	if flag.NArg() == 0 {
-		fail("LOCAL not specified, specify a port number or host:port connection string")
-	}
+Examples:
+	ngrok start www api blog pubsub
+	ngrok -log=stdout -config=ngrok.yml start ssh
+	ngrok version
 
-	if flag.NArg() > 1 {
-		fail("Only one LOCAL may be specified, not %d", flag.NArg())
-	}
+`
 
-	addr := flag.Arg(0)
-
-	// try to parse as a port number
-	if err := parsePort(addr); err == nil {
-		return fmt.Sprintf("127.0.0.1:%s", addr)
-	} else if err == PORT_OUT_OF_RANGE {
-		fail("%s is not in the valid port range 1-65535")
-	}
-
-	// try to parse as a connection string
-	_, port, err := net.SplitHostPort(addr)
-	if err != nil {
-		fail("%v", err)
-	}
-
-	if parsePort(port) != nil {
-		fail("'%s' is not a valid port number (1-65535)", port)
-	}
-
-	return addr
+type Options struct {
+	config    string
+	logto     string
+	authtoken string
+	httpauth  string
+	hostname  string
+	protocol  string
+	subdomain string
+	command   string
+	args      []string
 }
 
-func parseProtocol(proto string) string {
-	switch proto {
-	case "http", "https", "http+https", "tcp":
-		return proto
-	default:
-		fail("%s is not a valid protocol", proto)
+func parseArgs() (opts *Options, err error) {
+	flag.Usage = func() {
+		fmt.Fprintf(os.Stderr, usage1, os.Args[0])
+		flag.PrintDefaults()
+		fmt.Fprintf(os.Stderr, usage2)
 	}
-	panic("unreachable")
-}
 
-func parseArgs() *Options {
-	authtoken := flag.String(
-		"authtoken",
+	config := flag.String(
+		"config",
 		"",
-		"Authentication token for identifying a premium ngrok.com account")
+		"Path to ngrok configuration file. (default: $HOME/.ngrok)")
 
-	serverAddr := flag.String(
-		"serverAddr",
-		"ngrokd.ngrok.com:443",
-		"Address of the remote ngrokd server")
+	logto := flag.String(
+		"log",
+		"none",
+		"Write log messages to this file. 'stdout' and 'none' have special meanings")
 
-	proxyAddr := flag.String(
-		"proxyAddr",
+	authtoken := flag.String(
+		"authtoken",
 		"",
-		"The address of an http proxy to connect through (ex: [user:pw@]proxy.example.org:3128)")
+		"Authentication token for identifying an ngrok.com account")
 
-	httpAuth := flag.String(
+	httpauth := flag.String(
 		"httpauth",
 		"",
 		"username:password HTTP basic auth creds protecting the public tunnel endpoint")
@@ -114,7 +74,7 @@ func parseArgs() *Options {
 	subdomain := flag.String(
 		"subdomain",
 		"",
-		"Request a custom subdomain from the ngrok server. (HTTP mode only)")
+		"Request a custom subdomain from the ngrok server. (HTTP only)")
 
 	hostname := flag.String(
 		"hostname",
@@ -126,38 +86,41 @@ func parseArgs() *Options {
 		"http+https",
 		"The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')")
 
-	webport := flag.Int(
-		"webport",
-		4040,
-		"The port on which the web interface is served, -1 to disable")
-
-	logto := flag.String(
-		"log",
-		"none",
-		"Write log messages to this file. 'stdout' and 'none' have special meanings")
-
-	v := flag.Bool(
-		"version",
-		false,
-		"Print ngrok version and exit")
-
 	flag.Parse()
 
-	if *v {
+	opts = &Options{
+		config:    *config,
+		logto:     *logto,
+		httpauth:  *httpauth,
+		subdomain: *subdomain,
+		protocol:  *protocol,
+		authtoken: *authtoken,
+		hostname:  *hostname,
+		command:   flag.Arg(0),
+	}
+
+	switch opts.command {
+	case "start":
+		opts.args = flag.Args()[1:]
+	case "version":
 		fmt.Println(version.MajorMinor())
 		os.Exit(0)
-	}
+	case "help":
+		flag.Usage()
+		os.Exit(0)
+	case "":
+		err = fmt.Errorf("You must specify a local port to tunnel or an ngrok command.")
+		return
+
+	default:
+		if len(flag.Args()) > 1 {
+			err = fmt.Errorf("You may only specify one port/address to tunnel to on the command line")
+			return
+		}
 
-	return &Options{
-		serverAddr: *serverAddr,
-		proxyAddr:  *proxyAddr,
-		httpAuth:   *httpAuth,
-		subdomain:  *subdomain,
-		localaddr:  parseLocalAddr(),
-		protocol:   parseProtocol(*protocol),
-		webport:    *webport,
-		logto:      *logto,
-		authtoken:  *authtoken,
-		hostname:   *hostname,
+		opts.command = "default"
+		opts.args = flag.Args()
 	}
+
+	return
 }

+ 198 - 0
src/ngrok/client/config.go

@@ -0,0 +1,198 @@
+package client
+
+import (
+"fmt"
+	"io/ioutil"
+	"launchpad.net/goyaml"
+	"ngrok/log"
+	"os"
+	"os/user"
+	"path"
+	"strings"
+	"net/url"
+	"net"
+	"strconv"
+)
+
+type Configuration struct {
+	ProxyUrl           string `yaml:"proxy_url"`
+	ServerAddr         string `yaml:"server_addr"`
+	InspectAddr        string `yaml:"inspect_addr"`
+	TrustHostRootCerts bool   `yaml:"trust_host_root_certs"`
+	AuthToken          string `yaml:"auth_token"`
+	Tunnels            map[string]TunnelConfiguration `yaml:"tunnels"`
+	LogTo			string
+}
+
+type TunnelConfiguration struct {
+	Subdomain string            `yaml:"subdomain"`
+	Hostname  string            `yaml:"hostname"`
+	Protocols map[string]string `yaml:"proto"`
+	HttpAuth  string            `yaml:"auth"`
+}
+
+func defaultPath() string {
+	user, err := user.Current()
+
+	// user.Current() does not work on linux when cross compilling because
+	// it requires CGO; use os.Getenv("HOME") hack until we compile natively
+	homeDir := os.Getenv("HOME")
+	if err != nil {
+		log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir)
+	} else {
+		homeDir = user.HomeDir
+	}
+
+	return path.Join(homeDir, ".ngrok")
+}
+
+func LoadConfiguration(opts *Options) (config *Configuration, err error) {
+	configPath := opts.config
+	if configPath == "" {
+		configPath = defaultPath()
+	}
+
+	log.Info("Reading configuration file %s", configPath)
+	configBuf, err := ioutil.ReadFile(configPath)
+	if err != nil {
+		// failure to read a configuration file is only a fatal error if
+		// the user specified one explicitly
+		if opts.config != "" {
+			err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
+			return
+		}
+	}
+
+	// deserialize/parse the config
+	config = new(Configuration)
+	if err = goyaml.Unmarshal(configBuf, &config); err != nil {
+		err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
+		return
+	}
+
+	// set configuration defaults
+	if config.ServerAddr == "" {
+		config.ServerAddr = defaultServerAddr
+	}
+
+	if config.InspectAddr == "" {
+		config.InspectAddr = "127.0.0.1:4040"
+	}
+
+	if config.ProxyUrl == "" {
+		config.ProxyUrl = os.Getenv("http_proxy")
+	}
+
+	// override configuration with command-line options
+	config.LogTo = opts.logto
+	if opts.authtoken != "" {
+		config.AuthToken = opts.authtoken
+	}
+
+	switch opts.command {
+	// start a single tunnel, the default, simple ngrok behavior
+	case "default":
+		config.Tunnels = make(map[string]TunnelConfiguration)
+		config.Tunnels["default"] = TunnelConfiguration{
+			Subdomain: opts.subdomain,
+			Hostname:  opts.hostname,
+			HttpAuth:  opts.httpauth,
+			Protocols: make(map[string]string),
+		}
+
+		for _, proto := range strings.Split(opts.protocol, "+") {
+			config.Tunnels["default"].Protocols[proto] = opts.args[0]
+		}
+
+	// start tunnels
+	case "start":
+		if len(opts.args) == 0 {
+			err = fmt.Errorf("You must specify at least one tunnel to start")
+			return
+		}
+
+		requestedTunnels := make(map[string]bool)
+		for _, arg := range opts.args {
+			requestedTunnels[arg] = true
+
+			if _, ok := config.Tunnels[arg]; !ok {
+				err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg)
+				return
+			}
+		}
+
+		for name, _ := range config.Tunnels {
+			if !requestedTunnels[name] {
+				delete(config.Tunnels, name)
+			}
+		}
+
+	default:
+		err = fmt.Errorf("Unknown command: %s", opts.command)
+		return
+	}
+
+	// validate and normalize configuration
+	if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil {
+		return
+	}
+
+	if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
+		return
+	}
+
+	if config.ProxyUrl != "" {
+		var proxyUrl *url.URL
+		if proxyUrl, err = url.Parse(config.ProxyUrl); err != nil {
+			return
+		} else {
+			if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" {
+				err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme)
+				return
+			}
+		}
+	}
+
+	for name, t := range config.Tunnels {
+		for k, addr := range t.Protocols {
+			tunnelName := fmt.Sprintf("tunnel %s[%s]", name, k)
+			if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
+				return
+			}
+
+			if err = validateProtocol(k, tunnelName); err != nil {
+				return
+			}
+		}
+	}
+
+	return
+}
+
+func normalizeAddress(addr string, propName string) (string, error) {
+	// normalize port to address
+	if _, err := strconv.Atoi(addr); err == nil {
+		addr = ":" + addr
+	}
+
+	tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
+	if err != nil {
+		return "", fmt.Errorf("Invalid %s '%s': %s", propName, addr, err.Error())
+	}
+
+	if tcpAddr.IP == nil {
+		tcpAddr.IP = net.ParseIP("127.0.0.1")
+	}
+
+	return tcpAddr.String(), nil
+}
+
+func validateProtocol(proto, propName string) (err error) {
+	switch proto {
+	case "http", "https", "http+https", "tcp":
+	default:
+		err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
+	}
+
+	return
+}

+ 12 - 21
src/ngrok/client/controller.go

@@ -6,7 +6,6 @@ import (
 	"ngrok/client/views/term"
 	"ngrok/client/views/web"
 	"ngrok/log"
-	"ngrok/msg"
 	"ngrok/proto"
 	"ngrok/util"
 	"sync"
@@ -48,7 +47,7 @@ type Controller struct {
 	state chan mvc.State
 
 	// options
-	opts *Options
+	config *Configuration
 }
 
 // public interface
@@ -127,29 +126,29 @@ 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) GetWebInspectAddr() string {
+	return ctl.config.InspectAddr
 }
 
-func (ctl *Controller) Run(opts *Options) {
-	// Save the options
-	ctl.opts = opts
+func (ctl *Controller) Run(config *Configuration) {
+	// Save the configuration
+	ctl.config = config
 
 	// init the model
-	model := newClientModel(ctl)
+	model := newClientModel(config, 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)
+	if config.InspectAddr != "disabled" {
+		webView = web.NewWebView(ctl, config.InspectAddr)
 		ctl.addView(webView)
 	}
 
 	// init term ui
 	var termView *term.TermView
-	if opts.logto != "stdout" {
+	if config.LogTo != "stdout" {
 		termView = term.NewTermView(ctl)
 		ctl.addView(termView)
 	}
@@ -168,16 +167,8 @@ func (ctl *Controller) Run(opts *Options) {
 		}
 	}
 
-	ctl.Go(func() { autoUpdate(state, opts.authtoken) })
-
-	reqTunnel := &msg.ReqTunnel{
-		Protocol:  opts.protocol,
-		Hostname:  opts.hostname,
-		Subdomain: opts.subdomain,
-		HttpAuth:  opts.httpAuth,
-	}
-
-	ctl.Go(func() { ctl.model.Run(opts.serverAddr, opts.proxyAddr, opts.authtoken, ctl, reqTunnel, opts.localaddr) })
+	ctl.Go(func() { autoUpdate(state, config.AuthToken) })
+	ctl.Go(ctl.model.Run)
 
 	updates := ctl.updates.Reg()
 	defer ctl.updates.UnReg(updates)

+ 7 - 0
src/ngrok/client/debug.go

@@ -0,0 +1,7 @@
+// +build !release
+
+package client
+
+var (
+	rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"}
+)

+ 15 - 6
src/ngrok/client/main.go

@@ -1,6 +1,8 @@
 package client
 
 import (
+"fmt"
+	"os"
 	"math/rand"
 	"ngrok/log"
 	"ngrok/util"
@@ -8,22 +10,29 @@ import (
 
 func Main() {
 	// parse options
-	opts := parseArgs()
+	opts, err := parseArgs()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
 
 	// set up logging
 	log.LogTo(opts.logto)
 
-	// set up auth token
-	if opts.authtoken == "" {
-		opts.authtoken = LoadAuthToken()
+	// read configuration file
+	config, err := LoadConfiguration(opts)
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
 	}
 
 	// seed random number generator
 	seed, err := util.RandomSeed()
 	if err != nil {
-		log.Error("Couldn't securely seed the random number generator!")
+		fmt.Printf("Couldn't securely seed the random number generator!")
+		os.Exit(1)
 	}
 	rand.Seed(seed)
 
-	NewController().Run(opts)
+	NewController().Run(config)
 }

+ 71 - 24
src/ngrok/client/model.go

@@ -1,6 +1,7 @@
 package client
 
 import (
+	"crypto/tls"
 	"fmt"
 	metrics "github.com/inconshreveable/go-metrics"
 	"io/ioutil"
@@ -10,13 +11,16 @@ import (
 	"ngrok/log"
 	"ngrok/msg"
 	"ngrok/proto"
+	"ngrok/util"
 	"ngrok/version"
 	"runtime"
+	"strings"
 	"sync/atomic"
 	"time"
 )
 
 const (
+	defaultServerAddr   = "ngrokd.ngrok.com:443"
 	pingInterval        = 20 * time.Second
 	maxPongLatency      = 15 * time.Second
 	updateCheckInterval = 6 * time.Hour
@@ -41,20 +45,42 @@ type ClientModel struct {
 	protocols     []proto.Protocol
 	ctl           mvc.Controller
 	serverAddr    string
-	proxyAddr     string
+	proxyUrl      string
 	authToken     string
+	tlsConfig     *tls.Config
+	tunnelConfig  map[string]TunnelConfiguration
 }
 
-func newClientModel(ctl mvc.Controller) *ClientModel {
+func newClientModel(config *Configuration, 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"]}
 
+	// configure TLS
+	var tlsConfig *tls.Config
+	if config.TrustHostRootCerts {
+		tlsConfig = &tls.Config{}
+	} else {
+		var err error
+		if tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil {
+			panic(err)
+		}
+	}
+
 	return &ClientModel{
 		Logger: log.NewPrefixLogger("client"),
 
+		// server address
+		serverAddr: config.ServerAddr,
+
+		// proxy address
+		proxyUrl: config.ProxyUrl,
+
+		// auth token
+		authToken: config.AuthToken,
+
 		// connection status
 		connStatus: mvc.ConnConnecting,
 
@@ -75,6 +101,12 @@ func newClientModel(ctl mvc.Controller) *ClientModel {
 
 		// controller
 		ctl: ctl,
+
+		// tls configuration
+		tlsConfig: tlsConfig,
+
+		// tunnel configuration
+		tunnelConfig: config.Tunnels,
 	}
 }
 
@@ -130,22 +162,16 @@ func (c *ClientModel) update() {
 	c.ctl.Update(c)
 }
 
-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)
-}
-
-func (c *ClientModel) reconnectingControl(reqTunnel *msg.ReqTunnel, localaddr string) {
+func (c *ClientModel) Run() {
 	// how long we should wait before we reconnect
 	maxWait := 30 * time.Second
 	wait := 1 * time.Second
 
 	for {
-		c.control(reqTunnel, localaddr)
+		// run the control channel
+		c.control()
 
+		// control oonly returns when a failure has occurred, so we're going to try to reconnect
 		if c.connStatus == mvc.ConnOnline {
 			wait = 1 * time.Second
 		}
@@ -161,7 +187,7 @@ func (c *ClientModel) reconnectingControl(reqTunnel *msg.ReqTunnel, localaddr st
 }
 
 // Establishes and manages a tunnel control connection with the server
-func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
+func (c *ClientModel) control() {
 	defer func() {
 		if r := recover(); r != nil {
 			log.Error("control recovering from failure %v", r)
@@ -173,11 +199,11 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 		ctlConn conn.Conn
 		err     error
 	)
-	if c.proxyAddr == "" {
+	if c.proxyUrl == "" {
 		// simple non-proxied case, just connect to the server
-		ctlConn, err = conn.Dial(c.serverAddr, "ctl", tlsConfig)
+		ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig)
 	} else {
-		ctlConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "ctl", tlsConfig)
+		ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig)
 	}
 	if err != nil {
 		panic(err)
@@ -214,11 +240,32 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 	c.serverVersion = authResp.MmVersion
 	c.Info("Authenticated with server, client id: %v", c.id)
 	c.update()
-	SaveAuthToken(c.authToken)
 
-	// register the tunnel
-	if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
-		panic(err)
+	// request tunnels
+	reqIdToTunnelConfig := make(map[string]TunnelConfiguration)
+	for _, config := range c.tunnelConfig {
+		// create the protocol list to ask for
+		var protocols []string
+		for proto, _ := range config.Protocols {
+			protocols = append(protocols, proto)
+		}
+
+		reqTunnel := &msg.ReqTunnel{
+			ReqId:     util.RandId(8),
+			Protocol:  strings.Join(protocols, "+"),
+			Hostname:  config.Hostname,
+			Subdomain: config.Subdomain,
+			HttpAuth:  config.HttpAuth,
+		}
+
+		// send the tunnel request
+		if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
+			panic(err)
+		}
+
+		// save request id association so we know which local address
+		// to proxy to later
+		reqIdToTunnelConfig[reqTunnel.ReqId] = config
 	}
 
 	// start the heartbeat
@@ -249,7 +296,7 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
 
 			tunnel := mvc.Tunnel{
 				PublicUrl: m.Url,
-				LocalAddr: localaddr,
+				LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],
 				Protocol:  c.protoMap[m.Protocol],
 			}
 
@@ -271,10 +318,10 @@ func (c *ClientModel) proxy() {
 		err        error
 	)
 
-	if c.proxyAddr == "" {
-		remoteConn, err = conn.Dial(c.serverAddr, "pxy", tlsConfig)
+	if c.proxyUrl == "" {
+		remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig)
 	} else {
-		remoteConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "pxy", tlsConfig)
+		remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig)
 	}
 
 	if err != nil {

+ 2 - 2
src/ngrok/client/mvc/controller.go

@@ -23,6 +23,6 @@ type Controller interface {
 	// safe wrapper for running go-routines
 	Go(fn func())
 
-	// the port where the web interface is running
-	GetWebViewPort() int
+	// the address where the web inspection interface is running
+	GetWebInspectAddr() string
 }

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

@@ -1,11 +1,7 @@
 package mvc
 
-import (
-	"ngrok/msg"
-)
-
 type Model interface {
-	Run(serverAddr, proxyAddr, authToken string, ctl Controller, reqTunnel *msg.ReqTunnel, localaddr string)
+	Run()
 
 	Shutdown()
 

+ 7 - 0
src/ngrok/client/release.go

@@ -0,0 +1,7 @@
+// +build release
+
+package client
+
+var (
+	rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"}
+)

+ 13 - 21
src/ngrok/client/tls.go

@@ -4,42 +4,34 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/pem"
+	"fmt"
 	"ngrok/client/assets"
 )
 
-var (
-	tlsConfig *tls.Config
-)
-
-func init() {
+func LoadTLSConfig(rootCertPaths []string) (*tls.Config, error) {
 	pool := x509.NewCertPool()
 
-	ngrokRootCrt, err := assets.ReadAsset("assets/client/tls/ngrokroot.crt")
-	if err != nil {
-		panic(err)
-	}
-
-	snakeoilCaCrt, err := assets.ReadAsset("assets/client/tls/snakeoilca.crt")
-	if err != nil {
-		panic(err)
-	}
+	for _, certPath := range rootCertPaths {
+		rootCrt, err := assets.ReadAsset(certPath)
+		if err != nil {
+			return nil, err
+		}
 
-	for _, b := range [][]byte{ngrokRootCrt, snakeoilCaCrt} {
-		pemBlock, _ := pem.Decode(b)
+		pemBlock, _ := pem.Decode(rootCrt)
 		if pemBlock == nil {
-			panic("Bad PEM data")
+			return nil, fmt.Errorf("Bad PEM data")
 		}
 
 		certs, err := x509.ParseCertificates(pemBlock.Bytes)
 		if err != nil {
-			panic(err)
+			return nil, err
 		}
 
 		pool.AddCert(certs[0])
 	}
 
-	tlsConfig = &tls.Config{
+	return &tls.Config{
 		RootCAs:    pool,
-		ServerName: "tls.ngrok.com",
-	}
+		ServerName: "ngrokd.ngrok.com",
+	}, nil
 }

+ 1 - 6
src/ngrok/client/views/term/view.go

@@ -2,7 +2,6 @@
 package term
 
 import (
-	"fmt"
 	termbox "github.com/nsf/termbox-go"
 	"ngrok/client/mvc"
 	"ngrok/log"
@@ -108,11 +107,7 @@ func (v *TermView) draw() {
 		v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
 		i++
 	}
-	webAddr := fmt.Sprintf("http://localhost:%d", v.ctl.GetWebViewPort())
-	if v.ctl.GetWebViewPort() == -1 {
-		webAddr = "disabled"
-	}
-	v.Printf(0, i+0, "%-30s%s", "Web Interface", webAddr)
+	v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
 
 	connMeter, connTimer := state.GetConnectionMetrics()
 	v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())

+ 3 - 4
src/ngrok/client/views/web/view.go

@@ -2,7 +2,6 @@
 package web
 
 import (
-	"fmt"
 	"github.com/garyburd/go-websocket/websocket"
 	"net/http"
 	"ngrok/client/assets"
@@ -22,7 +21,7 @@ type WebView struct {
 	wsMessages *util.Broadcast
 }
 
-func NewWebView(ctl mvc.Controller, port int) *WebView {
+func NewWebView(ctl mvc.Controller, addr string) *WebView {
 	wv := &WebView{
 		Logger:     log.NewPrefixLogger("view", "web"),
 		wsMessages: util.NewBroadcast(),
@@ -66,8 +65,8 @@ func NewWebView(ctl mvc.Controller, port int) *WebView {
 		w.Write(buf)
 	})
 
-	wv.Info("Serving web interface on localhost:%d", port)
-	wv.ctl.Go(func() { http.ListenAndServe(fmt.Sprintf(":%d", port), nil) })
+	wv.Info("Serving web interface on %s", addr)
+	wv.ctl.Go(func() { http.ListenAndServe(addr, nil) })
 	return wv
 }
 

+ 21 - 8
src/ngrok/conn/conn.go

@@ -11,7 +11,7 @@ import (
 	"net"
 	"net/http"
 	"ngrok/log"
-	"strings"
+	"net/url"
 )
 
 type Conn interface {
@@ -90,18 +90,31 @@ func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
 	return
 }
 
-func DialHttpProxy(proxyAddr, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
+func DialHttpProxy(proxyUrl, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
+	// parse the proxy address
+	var parsedUrl *url.URL
+	if parsedUrl, err = url.Parse(proxyUrl); err != nil {
+		return
+	}
+
 	var proxyAuth string
+	if parsedUrl.User != nil {
+		proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(parsedUrl.User.String()))
+	}
 
-	// parse the proxy address for authentication credentials
-	addrParts := strings.Split(proxyAddr, "@")
-	if len(addrParts) == 2 {
-		proxyAddr = addrParts[1]
-		proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(addrParts[0]))
+	var proxyTlsConfig *tls.Config
+	switch parsedUrl.Scheme {
+		case "http":
+			proxyTlsConfig = nil
+		case "https":
+			proxyTlsConfig = new(tls.Config)
+		default:
+			err = fmt.Errorf("Proxy URL scheme must be http or https, got: %s", parsedUrl.Scheme)
+			return
 	}
 
 	// dial the proxy
-	if conn, err = Dial(proxyAddr, typ, nil); err != nil {
+	if conn, err = Dial(parsedUrl.Host, typ, proxyTlsConfig); err != nil {
 		return
 	}
 

+ 6 - 0
src/ngrok/server/cli.go

@@ -9,6 +9,8 @@ type Options struct {
 	httpsAddr  string
 	tunnelAddr string
 	domain     string
+	tlsCrt     string
+	tlsKey     string
 	logto      string
 }
 
@@ -17,6 +19,8 @@ func parseArgs() *Options {
 	httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable")
 	tunnelAddr := flag.String("tunnelAddr", ":4443", "Public address listening for ngrok client")
 	domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
+	tlsCrt := flag.String("tlsCrt", "", "Path to a TLS certificate file")
+	tlsKey := flag.String("tlsKey", "", "Path to a TLS key file")
 	logto := flag.String("log", "stdout", "Write log messages to this file. 'stdout' and 'none' have special meanings")
 
 	flag.Parse()
@@ -26,6 +30,8 @@ func parseArgs() *Options {
 		httpsAddr:  *httpsAddr,
 		tunnelAddr: *tunnelAddr,
 		domain:     *domain,
+		tlsCrt:     *tlsCrt,
+		tlsKey:     *tlsKey,
 		logto:      *logto,
 	}
 }

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

@@ -140,6 +140,7 @@ func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
 		c.out <- &msg.NewTunnel{
 			Url:      t.url,
 			Protocol: proto,
+			ReqId:    rawTunnelReq.ReqId,
 		}
 
 		rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)

+ 9 - 2
src/ngrok/server/main.go

@@ -1,6 +1,7 @@
 package server
 
 import (
+	"crypto/tls"
 	"math/rand"
 	"ngrok/conn"
 	log "ngrok/log"
@@ -53,7 +54,7 @@ func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
 // for ease of deployment. The hope is that by running on port 443, using
 // TLS and running all connections over the same port, we can bust through
 // restrictive firewalls.
-func tunnelListener(addr string) {
+func tunnelListener(addr string, tlsConfig *tls.Config) {
 	// listen for incoming connections
 	listener, err := conn.Listen(addr, "tun", tlsConfig)
 	if err != nil {
@@ -111,6 +112,12 @@ func Main() {
 	// start listeners
 	listeners = make(map[string]*conn.Listener)
 
+	// load tls configuration
+	tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)
+	if err != nil {
+		panic(err)
+	}
+
 	// listen for http
 	if opts.httpAddr != "" {
 		listeners["http"] = startHttpListener(opts.httpAddr, nil)
@@ -122,5 +129,5 @@ func Main() {
 	}
 
 	// ngrok clients
-	tunnelListener(opts.tunnelAddr)
+	tunnelListener(opts.tunnelAddr, tlsConfig)
 }

+ 25 - 25
src/ngrok/server/tls.go

@@ -4,40 +4,40 @@ import (
 	"crypto/tls"
 	"io/ioutil"
 	"ngrok/server/assets"
-	"os"
 )
 
-var (
-	tlsConfig *tls.Config
-)
-
-func init() {
-	readOrBytes := func(envVar string, default_path string) []byte {
-		f := os.Getenv(envVar)
-		if f == "" {
-			b, err := assets.ReadAsset(default_path)
-			if err != nil {
-				panic(err)
-			}
-			return b
-		} else {
-			if b, err := ioutil.ReadFile(f); err != nil {
-				panic(err)
-			} else {
-				return b
-			}
+func LoadTLSConfig(crtPath string, keyPath string) (tlsConfig *tls.Config, err error) {
+	fileOrAsset := func(path string, default_path string) ([]byte, error) {
+		loadFn := ioutil.ReadFile
+		if path == "" {
+			loadFn = assets.ReadAsset
+			path = default_path
 		}
+
+		return loadFn(path)
+	}
+
+	var (
+		crt  []byte
+		key  []byte
+		cert tls.Certificate
+	)
+
+	if crt, err = fileOrAsset(crtPath, "assets/server/tls/snakeoil.crt"); err != nil {
+		return
 	}
 
-	crt := readOrBytes("TLS_CRT_FILE", "assets/server/tls/snakeoil.crt")
-	key := readOrBytes("TLS_KEY_FILE", "assets/server/tls/snakeoil.key")
-	cert, err := tls.X509KeyPair(crt, key)
+	if key, err = fileOrAsset(keyPath, "assets/server/tls/snakeoil.key"); err != nil {
+		return
+	}
 
-	if err != nil {
-		panic(err)
+	if cert, err = tls.X509KeyPair(crt, key); err != nil {
+		return
 	}
 
 	tlsConfig = &tls.Config{
 		Certificates: []tls.Certificate{cert},
 	}
+
+	return
 }