Repository: inconshreveable/ngrok Branch: master Commit: 61d48289f3bf Files: 66 Total size: 271.5 KB Directory structure: gitextract_7xnxnl94/ ├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── LICENSE ├── Makefile ├── README.md ├── assets/ │ ├── client/ │ │ ├── page.html │ │ ├── static/ │ │ │ └── js/ │ │ │ ├── angular.js │ │ │ ├── base64.js │ │ │ ├── jquery.timeago.js │ │ │ ├── ngrok.js │ │ │ └── vkbeautify.js │ │ └── tls/ │ │ ├── ngrokroot.crt │ │ └── snakeoilca.crt │ └── server/ │ └── tls/ │ ├── snakeoil.crt │ └── snakeoil.key ├── contrib/ │ └── com.ngrok.client.plist ├── docs/ │ ├── CHANGELOG.md │ ├── DEVELOPMENT.md │ └── SELFHOSTING.md └── src/ └── ngrok/ ├── cache/ │ └── lru.go ├── client/ │ ├── cli.go │ ├── config.go │ ├── controller.go │ ├── debug.go │ ├── main.go │ ├── metrics.go │ ├── model.go │ ├── mvc/ │ │ ├── controller.go │ │ ├── model.go │ │ ├── state.go │ │ └── view.go │ ├── release.go │ ├── tls.go │ ├── update_debug.go │ ├── update_release.go │ └── views/ │ ├── term/ │ │ ├── area.go │ │ ├── http.go │ │ └── view.go │ └── web/ │ ├── http.go │ └── view.go ├── conn/ │ ├── conn.go │ └── tee.go ├── log/ │ └── logger.go ├── main/ │ ├── ngrok/ │ │ └── ngrok.go │ └── ngrokd/ │ └── ngrokd.go ├── msg/ │ ├── conn.go │ ├── msg.go │ └── pack.go ├── proto/ │ ├── http.go │ ├── interface.go │ └── tcp.go ├── server/ │ ├── cli.go │ ├── control.go │ ├── http.go │ ├── main.go │ ├── metrics.go │ ├── registry.go │ ├── tls.go │ └── tunnel.go ├── util/ │ ├── broadcast.go │ ├── errors.go │ ├── id.go │ ├── ring.go │ └── shutdown.go └── version/ └── version.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.swp bin/ pkg/ src/code.google.com src/github.com src/bitbucket.org src/launchpad.net src/gopkg.in src/ngrok/client/assets/ src/ngrok/server/assets/ ================================================ FILE: .travis.yml ================================================ sudo: false language: go script: make release-all install: true go: - 1.4 - 1.5 - 1.6 - tip matrix: allow_failures: - go: tip ================================================ FILE: CONTRIBUTORS ================================================ Contributors to ngrok, both large and small: - Alan Shreve - Brandon Philips - Caleb Spare - Jay Hayes - Kevin Burke - Kyle Conroy - Nick Presta - Stephen Huenneke - inconshreveable - jzs ================================================ FILE: LICENSE ================================================ Copyright 2013 Alan Shreve Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ .PHONY: default server client deps fmt clean all release-all assets client-assets server-assets contributors export GOPATH:=$(shell pwd) BUILDTAGS=debug default: all deps: assets go get -tags '$(BUILDTAGS)' -d -v ngrok/... server: deps go install -tags '$(BUILDTAGS)' ngrok/main/ngrokd fmt: go fmt ngrok/... client: deps go install -tags '$(BUILDTAGS)' ngrok/main/ngrok assets: client-assets server-assets bin/go-bindata: GOOS="" GOARCH="" go get github.com/jteeuwen/go-bindata/go-bindata client-assets: bin/go-bindata bin/go-bindata -nomemcopy -pkg=assets -tags=$(BUILDTAGS) \ -debug=$(if $(findstring debug,$(BUILDTAGS)),true,false) \ -o=src/ngrok/client/assets/assets_$(BUILDTAGS).go \ assets/client/... server-assets: bin/go-bindata bin/go-bindata -nomemcopy -pkg=assets -tags=$(BUILDTAGS) \ -debug=$(if $(findstring debug,$(BUILDTAGS)),true,false) \ -o=src/ngrok/server/assets/assets_$(BUILDTAGS).go \ assets/server/... release-client: BUILDTAGS=release release-client: client release-server: BUILDTAGS=release release-server: server release-all: fmt release-client release-server all: fmt client server clean: go clean -i -r ngrok/... rm -rf src/ngrok/client/assets/ src/ngrok/server/assets/ contributors: echo "Contributors to ngrok, both large and small:\n" > CONTRIBUTORS git log --raw | grep "^Author: " | sort | uniq | cut -d ' ' -f2- | sed 's/^/- /' | cut -d '<' -f1 >> CONTRIBUTORS ================================================ FILE: README.md ================================================ # ngrok - Unified Ingress for Developers [https://ngrok.com](https://ngrok.com) ## ngrok Community on GitHub If you are having an issue with the ngrok cloud service please open an issue on the [ngrok community on GitHub](https://github.com/ngrok/ngrok) ## This repository is archived This is the GitHub repository for the old v1 version of ngrok which was actively developed from 2013-2016. **This repository is archived: ngrok v1 is no longer developed, supported or maintained.** Thank you to everyone who contributed to ngrok v1 it in its early days with PRs, issues and feedback. If you wish to continue development on this codebase, please fork it. ngrok's cloud service continues to operate and you can sign up for it here: [https://ngrok.com/signup](https://ngrok.com/signup) ## What is ngrok? ngrok is a globally distributed reverse proxy that secures, protects and accelerates your applications and network services, no matter where you run them. You can think of ngrok as the front door to your applications. ngrok combines your reverse proxy, firewall, API gateway, and global load balancing into one. ngrok can capture and analyze all traffic to your web service for later inspection and replay. To use ngrok, sign up at [https://ngrok.com/signup](https://ngrok.com/signup) ## ngrok open-source development ngrok continues to contribute to the open source ecosystem at [https://github.com/ngrok](https://github.com/ngrok) with: - [The ngrok kubernetes operator](https://github.com/ngrok/kubernetes-ingress-controller) - [The ngrok agent SDKs](https://ngrok.com/docs/agent-sdks/) for [Python](https://github.com/ngrok/ngrok-python), [JavaScript](https://github.com/ngrok/ngrok-javascript), [Go](https://github.com/ngrok/ngrok-go), [Rust](https://github.com/ngrok/ngrok-rust) and [Java](https://github.com/ngrok/ngrok-java) ## What is ngrok for? [What can you do with ngrok?](https://ngrok.com/docs/what-is-ngrok/#what-can-you-do-with-ngrok) - Site-to-site Connectivity: Connect securely to APIs and databases in your customers' networks without complex network configuration. - Developer Previews: Demoing an app from your local machine without deploying it - Webhook Testing: Developing any services which consume webhooks (HTTP callbacks) by allowing you to replay those requests - API Gateway: An global gateway-as-a-service that works for API running anywhere with simple CEL-based traffic policy for rate limiting, jwt authentication and more. - Device Gateway: Run ngrok on your IoT devices to control device APIs from your cloud - Debug and understand any web service by inspecting the HTTP traffic to it ================================================ FILE: assets/client/page.html ================================================
{{ txn.Req.MethodPath }} |
{{ txn.Resp.Status }} | {{ txn.Duration }} |
{{ Req.RawText }}
{{ Req.RawBytes }}
{{ Resp.RawText }}
{{ Resp.RawBytes }}
| {{ key }} | ' + '{{ value }} | ' + '
|---|
' +
'Unable to initiate connection to %s. A web server must be running on port %s to complete the tunnel.
` ) type ClientModel struct { log.Logger id string tunnels map[string]mvc.Tunnel serverVersion string metrics *ClientMetrics updateStatus mvc.UpdateStatus connStatus mvc.ConnStatus protoMap map[string]proto.Protocol protocols []proto.Protocol ctl mvc.Controller serverAddr string proxyUrl string authToken string tlsConfig *tls.Config tunnelConfig map[string]*TunnelConfiguration configPath string } 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"]} m := &ClientModel{ Logger: log.NewPrefixLogger("client"), // server address serverAddr: config.ServerAddr, // proxy address proxyUrl: config.HttpProxy, // auth token authToken: config.AuthToken, // connection status connStatus: mvc.ConnConnecting, // update status updateStatus: mvc.UpdateNone, // metrics metrics: NewClientMetrics(), // protocols protoMap: protoMap, // protocol list protocols: protocols, // open tunnels tunnels: make(map[string]mvc.Tunnel), // controller ctl: ctl, // tunnel configuration tunnelConfig: config.Tunnels, // config path configPath: config.Path, } // configure TLS if config.TrustHostRootCerts { m.Info("Trusting host's root certificates") m.tlsConfig = &tls.Config{} } else { m.Info("Trusting root CAs: %v", rootCrtPaths) var err error if m.tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil { panic(err) } } // configure TLS SNI m.tlsConfig.ServerName = serverName(m.serverAddr) m.tlsConfig.InsecureSkipVerify = useInsecureSkipVerify() return m } // server name in release builds is the host part of the server address func serverName(addr string) string { host, _, err := net.SplitHostPort(addr) // should never panic because the config parser calls SplitHostPort first if err != nil { panic(err) } return host } // mvc.State interface 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) { return c.metrics.connMeter, c.metrics.connTimer } func (c ClientModel) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) { return c.metrics.bytesInCount, c.metrics.bytesIn } func (c ClientModel) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) { return c.metrics.bytesOutCount, c.metrics.bytesOut } func (c ClientModel) SetUpdateStatus(updateStatus mvc.UpdateStatus) { c.updateStatus = updateStatus c.update() } // mvc.Model interface func (c *ClientModel) PlayRequest(tunnel mvc.Tunnel, payload []byte) { var localConn conn.Conn localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil) if err != nil { c.Warn("Failed to open private leg to %s: %v", tunnel.LocalAddr, err) return } defer localConn.Close() localConn = tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: "127.0.0.1"}) localConn.Write(payload) ioutil.ReadAll(localConn) } func (c *ClientModel) Shutdown() { } func (c *ClientModel) update() { c.ctl.Update(c) } func (c *ClientModel) Run() { // how long we should wait before we reconnect maxWait := 30 * time.Second wait := 1 * time.Second for { // run the control channel c.control() // control only returns when a failure has occurred, so we're going to try to reconnect if c.connStatus == mvc.ConnOnline { wait = 1 * time.Second } log.Info("Waiting %d seconds before reconnecting", int(wait.Seconds())) time.Sleep(wait) // exponentially increase wait time wait = 2 * wait wait = time.Duration(math.Min(float64(wait), float64(maxWait))) c.connStatus = mvc.ConnReconnecting c.update() } } // Establishes and manages a tunnel control connection with the server func (c *ClientModel) control() { defer func() { if r := recover(); r != nil { log.Error("control recovering from failure %v", r) } }() // establish control channel var ( ctlConn conn.Conn err error ) if c.proxyUrl == "" { // simple non-proxied case, just connect to the server ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig) } else { ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig) } if err != nil { panic(err) } defer ctlConn.Close() // authenticate with the server auth := &msg.Auth{ ClientId: c.id, OS: runtime.GOOS, Arch: runtime.GOARCH, Version: version.Proto, MmVersion: version.MajorMinor(), User: c.authToken, } 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(ctlConn, &authResp); err != nil { panic(err) } if authResp.Error != "" { emsg := fmt.Sprintf("Failed to authenticate to server: %s", authResp.Error) c.ctl.Shutdown(emsg) return } c.id = authResp.ClientId c.serverVersion = authResp.MmVersion c.Info("Authenticated with server, client id: %v", c.id) c.update() if err = SaveAuthToken(c.configPath, c.authToken); err != nil { c.Error("Failed to save auth token: %v", 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, RemotePort: config.RemotePort, } // 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 lastPong := time.Now().UnixNano() c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) }) // main control loop for { var rawMsg msg.Message if rawMsg, err = msg.ReadMsg(ctlConn); err != nil { panic(err) } switch m := rawMsg.(type) { case *msg.ReqProxy: c.ctl.Go(c.proxy) case *msg.Pong: atomic.StoreInt64(&lastPong, time.Now().UnixNano()) case *msg.NewTunnel: if m.Error != "" { emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", m.Error) c.Error(emsg) c.ctl.Shutdown(emsg) continue } tunnel := mvc.Tunnel{ PublicUrl: m.Url, LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol], Protocol: c.protoMap[m.Protocol], } c.tunnels[tunnel.PublicUrl] = tunnel c.connStatus = mvc.ConnOnline c.Info("Tunnel established at %v", tunnel.PublicUrl) c.update() default: ctlConn.Warn("Ignoring unknown control message %v ", m) } } } // Establishes and manages a tunnel proxy connection with the server func (c *ClientModel) proxy() { var ( remoteConn conn.Conn err error ) if c.proxyUrl == "" { remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig) } else { remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.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 { remoteConn.Error("Failed to write RegProxy: %v", err) return } // wait for the server to ack our register var startPxy msg.StartProxy if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil { remoteConn.Error("Server failed to write StartProxy: %v", err) return } tunnel, ok := c.tunnels[startPxy.Url] if !ok { remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url) return } // start up the private connection start := time.Now() localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil) if err != nil { 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() m := c.metrics m.proxySetupTimer.Update(time.Since(start)) m.connMeter.Mark(1) c.update() m.connTimer.Time(func() { localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxy.ClientAddr}) bytesIn, bytesOut := conn.Join(localConn, remoteConn) m.bytesIn.Update(bytesIn) m.bytesOut.Update(bytesOut) m.bytesInCount.Inc(bytesIn) m.bytesOutCount.Inc(bytesOut) }) c.update() } // Hearbeating to ensure our connection ngrokd is still live func (c *ClientModel) heartbeat(lastPongAddr *int64, conn conn.Conn) { lastPing := time.Unix(atomic.LoadInt64(lastPongAddr)-1, 0) ping := time.NewTicker(pingInterval) pongCheck := time.NewTicker(time.Second) defer func() { conn.Close() ping.Stop() pongCheck.Stop() }() for { select { case <-pongCheck.C: lastPong := time.Unix(0, atomic.LoadInt64(lastPongAddr)) needPong := lastPong.Sub(lastPing) < 0 pongLatency := time.Since(lastPing) if needPong && pongLatency > maxPongLatency { c.Info("Last ping: %v, Last pong: %v", lastPing, lastPong) c.Info("Connection stale, haven't gotten PongMsg in %d seconds", int(pongLatency.Seconds())) return } case <-ping.C: err := msg.WriteMsg(conn, &msg.Ping{}) if err != nil { conn.Debug("Got error %v when writing PingMsg", err) return } lastPing = time.Now() } } } ================================================ FILE: src/ngrok/client/mvc/controller.go ================================================ package mvc import ( "ngrok/util" ) type Controller interface { // how the model communicates that it has changed state Update(State) // instructs the controller to shut the app down Shutdown(message string) // PlayRequest instructs the model to play requests 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 address where the web inspection interface is running GetWebInspectAddr() string } ================================================ FILE: src/ngrok/client/mvc/model.go ================================================ package mvc type Model interface { Run() Shutdown() PlayRequest(tunnel Tunnel, payload []byte) } ================================================ FILE: src/ngrok/client/mvc/state.go ================================================ package mvc import ( metrics "github.com/rcrowley/go-metrics" "ngrok/proto" ) type UpdateStatus int const ( UpdateNone = -1 * iota UpdateInstalling UpdateReady UpdateAvailable ) type ConnStatus int const ( ConnConnecting = iota ConnReconnecting ConnOnline ) type Tunnel struct { PublicUrl string Protocol proto.Protocol LocalAddr string } type ConnectionContext struct { Tunnel Tunnel ClientAddr string } type State interface { GetClientVersion() string GetServerVersion() string GetTunnels() []Tunnel GetProtocols() []proto.Protocol GetUpdateStatus() UpdateStatus GetConnStatus() ConnStatus GetConnectionMetrics() (metrics.Meter, metrics.Timer) GetBytesInMetrics() (metrics.Counter, metrics.Histogram) GetBytesOutMetrics() (metrics.Counter, metrics.Histogram) SetUpdateStatus(UpdateStatus) } ================================================ FILE: src/ngrok/client/mvc/view.go ================================================ package mvc type View interface { Shutdown() } ================================================ FILE: src/ngrok/client/release.go ================================================ // +build release package client var ( rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"} ) func useInsecureSkipVerify() bool { return false } ================================================ FILE: src/ngrok/client/tls.go ================================================ package client import ( _ "crypto/sha512" "crypto/tls" "crypto/x509" "encoding/pem" "fmt" "ngrok/client/assets" ) func LoadTLSConfig(rootCertPaths []string) (*tls.Config, error) { pool := x509.NewCertPool() for _, certPath := range rootCertPaths { rootCrt, err := assets.Asset(certPath) if err != nil { return nil, err } pemBlock, _ := pem.Decode(rootCrt) if pemBlock == nil { return nil, fmt.Errorf("Bad PEM data") } certs, err := x509.ParseCertificates(pemBlock.Bytes) if err != nil { return nil, err } pool.AddCert(certs[0]) } return &tls.Config{RootCAs: pool}, nil } ================================================ FILE: src/ngrok/client/update_debug.go ================================================ // +build !release,!autoupdate package client import ( "ngrok/client/mvc" ) // no auto-updating in debug mode func autoUpdate(state mvc.State, token string) { } ================================================ FILE: src/ngrok/client/update_release.go ================================================ // +build release autoupdate package client import ( "ngrok/client/mvc" "ngrok/log" "ngrok/version" "time" "gopkg.in/inconshreveable/go-update.v0" "gopkg.in/inconshreveable/go-update.v0/check" ) const ( appId = "ap_pJSFC5wQYkAyI0FIVwKYs9h1hW" updateEndpoint = "https://api.equinox.io/1/Updates" ) const publicKey = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Gx8r9no1QBtCruJW2tu 082MJJ5ZA7k803GisR2c6WglPOD1b/+kUg+dx5Y0TKXz+uNlR3GrCxLh8WkoA95M T38CQldIjoVN/bWP6jzFxL+6BRoKy5L1TcaIf3xb9B8OhwEq60cvFy7BBrLKEHJN ua/D1S5axgNOAJ8tQ2w8gISICd84ng+U9tNMqIcEjUN89h3Z4zablfNIfVkbqbSR fnkR9boUaMr6S1w8OeInjWdiab9sUr87GmEo/3tVxrHVCzHB8pzzoZceCkjgI551 d/hHfAl567YhlkQMNz8dawxBjQwCHHekgC8gAvTO7kmXkAm6YAbpa9kjwgnorPEP ywIDAQAB -----END PUBLIC KEY-----` func autoUpdate(s mvc.State, token string) { up, err := update.New().VerifySignatureWithPEM([]byte(publicKey)) if err != nil { log.Error("Failed to create update with signature: %v", err) return } update := func() (tryAgain bool) { log.Info("Checking for update") params := check.Params{ AppId: appId, AppVersion: version.MajorMinor(), UserId: token, } result, err := params.CheckForUpdate(updateEndpoint, up) if err == check.NoUpdateAvailable { log.Info("No update available") return true } else if err != nil { log.Error("Error while checking for update: %v", err) return true } if result.Initiative == check.INITIATIVE_AUTO { if err := up.CanUpdate(); err != nil { log.Error("Can't update: insufficient permissions: %v", err) // tell the user to update manually s.SetUpdateStatus(mvc.UpdateAvailable) } else { applyUpdate(s, result) } } else if result.Initiative == check.INITIATIVE_MANUAL { // this is the way the server tells us to update manually log.Info("Server wants us to update manually") s.SetUpdateStatus(mvc.UpdateAvailable) } else { log.Info("Update available, but ignoring") } // stop trying after a single download attempt // XXX: improve this so the we can: // 1. safely update multiple times // 2. only retry after temporary errors return false } // try to update immediately and then at a set interval for { if tryAgain := update(); !tryAgain { break } time.Sleep(updateCheckInterval) } } func applyUpdate(s mvc.State, result *check.Result) { err, errRecover := result.Update() if err == nil { log.Info("Update ready!") s.SetUpdateStatus(mvc.UpdateReady) return } log.Error("Error while updating ngrok: %v", err) if errRecover != nil { log.Error("Error while recovering from failed ngrok update, your binary may be missing: %v", errRecover.Error()) } // tell the user to update manually s.SetUpdateStatus(mvc.UpdateAvailable) } ================================================ FILE: src/ngrok/client/views/term/area.go ================================================ // shared internal functions for handling output to the terminal package term import ( "fmt" termbox "github.com/nsf/termbox-go" ) const ( fgColor = termbox.ColorWhite bgColor = termbox.ColorDefault ) type area struct { // top-left corner x, y int // size of the area w, h int // default colors fgColor, bgColor termbox.Attribute } func NewArea(x, y, w, h int) *area { return &area{x, y, w, h, fgColor, bgColor} } func (a *area) Clear() { for i := 0; i < a.w; i++ { for j := 0; j < a.h; j++ { termbox.SetCell(a.x+i, a.y+j, ' ', a.fgColor, a.bgColor) } } } func (a *area) APrintf(fg termbox.Attribute, x, y int, arg0 string, args ...interface{}) { s := fmt.Sprintf(arg0, args...) for i, ch := range s { termbox.SetCell(a.x+x+i, a.y+y, ch, fg, bgColor) } } func (a *area) Printf(x, y int, arg0 string, args ...interface{}) { a.APrintf(a.fgColor, x, y, arg0, args...) } ================================================ FILE: src/ngrok/client/views/term/http.go ================================================ package term import ( termbox "github.com/nsf/termbox-go" "ngrok/client/mvc" "ngrok/log" "ngrok/proto" "ngrok/util" "unicode/utf8" ) const ( size = 10 pathMaxLength = 25 ) type HttpView struct { log.Logger *area httpProto *proto.Http HttpRequests *util.Ring shutdown chan int termView *TermView } func colorFor(status string) termbox.Attribute { switch status[0] { case '3': return termbox.ColorCyan case '4': return termbox.ColorYellow case '5': return termbox.ColorRed default: } return termbox.ColorWhite } 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: make(chan int), termView: termView, Logger: log.NewPrefixLogger("view", "term", "http"), } ctl.Go(v.Run) return v } func (v *HttpView) Run() { updates := v.httpProto.Txns.Reg() for { select { case txn := <-updates: v.Debug("Got HTTP update") if txn.(*proto.HttpTxn).Resp == nil { v.HttpRequests.Add(txn) } v.Render() } } } func (v *HttpView) Render() { v.Clear() v.Printf(0, 0, "HTTP Requests") v.Printf(0, 1, "-------------") for i, obj := range v.HttpRequests.Slice() { txn := obj.(*proto.HttpTxn) path := truncatePath(txn.Req.URL.Path) v.Printf(0, 3+i, "%s %v", txn.Req.Method, path) if txn.Resp != nil { v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status) } } v.termView.Flush() } func (v *HttpView) Shutdown() { close(v.shutdown) } func truncatePath(path string) string { // Truncate all long strings based on rune count if utf8.RuneCountInString(path) > pathMaxLength { path = string([]rune(path)[:pathMaxLength]) } // By this point, len(path) should be < pathMaxLength if we're dealing with single-byte runes. // Otherwise, we have a multi-byte string and need to calculate the size of each rune and // truncate manually. // // This is a workaround for a bug in termbox-go. Remove it when this issue is fixed: // https://github.com/nsf/termbox-go/pull/21 if len(path) > pathMaxLength { out := make([]byte, pathMaxLength, pathMaxLength) length := 0 for { r, size := utf8.DecodeRuneInString(path[length:]) if r == utf8.RuneError && size == 1 { break } // utf8.EncodeRune expects there to be enough room to store the full size of the rune if length+size <= pathMaxLength { utf8.EncodeRune(out[length:], r) length += size } else { break } } path = string(out[:length]) } return path } ================================================ FILE: src/ngrok/client/views/term/view.go ================================================ // interactive terminal interface for local clients package term import ( termbox "github.com/nsf/termbox-go" "ngrok/client/mvc" "ngrok/log" "ngrok/proto" "ngrok/util" "time" ) type TermView struct { ctl mvc.Controller updates chan interface{} flush chan int shutdown chan int redraw *util.Broadcast subviews []mvc.View log.Logger *area } func NewTermView(ctl mvc.Controller) *TermView { // initialize terminal display termbox.Init() w, _ := termbox.Size() v := &TermView{ ctl: ctl, updates: ctl.Updates().Reg(), redraw: util.NewBroadcast(), flush: make(chan int), shutdown: make(chan int), Logger: log.NewPrefixLogger("view", "term"), area: NewArea(0, 0, w, 10), } ctl.Go(v.run) ctl.Go(v.input) return v } func connStatusRepr(status mvc.ConnStatus) (string, termbox.Attribute) { switch status { case mvc.ConnConnecting: return "connecting", termbox.ColorCyan case mvc.ConnReconnecting: return "reconnecting", termbox.ColorRed case mvc.ConnOnline: return "online", termbox.ColorGreen } return "unknown", termbox.ColorWhite } func (v *TermView) draw() { state := v.ctl.State() v.Clear() // quit instructions quitMsg := "(Ctrl+C to quit)" v.Printf(v.w-len(quitMsg), 0, quitMsg) // new version message updateStatus := state.GetUpdateStatus() var updateMsg string switch updateStatus { case mvc.UpdateNone: updateMsg = "" case mvc.UpdateInstalling: updateMsg = "ngrok is updating" case mvc.UpdateReady: updateMsg = "ngrok has updated: restart ngrok for the new version" case mvc.UpdateAvailable: updateMsg = "new version available at https://ngrok.com" default: pct := float64(updateStatus) / 100.0 const barLength = 25 full := int(barLength * pct) bar := make([]byte, barLength+2) bar[0] = '[' bar[barLength+1] = ']' for i := 0; i < 25; i++ { if i <= full { bar[i+1] = '#' } else { bar[i+1] = ' ' } } updateMsg = "Downloading update: " + string(bar) } if updateMsg != "" { v.APrintf(termbox.ColorYellow, 30, 0, updateMsg) } 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++ } 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()) msec := float64(time.Millisecond) v.Printf(0, i+2, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec) termbox.Flush() } func (v *TermView) run() { 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 <-v.updates: v.draw() case <-redraw: v.draw() case <-v.shutdown: return } } } 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() { for { ev := termbox.PollEvent() switch ev.Type { case termbox.EventKey: switch ev.Key { case termbox.KeyCtrlC: v.Info("Got quit command") v.ctl.Shutdown("") } case termbox.EventResize: v.Info("Resize event, redrawing") v.redraw.In() <- 1 case termbox.EventError: panic(ev.Err) } } } ================================================ FILE: src/ngrok/client/views/web/http.go ================================================ // interactive web user interface package web import ( "encoding/base64" "encoding/json" "encoding/xml" "html/template" "net/http" "net/http/httputil" "net/url" "ngrok/client/assets" "ngrok/client/mvc" "ngrok/log" "ngrok/proto" "ngrok/util" "strings" "unicode/utf8" ) type SerializedTxn struct { Id string Duration int64 Start int64 ConnCtx mvc.ConnectionContext *proto.HttpTxn `json:"-"` Req SerializedRequest Resp SerializedResponse } type SerializedBody struct { RawContentType string ContentType string Text string Length int Error string ErrorOffset int Form url.Values } type SerializedRequest struct { Raw string MethodPath string Params url.Values Header http.Header Body SerializedBody Binary bool } type SerializedResponse struct { Raw string Status string Header http.Header Body SerializedBody Binary bool } type WebHttpView struct { log.Logger webview *WebView ctl mvc.Controller httpProto *proto.Http state chan SerializedUiState HttpRequests *util.Ring idToTxn map[string]*SerializedTxn } type SerializedUiState struct { Tunnels []mvc.Tunnel } type SerializedPayload struct { Txns []interface{} UiState SerializedUiState } 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), HttpRequests: util.NewRing(20), } ctl.Go(whv.updateHttp) whv.register() return whv } type XMLDoc struct { data []byte `xml:",innerxml"` } func makeBody(h http.Header, body []byte) SerializedBody { b := SerializedBody{ Length: len(body), Text: base64.StdEncoding.EncodeToString(body), ErrorOffset: -1, } // some errors like XML errors only give a line number // and not an exact offset offsetForLine := func(line int) int { lines := strings.SplitAfterN(b.Text, "\n", line) return b.Length - len(lines[len(lines)-1]) } var err error b.RawContentType = h.Get("Content-Type") if b.RawContentType != "" { b.ContentType = strings.TrimSpace(strings.Split(b.RawContentType, ";")[0]) switch b.ContentType { case "application/xml", "text/xml": err = xml.Unmarshal(body, new(XMLDoc)) if err != nil { if syntaxError, ok := err.(*xml.SyntaxError); ok { // xml syntax errors only give us a line number, so we // count to find an offset b.ErrorOffset = offsetForLine(syntaxError.Line) } } case "application/json": err = json.Unmarshal(body, new(json.RawMessage)) if err != nil { if syntaxError, ok := err.(*json.SyntaxError); ok { b.ErrorOffset = int(syntaxError.Offset) } } case "application/x-www-form-urlencoded": b.Form, err = url.ParseQuery(string(body)) } } if err != nil { b.Error = err.Error() } return b } func (whv *WebHttpView) updateHttp() { // open channels for incoming http state changes // and broadcasts txnUpdates := whv.httpProto.Txns.Reg() for txn := range txnUpdates { // XXX: it's not safe for proto.Http and this code // to be accessing txn and txn.(req/resp) without synchronization htxn := txn.(*proto.HttpTxn) // we haven't processed this transaction yet if we haven't set the // user data if htxn.UserCtx == nil { rawReq, err := proto.DumpRequestOut(htxn.Req.Request, true) if err != nil { whv.Error("Failed to dump request: %v", err) continue } body := makeBody(htxn.Req.Header, htxn.Req.BodyBytes) whtxn := &SerializedTxn{ Id: util.RandId(8), HttpTxn: htxn, Req: SerializedRequest{ MethodPath: htxn.Req.Method + " " + htxn.Req.URL.Path, Raw: base64.StdEncoding.EncodeToString(rawReq), Params: htxn.Req.URL.Query(), Header: htxn.Req.Header, Body: body, Binary: !utf8.Valid(rawReq), }, Start: htxn.Start.Unix(), ConnCtx: htxn.ConnUserCtx.(mvc.ConnectionContext), } htxn.UserCtx = whtxn // XXX: unsafe map access from multiple go routines whv.idToTxn[whtxn.Id] = whtxn // XXX: use return value to delete from map so we don't leak memory whv.HttpRequests.Add(whtxn) } else { rawResp, err := httputil.DumpResponse(htxn.Resp.Response, true) if err != nil { whv.Error("Failed to dump response: %v", err) continue } txn := htxn.UserCtx.(*SerializedTxn) body := makeBody(htxn.Resp.Header, htxn.Resp.BodyBytes) txn.Duration = htxn.Duration.Nanoseconds() txn.Resp = SerializedResponse{ Status: htxn.Resp.Status, Raw: base64.StdEncoding.EncodeToString(rawResp), Header: htxn.Resp.Header, Body: body, Binary: !utf8.Valid(rawResp), } payload, err := json.Marshal(txn) if err != nil { whv.Error("Failed to serialized txn payload for websocket: %v", err) } whv.webview.wsMessages.In() <- payload } } } func (whv *WebHttpView) register() { http.HandleFunc("/http/in/replay", func(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { err := util.MakePanicTrace(r) whv.Error("Replay failed: %v", err) http.Error(w, err, 500) } }() r.ParseForm() txnid := r.Form.Get("txnid") if txn, ok := whv.idToTxn[txnid]; ok { reqBytes, err := base64.StdEncoding.DecodeString(txn.Req.Raw) if err != nil { panic(err) } whv.ctl.PlayRequest(txn.ConnCtx.Tunnel, reqBytes) w.Write([]byte(http.StatusText(200))) } else { http.Error(w, http.StatusText(400), 400) } }) http.HandleFunc("/http/in", func(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { err := util.MakePanicTrace(r) whv.Error("HTTP web view failed: %v", err) http.Error(w, err, 500) } }() pageTmpl, err := assets.Asset("assets/client/page.html") if err != nil { panic(err) } tmpl := template.Must(template.New("page.html").Delims("{%", "%}").Parse(string(pageTmpl))) payloadData := SerializedPayload{ Txns: whv.HttpRequests.Slice(), UiState: SerializedUiState{Tunnels: whv.ctl.State().GetTunnels()}, } payload, err := json.Marshal(payloadData) if err != nil { panic(err) } // write the response if err := tmpl.Execute(w, string(payload)); err != nil { panic(err) } }) } func (whv *WebHttpView) Shutdown() { } ================================================ FILE: src/ngrok/client/views/web/view.go ================================================ // interactive web user interface package web import ( "github.com/gorilla/websocket" "net/http" "ngrok/client/assets" "ngrok/client/mvc" "ngrok/log" "ngrok/proto" "ngrok/util" "path" ) type WebView struct { log.Logger ctl mvc.Controller // messages sent over this broadcast are sent to all websocket connections wsMessages *util.Broadcast } func NewWebView(ctl mvc.Controller, addr string) *WebView { wv := &WebView{ Logger: log.NewPrefixLogger("view", "web"), wsMessages: util.NewBroadcast(), ctl: ctl, } // 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, nil, 1024, 1024) if err != nil { http.Error(w, "Failed websocket upgrade", 400) wv.Warn("Failed websocket upgrade: %v", err) return } msgs := wv.wsMessages.Reg() defer wv.wsMessages.UnReg(msgs) for m := range msgs { err := conn.WriteMessage(websocket.TextMessage, m.([]byte)) if err != nil { // connection is closed break } } }) // serve static assets http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { buf, err := assets.Asset(path.Join("assets", "client", r.URL.Path[1:])) if err != nil { wv.Warn("Error serving static file: %s", err.Error()) http.NotFound(w, r) return } w.Write(buf) }) wv.Info("Serving web interface on %s", addr) wv.ctl.Go(func() { http.ListenAndServe(addr, nil) }) return wv } func (wv *WebView) NewHttpView(proto *proto.Http) *WebHttpView { return newWebHttpView(wv.ctl, wv, proto) } func (wv *WebView) Shutdown() { } ================================================ FILE: src/ngrok/conn/conn.go ================================================ package conn import ( "bufio" "crypto/tls" "encoding/base64" "fmt" vhost "github.com/inconshreveable/go-vhost" "io" "math/rand" "net" "net/http" "net/url" "ngrok/log" "sync" ) type Conn interface { net.Conn log.Logger Id() string SetType(string) CloseRead() error } type loggedConn struct { tcp *net.TCPConn net.Conn log.Logger id int32 typ string } type Listener struct { net.Addr Conns chan *loggedConn } func wrapConn(conn net.Conn, typ string) *loggedConn { switch c := conn.(type) { case *vhost.HTTPConn: wrapped := c.Conn.(*loggedConn) return &loggedConn{wrapped.tcp, conn, wrapped.Logger, wrapped.id, wrapped.typ} case *loggedConn: return c case *net.TCPConn: wrapped := &loggedConn{c, conn, log.NewPrefixLogger(), rand.Int31(), typ} wrapped.AddLogPrefix(wrapped.Id()) return wrapped } return nil } func Listen(addr, typ string, tlsCfg *tls.Config) (l *Listener, err error) { // listen for incoming connections listener, err := net.Listen("tcp", addr) if err != nil { return } l = &Listener{ Addr: listener.Addr(), Conns: make(chan *loggedConn), } go func() { for { rawConn, err := listener.Accept() if err != nil { log.Error("Failed to accept new TCP connection of type %s: %v", typ, err) continue } c := wrapConn(rawConn, typ) if tlsCfg != nil { c.Conn = tls.Server(c.Conn, tlsCfg) } c.Info("New connection from %v", c.RemoteAddr()) l.Conns <- c } }() return } func Wrap(conn net.Conn, typ string) *loggedConn { return wrapConn(conn, typ) } func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) { var rawConn net.Conn if rawConn, err = net.Dial("tcp", addr); err != nil { return } conn = wrapConn(rawConn, typ) conn.Debug("New connection to: %v", rawConn.RemoteAddr()) if tlsCfg != nil { conn.StartTLS(tlsCfg) } return } 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())) } 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(parsedUrl.Host, typ, proxyTlsConfig); err != nil { return } // send an HTTP proxy CONNECT message req, err := http.NewRequest("CONNECT", "https://"+addr, nil) if err != nil { return } if proxyAuth != "" { req.Header.Set("Proxy-Authorization", proxyAuth) } 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() (err error) { if err := c.Conn.Close(); err == nil { c.Debug("Closing") } return } func (c *loggedConn) Id() string { return fmt.Sprintf("%s:%x", c.typ, c.id) } func (c *loggedConn) SetType(typ string) { oldId := c.Id() c.typ = typ c.ClearLogPrefixes() c.AddLogPrefix(c.Id()) c.Info("Renamed connection %s", oldId) } func (c *loggedConn) CloseRead() error { // XXX: use CloseRead() in Conn.Join() and in Control.shutdown() for cleaner // connection termination. Unfortunately, when I've tried that, I've observed // failures where the connection was closed *before* flushing its write buffer, // set with SetLinger() set properly (which it is by default). return c.tcp.CloseRead() } func Join(c Conn, c2 Conn) (int64, int64) { var wait sync.WaitGroup pipe := func(to Conn, from Conn, bytesCopied *int64) { defer to.Close() defer from.Close() defer wait.Done() var err error *bytesCopied, err = io.Copy(to, from) if err != nil { from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err) } else { from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id()) } } wait.Add(2) var fromBytes, toBytes int64 go pipe(c, c2, &fromBytes) go pipe(c2, c, &toBytes) c.Info("Joined with connection %s", c2.Id()) wait.Wait() return fromBytes, toBytes } ================================================ FILE: src/ngrok/conn/tee.go ================================================ package conn import ( "bufio" "io" ) // conn.Tee is a wraps a conn.Conn // causing all writes/reads to be tee'd just // like the unix command such that all data that // is read and written to the connection through its // interfaces will also be copied into two dedicated pipes // used for consuming a copy of the data stream // // this is useful for introspecting the traffic flowing // over a connection without having to tamper with the actual // code that reads and writes over the connection // // NB: the data is Tee'd into a shared-memory io.Pipe which // has a limited (and small) buffer. If you are not consuming from // the ReadBuffer() and WriteBuffer(), you are going to block // your application's real traffic from flowing over the connection type Tee struct { rd io.Reader wr io.Writer readPipe struct { rd *io.PipeReader wr *io.PipeWriter } writePipe struct { rd *io.PipeReader wr *io.PipeWriter } Conn } func NewTee(conn Conn) *Tee { c := &Tee{ rd: nil, wr: nil, Conn: conn, } c.readPipe.rd, c.readPipe.wr = io.Pipe() c.writePipe.rd, c.writePipe.wr = io.Pipe() c.rd = io.TeeReader(c.Conn, c.readPipe.wr) c.wr = io.MultiWriter(c.Conn, c.writePipe.wr) return c } func (c *Tee) ReadBuffer() *bufio.Reader { return bufio.NewReader(c.readPipe.rd) } func (c *Tee) WriteBuffer() *bufio.Reader { return bufio.NewReader(c.writePipe.rd) } func (c *Tee) Read(b []byte) (n int, err error) { n, err = c.rd.Read(b) if err != nil { c.readPipe.wr.Close() } return } func (c *Tee) ReadFrom(r io.Reader) (n int64, err error) { n, err = io.Copy(c.wr, r) if err != nil { c.writePipe.wr.Close() } return } func (c *Tee) Write(b []byte) (n int, err error) { n, err = c.wr.Write(b) if err != nil { c.writePipe.wr.Close() } return } ================================================ FILE: src/ngrok/log/logger.go ================================================ package log import ( "fmt" log "github.com/alecthomas/log4go" ) var root log.Logger = make(log.Logger) func LogTo(target string, level_name string) { var writer log.LogWriter = nil switch target { case "stdout": writer = log.NewConsoleLogWriter() case "none": // no logging default: writer = log.NewFileLogWriter(target, true) } if writer != nil { var level = log.DEBUG switch level_name { case "FINEST": level = log.FINEST case "FINE": level = log.FINE case "DEBUG": level = log.DEBUG case "TRACE": level = log.TRACE case "INFO": level = log.INFO case "WARNING": level = log.WARNING case "ERROR": level = log.ERROR case "CRITICAL": level = log.CRITICAL default: level = log.DEBUG } root.AddFilter("log", level, writer) } } type Logger interface { AddLogPrefix(string) ClearLogPrefixes() Debug(string, ...interface{}) Info(string, ...interface{}) Warn(string, ...interface{}) error Error(string, ...interface{}) error } type PrefixLogger struct { *log.Logger prefix string } func NewPrefixLogger(prefixes ...string) Logger { logger := &PrefixLogger{Logger: &root} for _, p := range prefixes { logger.AddLogPrefix(p) } return logger } func (pl *PrefixLogger) pfx(fmtstr string) interface{} { return fmt.Sprintf("%s %s", pl.prefix, fmtstr) } func (pl *PrefixLogger) Debug(arg0 string, args ...interface{}) { pl.Logger.Debug(pl.pfx(arg0), args...) } func (pl *PrefixLogger) Info(arg0 string, args ...interface{}) { pl.Logger.Info(pl.pfx(arg0), args...) } func (pl *PrefixLogger) Warn(arg0 string, args ...interface{}) error { return pl.Logger.Warn(pl.pfx(arg0), args...) } func (pl *PrefixLogger) Error(arg0 string, args ...interface{}) error { return pl.Logger.Error(pl.pfx(arg0), args...) } func (pl *PrefixLogger) AddLogPrefix(prefix string) { if len(pl.prefix) > 0 { pl.prefix += " " } pl.prefix += "[" + prefix + "]" } func (pl *PrefixLogger) ClearLogPrefixes() { pl.prefix = "" } // we should never really use these . . . always prefer logging through a prefix logger func Debug(arg0 string, args ...interface{}) { root.Debug(arg0, args...) } func Info(arg0 string, args ...interface{}) { root.Info(arg0, args...) } func Warn(arg0 string, args ...interface{}) error { return root.Warn(arg0, args...) } func Error(arg0 string, args ...interface{}) error { return root.Error(arg0, args...) } ================================================ FILE: src/ngrok/main/ngrok/ngrok.go ================================================ package main import ( "ngrok/client" ) func main() { client.Main() } ================================================ FILE: src/ngrok/main/ngrokd/ngrokd.go ================================================ package main import ( "ngrok/server" ) func main() { server.Main() } ================================================ FILE: src/ngrok/msg/conn.go ================================================ package msg import ( "encoding/binary" "errors" "fmt" "ngrok/conn" ) func readMsgShared(c conn.Conn) (buffer []byte, err error) { c.Debug("Waiting to read message") var sz int64 err = binary.Read(c, binary.LittleEndian, &sz) if err != nil { return } c.Debug("Reading message with length: %d", sz) buffer = make([]byte, sz) n, err := c.Read(buffer) c.Debug("Read message %s", buffer) if err != nil { return } if int64(n) != sz { err = errors.New(fmt.Sprintf("Expected to read %d bytes, but only read %d", sz, n)) return } return } func ReadMsg(c conn.Conn) (msg Message, err error) { buffer, err := readMsgShared(c) if err != nil { return } return Unpack(buffer) } func ReadMsgInto(c conn.Conn, msg Message) (err error) { buffer, err := readMsgShared(c) if err != nil { return } return UnpackInto(buffer, msg) } func WriteMsg(c conn.Conn, msg interface{}) (err error) { buffer, err := Pack(msg) if err != nil { return } c.Debug("Writing message: %s", string(buffer)) err = binary.Write(c, binary.LittleEndian, int64(len(buffer))) if err != nil { return } if _, err = c.Write(buffer); err != nil { return } return nil } ================================================ FILE: src/ngrok/msg/msg.go ================================================ package msg import ( "encoding/json" "reflect" ) var TypeMap map[string]reflect.Type func init() { TypeMap = make(map[string]reflect.Type) t := func(obj interface{}) reflect.Type { return reflect.TypeOf(obj).Elem() } TypeMap["Auth"] = t((*Auth)(nil)) TypeMap["AuthResp"] = t((*AuthResp)(nil)) TypeMap["ReqTunnel"] = t((*ReqTunnel)(nil)) TypeMap["NewTunnel"] = t((*NewTunnel)(nil)) TypeMap["RegProxy"] = t((*RegProxy)(nil)) TypeMap["ReqProxy"] = t((*ReqProxy)(nil)) TypeMap["StartProxy"] = t((*StartProxy)(nil)) TypeMap["Ping"] = t((*Ping)(nil)) TypeMap["Pong"] = t((*Pong)(nil)) } type Message interface{} type Envelope struct { Type string Payload json.RawMessage } // When a client opens a new control channel to the server // it must start by sending an Auth message. type Auth struct { Version string // protocol version MmVersion string // major/minor software version (informational only) User string Password string OS string Arch string ClientId string // empty for new sessions } // A server responds to an Auth message with an // AuthResp message over the control channel. // // If Error is not the empty string // the server has indicated it will not accept // the new session and will close the connection. // // The server response includes a unique ClientId // that is used to associate and authenticate future // proxy connections via the same field in RegProxy messages. type AuthResp struct { Version string MmVersion string ClientId string Error string } // A client sends this message to the server over the control channel // to request a new tunnel be opened on the client's behalf. // ReqId is a random number set by the client that it can pull // from future NewTunnel's to correlate then to the requesting ReqTunnel. type ReqTunnel struct { ReqId string Protocol string // http only Hostname string Subdomain string HttpAuth string // tcp only RemotePort uint16 } // When the server opens a new tunnel on behalf of // a client, it sends a NewTunnel message to notify the client. // ReqId is the ReqId from the corresponding ReqTunnel message. // // A client may receive *multiple* NewTunnel messages from a single // ReqTunnel. (ex. A client opens an https tunnel and the server // chooses to open an http tunnel of the same name as well) type NewTunnel struct { ReqId string Url string Protocol string Error string } // When the server wants to initiate a new tunneled connection, it sends // this message over the control channel to the client. When a client receives // this message, it must initiate a new proxy connection to the server. type ReqProxy struct { } // After a client receives a ReqProxy message, it opens a new // connection to the server and sends a RegProxy message. type RegProxy struct { ClientId string } // This message is sent by the server to the client over a *proxy* connection before it // begins to send the bytes of the proxied request. type StartProxy struct { Url string // URL of the tunnel this connection connection is being proxied for ClientAddr string // Network address of the client initiating the connection to the tunnel } // A client or server may send this message periodically over // the control channel to request that the remote side acknowledge // its connection is still alive. The remote side must respond with a Pong. type Ping struct { } // Sent by a client or server over the control channel to indicate // it received a Ping. type Pong struct { } ================================================ FILE: src/ngrok/msg/pack.go ================================================ package msg import ( "encoding/json" "errors" "fmt" "reflect" ) func unpack(buffer []byte, msgIn Message) (msg Message, err error) { var env Envelope if err = json.Unmarshal(buffer, &env); err != nil { return } if msgIn == nil { t, ok := TypeMap[env.Type] if !ok { err = errors.New(fmt.Sprintf("Unsupported message type %s", env.Type)) return } // guess type msg = reflect.New(t).Interface().(Message) } else { msg = msgIn } err = json.Unmarshal(env.Payload, &msg) return } func UnpackInto(buffer []byte, msg Message) (err error) { _, err = unpack(buffer, msg) return } func Unpack(buffer []byte) (msg Message, err error) { return unpack(buffer, nil) } func Pack(payload interface{}) ([]byte, error) { return json.Marshal(struct { Type string Payload interface{} }{ Type: reflect.TypeOf(payload).Elem().Name(), Payload: payload, }) } ================================================ FILE: src/ngrok/proto/http.go ================================================ package proto import ( "bufio" "bytes" "io" "io/ioutil" "net" "net/http" "net/http/httputil" "net/url" "ngrok/conn" "ngrok/util" "strings" "sync" "time" metrics "github.com/rcrowley/go-metrics" ) type HttpRequest struct { *http.Request BodyBytes []byte } type HttpResponse struct { *http.Response BodyBytes []byte } type HttpTxn struct { Req *HttpRequest Resp *HttpResponse Start time.Time Duration time.Duration UserCtx interface{} ConnUserCtx interface{} } type Http struct { Txns *util.Broadcast reqGauge metrics.Gauge reqMeter metrics.Meter reqTimer metrics.Timer } func NewHttp() *Http { return &Http{ Txns: util.NewBroadcast(), reqGauge: metrics.NewGauge(), reqMeter: metrics.NewMeter(), reqTimer: metrics.NewTimer(), } } func extractBody(r io.Reader) ([]byte, io.ReadCloser, error) { buf := new(bytes.Buffer) _, err := buf.ReadFrom(r) return buf.Bytes(), ioutil.NopCloser(buf), err } func (h *Http) GetName() string { return "http" } func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn { tee := conn.NewTee(c) lastTxn := make(chan *HttpTxn) go h.readRequests(tee, lastTxn, ctx) go h.readResponses(tee, lastTxn) return tee } func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) { defer close(lastTxn) for { req, err := http.ReadRequest(tee.WriteBuffer()) if err != nil { // no more requests to be read, we're done break } // make sure we read the body of the request so that // we don't block the writer _, err = httputil.DumpRequest(req, true) h.reqMeter.Mark(1) if err != nil { tee.Warn("Failed to extract request body: %v", err) } // golang's ReadRequest/DumpRequestOut is broken. Fix up the request so it works later req.URL.Scheme = "http" req.URL.Host = req.Host txn := &HttpTxn{Start: time.Now(), ConnUserCtx: connCtx} txn.Req = &HttpRequest{Request: req} if req.Body != nil { txn.Req.BodyBytes, txn.Req.Body, err = extractBody(req.Body) if err != nil { tee.Warn("Failed to extract request body: %v", err) } } lastTxn <- txn h.Txns.In() <- txn } } func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) { for txn := range lastTxn { resp, err := http.ReadResponse(tee.ReadBuffer(), txn.Req.Request) txn.Duration = time.Since(txn.Start) h.reqTimer.Update(txn.Duration) if err != nil { tee.Warn("Error reading response from server: %v", err) // no more responses to be read, we're done break } // make sure we read the body of the response so that // we don't block the reader _, _ = httputil.DumpResponse(resp, true) txn.Resp = &HttpResponse{Response: resp} // apparently, Body can be nil in some cases if resp.Body != nil { txn.Resp.BodyBytes, txn.Resp.Body, err = extractBody(resp.Body) if err != nil { tee.Warn("Failed to extract response body: %v", err) } } h.Txns.In() <- txn // XXX: remove web socket shim in favor of a real websocket protocol analyzer if txn.Req.Header.Get("Upgrade") == "websocket" { tee.Info("Upgrading to websocket") var wg sync.WaitGroup // shim for websockets // in order for websockets to work, we need to continue reading all of the // the bytes in the analyzer so that the joined connections will continue // sending bytes to each other wg.Add(2) go func() { ioutil.ReadAll(tee.WriteBuffer()) wg.Done() }() go func() { ioutil.ReadAll(tee.ReadBuffer()) wg.Done() }() wg.Wait() break } } } // we have to vendor DumpRequestOut because it's broken and the fix won't be in until at least 1.4 // XXX: remove this all in favor of actually parsing the HTTP traffic ourselves for more transparent // replay and inspection, regardless of when it gets fixed in stdlib // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // One of the copies, say from b to r2, could be avoided by using a more // elaborate trick where the other copy is made during Request/Response.Write. // This would complicate things too much, given that these functions are for // debugging only. func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { var buf bytes.Buffer if _, err = buf.ReadFrom(b); err != nil { return nil, nil, err } if err = b.Close(); err != nil { return nil, nil, err } return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil } // dumpConn is a net.Conn which writes to Writer and reads from Reader type dumpConn struct { io.Writer io.Reader } func (c *dumpConn) Close() error { return nil } func (c *dumpConn) LocalAddr() net.Addr { return nil } func (c *dumpConn) RemoteAddr() net.Addr { return nil } func (c *dumpConn) SetDeadline(t time.Time) error { return nil } func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil } func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil } type neverEnding byte func (b neverEnding) Read(p []byte) (n int, err error) { for i := range p { p[i] = byte(b) } return len(p), nil } // DumpRequestOut is like DumpRequest but includes // headers that the standard http.Transport adds, // such as User-Agent. func DumpRequestOut(req *http.Request, body bool) ([]byte, error) { save := req.Body dummyBody := false if !body || req.Body == nil { req.Body = nil if req.ContentLength != 0 { req.Body = ioutil.NopCloser(io.LimitReader(neverEnding('x'), req.ContentLength)) dummyBody = true } } else { var err error save, req.Body, err = drainBody(req.Body) if err != nil { return nil, err } } // Since we're using the actual Transport code to write the request, // switch to http so the Transport doesn't try to do an SSL // negotiation with our dumpConn and its bytes.Buffer & pipe. // The wire format for https and http are the same, anyway. reqSend := req if req.URL.Scheme == "https" { reqSend = new(http.Request) *reqSend = *req reqSend.URL = new(url.URL) *reqSend.URL = *req.URL reqSend.URL.Scheme = "http" } // Use the actual Transport code to record what we would send // on the wire, but not using TCP. Use a Transport with a // custom dialer that returns a fake net.Conn that waits // for the full input (and recording it), and then responds // with a dummy response. var buf bytes.Buffer // records the output pr, pw := io.Pipe() dr := &delegateReader{c: make(chan io.Reader)} // Wait for the request before replying with a dummy response: go func() { req, _ := http.ReadRequest(bufio.NewReader(pr)) // THIS IS THE PART THAT'S BROKEN IN THE STDLIB (as of Go 1.3) if req != nil && req.Body != nil { ioutil.ReadAll(req.Body) } dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\n\r\n") }() t := &http.Transport{ Dial: func(net, addr string) (net.Conn, error) { return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil }, } defer t.CloseIdleConnections() _, err := t.RoundTrip(reqSend) req.Body = save if err != nil { return nil, err } dump := buf.Bytes() // If we used a dummy body above, remove it now. // TODO: if the req.ContentLength is large, we allocate memory // unnecessarily just to slice it off here. But this is just // a debug function, so this is acceptable for now. We could // discard the body earlier if this matters. if dummyBody { if i := bytes.Index(dump, []byte("\r\n\r\n")); i >= 0 { dump = dump[:i+4] } } return dump, nil } // delegateReader is a reader that delegates to another reader, // once it arrives on a channel. type delegateReader struct { c chan io.Reader r io.Reader // nil until received from c } func (r *delegateReader) Read(p []byte) (int, error) { if r.r == nil { r.r = <-r.c } return r.r.Read(p) } // Return value if nonempty, def otherwise. func valueOrDefault(value, def string) string { if value != "" { return value } return def } var reqWriteExcludeHeaderDump = map[string]bool{ "Host": true, // not in Header map anyway "Content-Length": true, "Transfer-Encoding": true, "Trailer": true, } ================================================ FILE: src/ngrok/proto/interface.go ================================================ package proto import ( "ngrok/conn" ) type Protocol interface { GetName() string WrapConn(conn.Conn, interface{}) conn.Conn } ================================================ FILE: src/ngrok/proto/tcp.go ================================================ package proto import ( "ngrok/conn" ) type Tcp struct{} func NewTcp() *Tcp { return new(Tcp) } func (h *Tcp) GetName() string { return "tcp" } func (h *Tcp) WrapConn(c conn.Conn, ctx interface{}) conn.Conn { return c } ================================================ FILE: src/ngrok/server/cli.go ================================================ package server import ( "flag" ) type Options struct { httpAddr string httpsAddr string tunnelAddr string domain string tlsCrt string tlsKey string logto string loglevel string } func parseArgs() *Options { httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable") 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") loglevel := flag.String("log-level", "DEBUG", "The level of messages to log. One of: DEBUG, INFO, WARNING, ERROR") flag.Parse() return &Options{ httpAddr: *httpAddr, httpsAddr: *httpsAddr, tunnelAddr: *tunnelAddr, domain: *domain, tlsCrt: *tlsCrt, tlsKey: *tlsKey, logto: *logto, loglevel: *loglevel, } } ================================================ FILE: src/ngrok/server/control.go ================================================ package server import ( "fmt" "io" "ngrok/conn" "ngrok/msg" "ngrok/util" "ngrok/version" "runtime/debug" "strings" "time" ) const ( pingTimeoutInterval = 30 * time.Second connReapInterval = 10 * time.Second controlWriteTimeout = 10 * time.Second proxyStaleDuration = 60 * time.Second proxyMaxPoolSize = 10 ) type Control struct { // auth message auth *msg.Auth // actual connection conn conn.Conn // put a message in this channel to send it over // conn to the client out chan (msg.Message) // read from this channel to get the next message sent // to us over conn by the client in chan (msg.Message) // the last time we received a ping from the client - for heartbeats lastPing time.Time // all of the tunnels this control connection handles tunnels []*Tunnel // proxy connections proxies chan conn.Conn // identifier id string // synchronizer for controlled shutdown of writer() writerShutdown *util.Shutdown // synchronizer for controlled shutdown of reader() readerShutdown *util.Shutdown // synchronizer for controlled shutdown of manager() managerShutdown *util.Shutdown // synchronizer for controller shutdown of entire Control shutdown *util.Shutdown } func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) { var err error // create the object c := &Control{ auth: authMsg, conn: ctlConn, out: make(chan msg.Message), in: make(chan msg.Message), proxies: make(chan conn.Conn, 10), lastPing: time.Now(), writerShutdown: util.NewShutdown(), readerShutdown: util.NewShutdown(), managerShutdown: util.NewShutdown(), shutdown: util.NewShutdown(), } failAuth := func(e error) { _ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()}) ctlConn.Close() } // register the clientid c.id = authMsg.ClientId if c.id == "" { // it's a new session, assign an ID if c.id, err = util.SecureRandId(16); err != nil { failAuth(err) return } } // set logging prefix ctlConn.SetType("ctl") ctlConn.AddLogPrefix(c.id) if authMsg.Version != version.Proto { failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version)) return } // register the control if replaced := controlRegistry.Add(c.id, c); replaced != nil { replaced.shutdown.WaitComplete() } // start the writer first so that the following messages get sent go c.writer() // Respond to authentication c.out <- &msg.AuthResp{ Version: version.Proto, MmVersion: version.MajorMinor(), ClientId: c.id, } // As a performance optimization, ask for a proxy connection up front c.out <- &msg.ReqProxy{} // manage the connection go c.manager() go c.reader() go c.stopper() } // Register a new tunnel on this control connection func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) { for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") { tunnelReq := *rawTunnelReq tunnelReq.Protocol = proto c.conn.Debug("Registering new tunnel") t, err := NewTunnel(&tunnelReq, c) if err != nil { c.out <- &msg.NewTunnel{Error: err.Error()} if len(c.tunnels) == 0 { c.shutdown.Begin() } // we're done return } // add it to the list of tunnels c.tunnels = append(c.tunnels, t) // acknowledge success c.out <- &msg.NewTunnel{ Url: t.url, Protocol: proto, ReqId: rawTunnelReq.ReqId, } rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1) } } func (c *Control) manager() { // don't crash on panics defer func() { if err := recover(); err != nil { c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack()) } }() // kill everything if the control manager stops defer c.shutdown.Begin() // notify that manager() has shutdown defer c.managerShutdown.Complete() // reaping timer for detecting heartbeat failure reap := time.NewTicker(connReapInterval) defer reap.Stop() for { select { case <-reap.C: if time.Since(c.lastPing) > pingTimeoutInterval { c.conn.Info("Lost heartbeat") c.shutdown.Begin() } case mRaw, ok := <-c.in: // c.in closes to indicate shutdown if !ok { return } switch m := mRaw.(type) { case *msg.ReqTunnel: c.registerTunnel(m) case *msg.Ping: c.lastPing = time.Now() c.out <- &msg.Pong{} } } } } func (c *Control) writer() { defer func() { if err := recover(); err != nil { c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack()) } }() // kill everything if the writer() stops defer c.shutdown.Begin() // notify that we've flushed all messages defer c.writerShutdown.Complete() // write messages to the control channel for m := range c.out { c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout)) if err := msg.WriteMsg(c.conn, m); err != nil { panic(err) } } } func (c *Control) reader() { defer func() { if err := recover(); err != nil { c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack()) } }() // kill everything if the reader stops defer c.shutdown.Begin() // notify that we're done defer c.readerShutdown.Complete() // read messages from the control channel for { if msg, err := msg.ReadMsg(c.conn); err != nil { if err == io.EOF { c.conn.Info("EOF") return } else { panic(err) } } else { // this can also panic during shutdown c.in <- msg } } } func (c *Control) stopper() { defer func() { if r := recover(); r != nil { c.conn.Error("Failed to shut down control: %v", r) } }() // wait until we're instructed to shutdown c.shutdown.WaitBegin() // remove ourself from the control registry controlRegistry.Del(c.id) // shutdown manager() so that we have no more work to do close(c.in) c.managerShutdown.WaitComplete() // shutdown writer() close(c.out) c.writerShutdown.WaitComplete() // close connection fully c.conn.Close() // shutdown all of the tunnels for _, t := range c.tunnels { t.Shutdown() } // shutdown all of the proxy connections close(c.proxies) for p := range c.proxies { p.Close() } c.shutdown.Complete() c.conn.Info("Shutdown complete") } func (c *Control) RegisterProxy(conn conn.Conn) { conn.AddLogPrefix(c.id) conn.SetDeadline(time.Now().Add(proxyStaleDuration)) select { case c.proxies <- conn: conn.Info("Registered") default: conn.Info("Proxies buffer is full, discarding.") conn.Close() } } // Remove a proxy connection from the pool and return it // If not proxy connections are in the pool, request one // and wait until it is available // Returns an error if we couldn't get a proxy because it took too long // or the tunnel is closing func (c *Control) GetProxy() (proxyConn conn.Conn, err error) { var ok bool // get a proxy connection from the pool select { case proxyConn, ok = <-c.proxies: if !ok { err = fmt.Errorf("No proxy connections available, control is closing") return } default: // no proxy available in the pool, ask for one over the control channel c.conn.Debug("No proxy in pool, requesting proxy from control . . .") if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil { return } select { case proxyConn, ok = <-c.proxies: if !ok { err = fmt.Errorf("No proxy connections available, control is closing") return } case <-time.After(pingTimeoutInterval): err = fmt.Errorf("Timeout trying to get proxy connection") return } } return } // Called when this control is replaced by another control // this can happen if the network drops out and the client reconnects // before the old tunnel has lost its heartbeat func (c *Control) Replaced(replacement *Control) { c.conn.Info("Replaced by control: %s", replacement.conn.Id()) // set the control id to empty string so that when stopper() // calls registry.Del it won't delete the replacement c.id = "" // tell the old one to shutdown c.shutdown.Begin() } ================================================ FILE: src/ngrok/server/http.go ================================================ package server import ( "crypto/tls" "fmt" vhost "github.com/inconshreveable/go-vhost" //"net" "ngrok/conn" "ngrok/log" "strings" "time" ) const ( NotAuthorized = `HTTP/1.0 401 Not Authorized WWW-Authenticate: Basic realm="ngrok" Content-Length: 23 Authorization required ` NotFound = `HTTP/1.0 404 Not Found Content-Length: %d Tunnel %s not found ` BadRequest = `HTTP/1.0 400 Bad Request Content-Length: 12 Bad Request ` ) // Listens for new http(s) connections from the public internet func startHttpListener(addr string, tlsCfg *tls.Config) (listener *conn.Listener) { // bind/listen for incoming connections var err error if listener, err = conn.Listen(addr, "pub", tlsCfg); err != nil { panic(err) } proto := "http" if tlsCfg != nil { proto = "https" } log.Info("Listening for public %s connections on %v", proto, listener.Addr.String()) go func() { for conn := range listener.Conns { go httpHandler(conn, proto) } }() return } // Handles a new http connection from the public internet func httpHandler(c conn.Conn, proto string) { defer c.Close() defer func() { // recover from failures if r := recover(); r != nil { c.Warn("httpHandler failed with error %v", r) } }() // Make sure we detect dead connections while we decide how to multiplex c.SetDeadline(time.Now().Add(connReadTimeout)) // multiplex by extracting the Host header, the vhost library vhostConn, err := vhost.HTTP(c) if err != nil { c.Warn("Failed to read valid %s request: %v", proto, err) c.Write([]byte(BadRequest)) return } // read out the Host header and auth from the request host := strings.ToLower(vhostConn.Host()) auth := vhostConn.Request.Header.Get("Authorization") // done reading mux data, free up the request memory vhostConn.Free() // We need to read from the vhost conn now since it mucked around reading the stream c = conn.Wrap(vhostConn, "pub") // multiplex to find the right backend host c.Debug("Found hostname %s in request", host) tunnel := tunnelRegistry.Get(fmt.Sprintf("%s://%s", proto, host)) if tunnel == nil { c.Info("No tunnel found for hostname %s", host) c.Write([]byte(fmt.Sprintf(NotFound, len(host)+18, host))) return } // If the client specified http auth and it doesn't match this request's auth // then fail the request with 401 Not Authorized and request the client reissue the // request with basic authdeny the request if tunnel.req.HttpAuth != "" && auth != tunnel.req.HttpAuth { c.Info("Authentication failed: %s", auth) c.Write([]byte(NotAuthorized)) return } // dead connections will now be handled by tunnel heartbeating and the client c.SetDeadline(time.Time{}) // let the tunnel handle the connection now tunnel.HandlePublicConnection(c) } ================================================ FILE: src/ngrok/server/main.go ================================================ package server import ( "crypto/tls" "math/rand" "ngrok/conn" log "ngrok/log" "ngrok/msg" "ngrok/util" "os" "runtime/debug" "time" ) const ( registryCacheSize uint64 = 1024 * 1024 // 1 MB connReadTimeout time.Duration = 10 * time.Second ) // GLOBALS var ( tunnelRegistry *TunnelRegistry controlRegistry *ControlRegistry // XXX: kill these global variables - they're only used in tunnel.go for constructing forwarding URLs opts *Options listeners map[string]*conn.Listener ) func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) { // fail gracefully if the proxy connection fails to register defer func() { if r := recover(); r != nil { pxyConn.Warn("Failed with error: %v", r) pxyConn.Close() } }() // set logging prefix pxyConn.SetType("pxy") // look up the control connection for this proxy pxyConn.Info("Registering new proxy for %s", regPxy.ClientId) ctl := controlRegistry.Get(regPxy.ClientId) if ctl == nil { panic("No client found for identifier: " + regPxy.ClientId) } ctl.RegisterProxy(pxyConn) } // Listen for incoming control and proxy connections // We listen for incoming control and proxy connections on the same port // 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, tlsConfig *tls.Config) { // listen for incoming connections listener, err := conn.Listen(addr, "tun", tlsConfig) if err != nil { panic(err) } log.Info("Listening for control and proxy connections on %s", listener.Addr.String()) for c := range listener.Conns { go func(tunnelConn conn.Conn) { // don't crash on panics defer func() { if r := recover(); r != nil { tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack()) } }() tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout)) var rawMsg msg.Message if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil { tunnelConn.Warn("Failed to read message: %v", err) tunnelConn.Close() return } // don't timeout after the initial read, tunnel heartbeating will kill // dead connections tunnelConn.SetReadDeadline(time.Time{}) switch m := rawMsg.(type) { case *msg.Auth: NewControl(tunnelConn, m) case *msg.RegProxy: NewProxy(tunnelConn, m) default: tunnelConn.Close() } }(c) } } func Main() { // parse options opts = parseArgs() // init logging log.LogTo(opts.logto, opts.loglevel) // seed random number generator seed, err := util.RandomSeed() if err != nil { panic(err) } rand.Seed(seed) // init tunnel/control registry registryCacheFile := os.Getenv("REGISTRY_CACHE_FILE") tunnelRegistry = NewTunnelRegistry(registryCacheSize, registryCacheFile) controlRegistry = NewControlRegistry() // 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) } // listen for https if opts.httpsAddr != "" { listeners["https"] = startHttpListener(opts.httpsAddr, tlsConfig) } // ngrok clients tunnelListener(opts.tunnelAddr, tlsConfig) } ================================================ FILE: src/ngrok/server/metrics.go ================================================ package server import ( "bytes" "encoding/json" "fmt" gometrics "github.com/rcrowley/go-metrics" "io/ioutil" "net/http" "ngrok/conn" "ngrok/log" "os" "time" ) var metrics Metrics func init() { keenApiKey := os.Getenv("KEEN_API_KEY") if keenApiKey != "" { metrics = NewKeenIoMetrics(60 * time.Second) } else { metrics = NewLocalMetrics(30 * time.Second) } } type Metrics interface { log.Logger OpenConnection(*Tunnel, conn.Conn) CloseConnection(*Tunnel, conn.Conn, time.Time, int64, int64) OpenTunnel(*Tunnel) CloseTunnel(*Tunnel) } type LocalMetrics struct { log.Logger reportInterval time.Duration windowsCounter gometrics.Counter linuxCounter gometrics.Counter osxCounter gometrics.Counter otherCounter gometrics.Counter tunnelMeter gometrics.Meter tcpTunnelMeter gometrics.Meter httpTunnelMeter gometrics.Meter connMeter gometrics.Meter lostHeartbeatMeter gometrics.Meter connTimer gometrics.Timer bytesInCount gometrics.Counter bytesOutCount gometrics.Counter /* tunnelGauge gometrics.Gauge tcpTunnelGauge gometrics.Gauge connGauge gometrics.Gauge */ } func NewLocalMetrics(reportInterval time.Duration) *LocalMetrics { metrics := LocalMetrics{ Logger: log.NewPrefixLogger("metrics"), reportInterval: reportInterval, windowsCounter: gometrics.NewCounter(), linuxCounter: gometrics.NewCounter(), osxCounter: gometrics.NewCounter(), otherCounter: gometrics.NewCounter(), tunnelMeter: gometrics.NewMeter(), tcpTunnelMeter: gometrics.NewMeter(), httpTunnelMeter: gometrics.NewMeter(), connMeter: gometrics.NewMeter(), lostHeartbeatMeter: gometrics.NewMeter(), connTimer: gometrics.NewTimer(), bytesInCount: gometrics.NewCounter(), bytesOutCount: gometrics.NewCounter(), /* metrics.tunnelGauge = gometrics.NewGauge(), metrics.tcpTunnelGauge = gometrics.NewGauge(), metrics.connGauge = gometrics.NewGauge(), */ } go metrics.Report() return &metrics } func (m *LocalMetrics) OpenTunnel(t *Tunnel) { m.tunnelMeter.Mark(1) switch t.ctl.auth.OS { case "windows": m.windowsCounter.Inc(1) case "linux": m.linuxCounter.Inc(1) case "darwin": m.osxCounter.Inc(1) default: m.otherCounter.Inc(1) } switch t.req.Protocol { case "tcp": m.tcpTunnelMeter.Mark(1) case "http": m.httpTunnelMeter.Mark(1) } } func (m *LocalMetrics) CloseTunnel(t *Tunnel) { } func (m *LocalMetrics) OpenConnection(t *Tunnel, c conn.Conn) { m.connMeter.Mark(1) } func (m *LocalMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, bytesIn, bytesOut int64) { m.bytesInCount.Inc(bytesIn) m.bytesOutCount.Inc(bytesOut) } func (m *LocalMetrics) Report() { m.Info("Reporting every %d seconds", int(m.reportInterval.Seconds())) for { time.Sleep(m.reportInterval) buffer, err := json.Marshal(map[string]interface{}{ "windows": m.windowsCounter.Count(), "linux": m.linuxCounter.Count(), "osx": m.osxCounter.Count(), "other": m.otherCounter.Count(), "httpTunnelMeter.count": m.httpTunnelMeter.Count(), "tcpTunnelMeter.count": m.tcpTunnelMeter.Count(), "tunnelMeter.count": m.tunnelMeter.Count(), "tunnelMeter.m1": m.tunnelMeter.Rate1(), "connMeter.count": m.connMeter.Count(), "connMeter.m1": m.connMeter.Rate1(), "bytesIn.count": m.bytesInCount.Count(), "bytesOut.count": m.bytesOutCount.Count(), }) if err != nil { m.Error("Failed to serialize metrics: %v", err) continue } m.Info("Reporting: %s", buffer) } } type KeenIoMetric struct { Collection string Event interface{} } type KeenIoMetrics struct { log.Logger ApiKey string ProjectToken string HttpClient http.Client Metrics chan *KeenIoMetric } func NewKeenIoMetrics(batchInterval time.Duration) *KeenIoMetrics { k := &KeenIoMetrics{ Logger: log.NewPrefixLogger("metrics"), ApiKey: os.Getenv("KEEN_API_KEY"), ProjectToken: os.Getenv("KEEN_PROJECT_TOKEN"), Metrics: make(chan *KeenIoMetric, 1000), } go func() { defer func() { if r := recover(); r != nil { k.Error("KeenIoMetrics failed: %v", r) } }() batch := make(map[string][]interface{}) batchTimer := time.Tick(batchInterval) for { select { case m := <-k.Metrics: list, ok := batch[m.Collection] if !ok { list = make([]interface{}, 0) } batch[m.Collection] = append(list, m.Event) case <-batchTimer: // no metrics to report if len(batch) == 0 { continue } payload, err := json.Marshal(batch) if err != nil { k.Error("Failed to serialize metrics payload: %v, %v", batch, err) } else { for key, val := range batch { k.Debug("Reporting %d metrics for %s", len(val), key) } k.AuthedRequest("POST", "/events", bytes.NewReader(payload)) } batch = make(map[string][]interface{}) } } }() return k } func (k *KeenIoMetrics) AuthedRequest(method, path string, body *bytes.Reader) (resp *http.Response, err error) { path = fmt.Sprintf("https://api.keen.io/3.0/projects/%s%s", k.ProjectToken, path) req, err := http.NewRequest(method, path, body) if err != nil { return } req.Header.Add("Authorization", k.ApiKey) if body != nil { req.Header.Add("Content-Type", "application/json") req.ContentLength = int64(body.Len()) } requestStartAt := time.Now() resp, err = k.HttpClient.Do(req) if err != nil { k.Error("Failed to send metric event to keen.io %v", err) } else { k.Info("keen.io processed request in %f sec", time.Since(requestStartAt).Seconds()) defer resp.Body.Close() if resp.StatusCode != 200 { bytes, _ := ioutil.ReadAll(resp.Body) k.Error("Got %v response from keen.io: %s", resp.StatusCode, bytes) } } return } func (k *KeenIoMetrics) OpenConnection(t *Tunnel, c conn.Conn) { } func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, in, out int64) { event := struct { Keen KeenStruct `json:"keen"` OS string ClientId string Protocol string Url string User string Version string Reason string HttpAuth bool Subdomain bool TunnelDuration float64 ConnectionDuration float64 BytesIn int64 BytesOut int64 }{ Keen: KeenStruct{ Timestamp: start.UTC().Format("2006-01-02T15:04:05.000Z"), }, OS: t.ctl.auth.OS, ClientId: t.ctl.id, Protocol: t.req.Protocol, Url: t.url, User: t.ctl.auth.User, Version: t.ctl.auth.MmVersion, HttpAuth: t.req.HttpAuth != "", Subdomain: t.req.Subdomain != "", TunnelDuration: time.Since(t.start).Seconds(), ConnectionDuration: time.Since(start).Seconds(), BytesIn: in, BytesOut: out, } k.Metrics <- &KeenIoMetric{Collection: "CloseConnection", Event: event} } func (k *KeenIoMetrics) OpenTunnel(t *Tunnel) { } type KeenStruct struct { Timestamp string `json:"timestamp"` } func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) { event := struct { Keen KeenStruct `json:"keen"` OS string ClientId string Protocol string Url string User string Version string Reason string Duration float64 HttpAuth bool Subdomain bool }{ Keen: KeenStruct{ Timestamp: t.start.UTC().Format("2006-01-02T15:04:05.000Z"), }, OS: t.ctl.auth.OS, ClientId: t.ctl.id, Protocol: t.req.Protocol, Url: t.url, User: t.ctl.auth.User, Version: t.ctl.auth.MmVersion, //Reason: reason, Duration: time.Since(t.start).Seconds(), HttpAuth: t.req.HttpAuth != "", Subdomain: t.req.Subdomain != "", } k.Metrics <- &KeenIoMetric{Collection: "CloseTunnel", Event: event} } ================================================ FILE: src/ngrok/server/registry.go ================================================ package server import ( "encoding/gob" "fmt" "net" "ngrok/cache" "ngrok/log" "sync" "time" ) const ( cacheSaveInterval time.Duration = 10 * time.Minute ) type cacheUrl string func (url cacheUrl) Size() int { return len(url) } // TunnelRegistry maps a tunnel URL to Tunnel structures type TunnelRegistry struct { tunnels map[string]*Tunnel affinity *cache.LRUCache log.Logger sync.RWMutex } func NewTunnelRegistry(cacheSize uint64, cacheFile string) *TunnelRegistry { registry := &TunnelRegistry{ tunnels: make(map[string]*Tunnel), affinity: cache.NewLRUCache(cacheSize), Logger: log.NewPrefixLogger("registry", "tun"), } // LRUCache uses Gob encoding. Unfortunately, Gob is fickle and will fail // to encode or decode any non-primitive types that haven't been "registered" // with it. Since we store cacheUrl objects, we need to register them here first // for the encoding/decoding to work var urlobj cacheUrl gob.Register(urlobj) // try to load and then periodically save the affinity cache to file, if specified if cacheFile != "" { err := registry.affinity.LoadItemsFromFile(cacheFile) if err != nil { registry.Error("Failed to load affinity cache %s: %v", cacheFile, err) } registry.SaveCacheThread(cacheFile, cacheSaveInterval) } else { registry.Info("No affinity cache specified") } return registry } // Spawns a goroutine the periodically saves the cache to a file. func (r *TunnelRegistry) SaveCacheThread(path string, interval time.Duration) { go func() { r.Info("Saving affinity cache to %s every %s", path, interval.String()) for { time.Sleep(interval) r.Debug("Saving affinity cache") err := r.affinity.SaveItemsToFile(path) if err != nil { r.Error("Failed to save affinity cache: %v", err) } else { r.Info("Saved affinity cache") } } }() } // Register a tunnel with a specific url, returns an error // if a tunnel is already registered at that url func (r *TunnelRegistry) Register(url string, t *Tunnel) error { r.Lock() defer r.Unlock() if r.tunnels[url] != nil { return fmt.Errorf("The tunnel %s is already registered.", url) } r.tunnels[url] = t return nil } func (r *TunnelRegistry) cacheKeys(t *Tunnel) (ip string, id string) { clientIp := t.ctl.conn.RemoteAddr().(*net.TCPAddr).IP.String() clientId := t.ctl.id ipKey := fmt.Sprintf("client-ip-%s:%s", t.req.Protocol, clientIp) idKey := fmt.Sprintf("client-id-%s:%s", t.req.Protocol, clientId) return ipKey, idKey } func (r *TunnelRegistry) GetCachedRegistration(t *Tunnel) (url string) { ipCacheKey, idCacheKey := r.cacheKeys(t) // check cache for ID first, because we prefer that over IP which might // not be specific to a user because of NATs if v, ok := r.affinity.Get(idCacheKey); ok { url = string(v.(cacheUrl)) t.Debug("Found registry affinity %s for %s", url, idCacheKey) } else if v, ok := r.affinity.Get(ipCacheKey); ok { url = string(v.(cacheUrl)) t.Debug("Found registry affinity %s for %s", url, ipCacheKey) } return } func (r *TunnelRegistry) RegisterAndCache(url string, t *Tunnel) (err error) { if err = r.Register(url, t); err == nil { // we successfully assigned a url, cache it ipCacheKey, idCacheKey := r.cacheKeys(t) r.affinity.Set(ipCacheKey, cacheUrl(url)) r.affinity.Set(idCacheKey, cacheUrl(url)) } return } // Register a tunnel with the following process: // Consult the affinity cache to try to assign a previously used tunnel url if possible // Generate new urls repeatedly with the urlFn and register until one is available. func (r *TunnelRegistry) RegisterRepeat(urlFn func() string, t *Tunnel) (string, error) { url := r.GetCachedRegistration(t) if url == "" { url = urlFn() } maxAttempts := 5 for i := 0; i < maxAttempts; i++ { if err := r.RegisterAndCache(url, t); err != nil { // pick a new url and try again url = urlFn() } else { // we successfully assigned a url, we're done return url, nil } } return "", fmt.Errorf("Failed to assign a URL after %d attempts!", maxAttempts) } func (r *TunnelRegistry) Del(url string) { r.Lock() defer r.Unlock() delete(r.tunnels, url) } func (r *TunnelRegistry) Get(url string) *Tunnel { r.RLock() defer r.RUnlock() return r.tunnels[url] } // ControlRegistry maps a client ID to Control structures type ControlRegistry struct { controls map[string]*Control log.Logger sync.RWMutex } func NewControlRegistry() *ControlRegistry { return &ControlRegistry{ controls: make(map[string]*Control), Logger: log.NewPrefixLogger("registry", "ctl"), } } func (r *ControlRegistry) Get(clientId string) *Control { r.RLock() defer r.RUnlock() return r.controls[clientId] } func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) { r.Lock() defer r.Unlock() oldCtl = r.controls[clientId] if oldCtl != nil { oldCtl.Replaced(ctl) } r.controls[clientId] = ctl r.Info("Registered control with id %s", clientId) return } func (r *ControlRegistry) Del(clientId string) error { r.Lock() defer r.Unlock() if r.controls[clientId] == nil { return fmt.Errorf("No control found for client id: %s", clientId) } else { r.Info("Removed control registry id %s", clientId) delete(r.controls, clientId) return nil } } ================================================ FILE: src/ngrok/server/tls.go ================================================ package server import ( "crypto/tls" "io/ioutil" "ngrok/server/assets" ) 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.Asset 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 } if key, err = fileOrAsset(keyPath, "assets/server/tls/snakeoil.key"); err != nil { return } if cert, err = tls.X509KeyPair(crt, key); err != nil { return } tlsConfig = &tls.Config{ Certificates: []tls.Certificate{cert}, } return } ================================================ FILE: src/ngrok/server/tunnel.go ================================================ package server import ( "encoding/base64" "fmt" "math/rand" "net" "ngrok/conn" "ngrok/log" "ngrok/msg" "ngrok/util" "os" "strconv" "strings" "sync/atomic" "time" ) var defaultPortMap = map[string]int{ "http": 80, "https": 443, "smtp": 25, } /** * Tunnel: A control connection, metadata and proxy connections which * route public traffic to a firewalled endpoint. */ type Tunnel struct { // request that opened the tunnel req *msg.ReqTunnel // time when the tunnel was opened start time.Time // public url url string // tcp listener listener *net.TCPListener // control connection ctl *Control // logger log.Logger // closing closing int32 } // Common functionality for registering virtually hosted protocols func registerVhost(t *Tunnel, protocol string, servingPort int) (err error) { vhost := os.Getenv("VHOST") if vhost == "" { vhost = fmt.Sprintf("%s:%d", opts.domain, servingPort) } // Canonicalize virtual host by removing default port (e.g. :80 on HTTP) defaultPort, ok := defaultPortMap[protocol] if !ok { return fmt.Errorf("Couldn't find default port for protocol %s", protocol) } defaultPortSuffix := fmt.Sprintf(":%d", defaultPort) if strings.HasSuffix(vhost, defaultPortSuffix) { vhost = vhost[0 : len(vhost)-len(defaultPortSuffix)] } // Canonicalize by always using lower-case vhost = strings.ToLower(vhost) // Register for specific hostname hostname := strings.ToLower(strings.TrimSpace(t.req.Hostname)) if hostname != "" { t.url = fmt.Sprintf("%s://%s", protocol, hostname) return tunnelRegistry.Register(t.url, t) } // Register for specific subdomain subdomain := strings.ToLower(strings.TrimSpace(t.req.Subdomain)) if subdomain != "" { t.url = fmt.Sprintf("%s://%s.%s", protocol, subdomain, vhost) return tunnelRegistry.Register(t.url, t) } // Register for random URL t.url, err = tunnelRegistry.RegisterRepeat(func() string { return fmt.Sprintf("%s://%x.%s", protocol, rand.Int31(), vhost) }, t) return } // Create a new tunnel from a registration message received // on a control channel func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) { t = &Tunnel{ req: m, start: time.Now(), ctl: ctl, Logger: log.NewPrefixLogger(), } proto := t.req.Protocol switch proto { case "tcp": bindTcp := func(port int) error { if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil { err = t.ctl.conn.Error("Error binding TCP listener: %v", err) return err } // create the url addr := t.listener.Addr().(*net.TCPAddr) t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port) // register it if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil { // This should never be possible because the OS will // only assign available ports to us. t.listener.Close() err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url) return err } go t.listenTcp(t.listener) return nil } // use the custom remote port you asked for if t.req.RemotePort != 0 { bindTcp(int(t.req.RemotePort)) return } // try to return to you the same port you had before cachedUrl := tunnelRegistry.GetCachedRegistration(t) if cachedUrl != "" { var port int parts := strings.Split(cachedUrl, ":") portPart := parts[len(parts)-1] port, err = strconv.Atoi(portPart) if err != nil { t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart) } else { // we have a valid, cached port, let's try to bind with it if bindTcp(port) != nil { t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err) } else { // success, we're done return } } } // Bind for TCP connections bindTcp(0) return case "http", "https": l, ok := listeners[proto] if !ok { err = fmt.Errorf("Not listening for %s connections", proto) return } if err = registerVhost(t, proto, l.Addr.(*net.TCPAddr).Port); err != nil { return } default: err = fmt.Errorf("Protocol %s is not supported", proto) return } // pre-encode the http basic auth for fast comparisons later if m.HttpAuth != "" { m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth)) } t.AddLogPrefix(t.Id()) t.Info("Registered new tunnel on: %s", t.ctl.conn.Id()) metrics.OpenTunnel(t) return } func (t *Tunnel) Shutdown() { t.Info("Shutting down") // mark that we're shutting down atomic.StoreInt32(&t.closing, 1) // if we have a public listener (this is a raw TCP tunnel), shut it down if t.listener != nil { t.listener.Close() } // remove ourselves from the tunnel registry tunnelRegistry.Del(t.url) // let the control connection know we're shutting down // currently, only the control connection shuts down tunnels, // so it doesn't need to know about it // t.ctl.stoptunnel <- t metrics.CloseTunnel(t) } func (t *Tunnel) Id() string { return t.url } // Listens for new public tcp connections from the internet. func (t *Tunnel) listenTcp(listener *net.TCPListener) { for { defer func() { if r := recover(); r != nil { log.Warn("listenTcp failed with error %v", r) } }() // accept public connections tcpConn, err := listener.AcceptTCP() if err != nil { // not an error, we're shutting down this tunnel if atomic.LoadInt32(&t.closing) == 1 { return } t.Error("Failed to accept new TCP connection: %v", err) continue } conn := conn.Wrap(tcpConn, "pub") conn.AddLogPrefix(t.Id()) conn.Info("New connection from %v", conn.RemoteAddr()) go t.HandlePublicConnection(conn) } } func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) { defer publicConn.Close() defer func() { if r := recover(); r != nil { publicConn.Warn("HandlePublicConnection failed with error %v", r) } }() startTime := time.Now() metrics.OpenConnection(t, publicConn) var proxyConn conn.Conn var err error for i := 0; i < (2 * proxyMaxPoolSize); i++ { // get a proxy connection if proxyConn, err = t.ctl.GetProxy(); err != nil { t.Warn("Failed to get proxy connection: %v", err) return } defer proxyConn.Close() t.Info("Got proxy connection %s", proxyConn.Id()) proxyConn.AddLogPrefix(t.Id()) // tell the client we're going to start using this proxy connection startPxyMsg := &msg.StartProxy{ Url: t.url, ClientAddr: publicConn.RemoteAddr().String(), } if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil { proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i) proxyConn.Close() } else { // success break } } if err != nil { // give up publicConn.Error("Too many failures starting proxy connection") return } // To reduce latency handling tunnel connections, we employ the following curde heuristic: // Whenever we take a proxy connection from the pool, replace it with a new one util.PanicToError(func() { t.ctl.out <- &msg.ReqProxy{} }) // no timeouts while connections are joined proxyConn.SetDeadline(time.Time{}) // join the public and proxy connections bytesIn, bytesOut := conn.Join(publicConn, proxyConn) metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut) } ================================================ FILE: src/ngrok/util/broadcast.go ================================================ package util type Broadcast struct { listeners []chan interface{} reg chan (chan interface{}) unreg chan (chan interface{}) in chan interface{} } func NewBroadcast() *Broadcast { b := &Broadcast{ listeners: make([]chan interface{}, 0), reg: make(chan (chan interface{})), unreg: make(chan (chan interface{})), in: make(chan interface{}), } go func() { for { select { case l := <-b.unreg: // remove L from b.listeners // this operation is slow: O(n) but not used frequently // unlike iterating over listeners oldListeners := b.listeners b.listeners = make([]chan interface{}, 0, len(oldListeners)) for _, oldL := range oldListeners { if l != oldL { b.listeners = append(b.listeners, oldL) } } case l := <-b.reg: b.listeners = append(b.listeners, l) case item := <-b.in: for _, l := range b.listeners { l <- item } } } }() return b } func (b *Broadcast) In() chan interface{} { return b.in } func (b *Broadcast) Reg() chan interface{} { listener := make(chan interface{}) b.reg <- listener return listener } func (b *Broadcast) UnReg(listener chan interface{}) { b.unreg <- listener } ================================================ FILE: src/ngrok/util/errors.go ================================================ package util import ( "fmt" "runtime" ) const crashMessage = `panic: %v %s Oh noes! ngrok crashed! Please submit the stack trace and any relevant information to: github.com/inconshreveable/ngrok/issues` func MakePanicTrace(err interface{}) string { stackBuf := make([]byte, 4096) n := runtime.Stack(stackBuf, false) return fmt.Sprintf(crashMessage, err, stackBuf[:n]) } // Runs the given function and converts any panic encountered while doing so // into an error. Useful for sending to channels that will close func PanicToError(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("Panic: %v", r) } }() fn() return } ================================================ FILE: src/ngrok/util/id.go ================================================ package util import ( "crypto/rand" "encoding/binary" "fmt" mrand "math/rand" ) func RandomSeed() (seed int64, err error) { err = binary.Read(rand.Reader, binary.LittleEndian, &seed) return } // creates a random identifier of the specified length func RandId(idlen int) string { b := make([]byte, idlen) var randVal uint32 for i := 0; i < idlen; i++ { byteIdx := i % 4 if byteIdx == 0 { randVal = mrand.Uint32() } b[i] = byte((randVal >> (8 * uint(byteIdx))) & 0xFF) } return fmt.Sprintf("%x", b) } // like RandId, but uses a crypto/rand for secure random identifiers func SecureRandId(idlen int) (id string, err error) { b := make([]byte, idlen) n, err := rand.Read(b) if n != idlen { err = fmt.Errorf("Only generated %d random bytes, %d requested", n, idlen) return } if err != nil { return } id = fmt.Sprintf("%x", b) return } func SecureRandIdOrPanic(idlen int) string { id, err := SecureRandId(idlen) if err != nil { panic(err) } return id } ================================================ FILE: src/ngrok/util/ring.go ================================================ package util import ( "container/list" "sync" ) type Ring struct { sync.Mutex *list.List capacity int } func NewRing(capacity int) *Ring { return &Ring{capacity: capacity, List: list.New()} } func (r *Ring) Add(item interface{}) interface{} { r.Lock() defer r.Unlock() // add new item r.PushFront(item) // remove old item if at capacity var old interface{} if r.Len() >= r.capacity { old = r.Remove(r.Back()) } return old } func (r *Ring) Slice() []interface{} { r.Lock() defer r.Unlock() i := 0 items := make([]interface{}, r.Len()) for e := r.Front(); e != nil; e = e.Next() { items[i] = e.Value i++ } return items } ================================================ FILE: src/ngrok/util/shutdown.go ================================================ package util import ( "sync" ) // A small utility class for managing controlled shutdowns type Shutdown struct { sync.Mutex inProgress bool begin chan int // closed when the shutdown begins complete chan int // closed when the shutdown completes } func NewShutdown() *Shutdown { return &Shutdown{ begin: make(chan int), complete: make(chan int), } } func (s *Shutdown) Begin() { s.Lock() defer s.Unlock() if s.inProgress == true { return } else { s.inProgress = true close(s.begin) } } func (s *Shutdown) WaitBegin() { <-s.begin } func (s *Shutdown) Complete() { close(s.complete) } func (s *Shutdown) WaitComplete() { <-s.complete } ================================================ FILE: src/ngrok/version/version.go ================================================ package version import ( "fmt" ) const ( Proto = "2" Major = "1" Minor = "7" ) func MajorMinor() string { return fmt.Sprintf("%s.%s", Major, Minor) } func Full() string { return fmt.Sprintf("%s-%s.%s", Proto, Major, Minor) } func Compat(client string, server string) bool { return client == server }