func versionCheck(s *State, ctl *ui.Controller) { check := func() { resp, err := http.Get(versionEndpoint) if err != nil { log.Warn("Failed to get version info %s: %v", versionEndpoint, err) return } defer resp.Body.Close() var payload struct { Client struct { Version string } } err = json.NewDecoder(resp.Body).Decode(&payload) if err != nil { log.Warn("Failed to read version info: %v", err) return } if payload.Client.Version != version.MajorMinor() { s.newVersion = payload.Client.Version ctl.Update(s) } } // check immediately and then at a set interval check() for _ = range time.Tick(versionCheckInterval) { check() } }
func parseArgs() *Options { authtoken := flag.String( "authtoken", "", "Authentication token for identifying a premium ngrok.com account") server := flag.String( "server", "ngrok.com:4443", "The remote ngrok server") httpAuth := flag.String( "httpauth", "", "username:password HTTP basic auth creds protecting the public tunnel endpoint") subdomain := flag.String( "subdomain", "", "Request a custom subdomain from the ngrok server. (HTTP mode only)") protocol := flag.String( "proto", "http", "The protocol of the traffic over the tunnel {'http', 'tcp'} (default: 'http')") webport := flag.Int( "webport", 4040, "The port on which the web interface is served") logto := flag.String( "log", "none", "Write log messages to this file. 'stdout' and 'none' have special meanings") v := flag.Bool( "version", false, "Print ngrok version and exit") flag.Parse() if *v { fmt.Println(version.MajorMinor()) os.Exit(0) } return &Options{ server: *server, httpAuth: *httpAuth, subdomain: *subdomain, localaddr: parseLocalAddr(), protocol: parseProtocol(*protocol), webport: *webport, logto: *logto, authtoken: parseAuthToken(*authtoken), } }
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 NewControl(ctlConn conn.Conn, authMsg *msg.Auth) { var err error // create the object // channels are buffered because we read and write to them // from the same goroutine in managerThread() c := &Control{ auth: authMsg, conn: ctlConn, out: make(chan msg.Message, 5), in: make(chan msg.Message, 5), stop: make(chan msg.Message, 5), proxies: make(chan conn.Conn, 10), lastPing: time.Now(), } 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 } } 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 controlRegistry.Add(c.id, c) 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{} // set logging prefix ctlConn.SetType("ctl") // manage the connection go c.managerThread() go c.readThread() }
func (c *Control) managerThread() { reap := time.NewTicker(connReapInterval) // all shutdown functionality in here defer func() { if err := recover(); err != nil { c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack()) } reap.Stop() c.conn.Close() // shutdown the tunnel if it's open if c.tun != nil { c.tun.shutdown() } }() for { select { case m := <-c.out: msg.WriteMsg(c.conn, m) case <-reap.C: if time.Since(c.lastPing) > pingTimeoutInterval { c.conn.Info("Lost heartbeat") metrics.lostHeartbeatMeter.Mark(1) return } case m := <-c.stop: if m != nil { msg.WriteMsg(c.conn, m) } return case mRaw := <-c.in: switch m := mRaw.(type) { case *msg.RegMsg: c.conn.Info("Registering new tunnel") c.tun = newTunnel(m, c) case *msg.PingMsg: c.lastPing = time.Now() c.out <- &msg.PongMsg{} case *msg.VersionMsg: c.out <- &msg.VersionRespMsg{ Version: version.Proto, MmVersion: version.MajorMinor(), } } } } }
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) { t = &Tunnel{ regMsg: m, start: time.Now(), ctl: ctl, proxies: make(chan conn.Conn), Logger: log.NewPrefixLogger(), } switch t.regMsg.Protocol { case "tcp": var err error t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0}) if err != nil { t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err) t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"} } go t.listenTcp(t.listener) default: } if err := tunnels.Add(t); err != nil { t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprint(err)} return } if m.Version != version.Proto { t.ctl.stop <- &msg.RegAckMsg{Error: fmt.Sprintf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)} } // pre-encode the http basic auth for fast comparisons later if m.HttpAuth != "" { m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth)) } t.ctl.conn.AddLogPrefix(t.Id()) t.AddLogPrefix(t.Id()) t.Info("Registered new tunnel") t.ctl.out <- &msg.RegAckMsg{ Url: t.url, ProxyAddr: fmt.Sprintf("%s", proxyAddr), Version: version.Proto, MmVersion: version.MajorMinor(), } metrics.OpenTunnel(t) return }
// implement client.ui.State func (s State) GetClientVersion() string { return version.MajorMinor() }
func ParseArgs() (opts *Options, err error) { flag.Usage = func() { fmt.Fprintf(os.Stderr, usage1, os.Args[0]) flag.PrintDefaults() fmt.Fprintf(os.Stderr, usage2) } config := flag.String( "config", "", "Path to ngrok configuration file. (default: $HOME/.ngrok)") logto := flag.String( "log", "none", "Write log messages to this file. 'stdout' and 'none' have special meanings") authtoken := flag.String( "authtoken", "", "Authentication token for identifying an ngrok.com account") httpauth := flag.String( "httpauth", "", "username:password HTTP basic auth creds protecting the public tunnel endpoint") subdomain := flag.String( "subdomain", "", "Request a custom subdomain from the ngrok server. (HTTP only)") hostname := flag.String( "hostname", "", "Request a custom hostname from the ngrok server. (HTTP only) (requires CNAME of your DNS)") protocol := flag.String( "proto", "http+https", "The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')") flag.Parse() opts = &Options{ config: *config, logto: *logto, httpauth: *httpauth, subdomain: *subdomain, protocol: *protocol, authtoken: *authtoken, hostname: *hostname, command: flag.Arg(0), } switch opts.command { case "start": opts.args = flag.Args()[1:] case "version": fmt.Println(version.MajorMinor()) os.Exit(0) case "help": flag.Usage() os.Exit(0) case "": err = fmt.Errorf("Error: Specify a local port to tunnel to, or " + "an ngrok command.\n\nExample: To expose port 80, run " + "'ngrok 80'") return default: if len(flag.Args()) > 1 { err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v", len(flag.Args()), flag.Args()) return } opts.command = "default" opts.args = flag.Args() } return }
// 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) } } }
func (c ClientModel) GetClientVersion() string { return version.MajorMinor() }
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth, required_secret string) { 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 } if required_secret != "" && authMsg.User != required_secret { failAuth(fmt.Errorf("Invalid authtoken %s", authMsg.User)) 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() }
/** * Establishes and manages a tunnel control connection with the server */ func control(s *State, ctl *ui.Controller) { defer func() { if r := recover(); r != nil { log.Error("control recovering from failure %v", r) } }() // establish control channel conn, err := conn.Dial(s.opts.server, "ctl", tlsConfig) if err != nil { panic(err) } defer conn.Close() // register with the server err = msg.WriteMsg(conn, &msg.RegMsg{ Protocol: s.opts.protocol, OS: runtime.GOOS, HttpAuth: s.opts.httpAuth, Hostname: s.opts.hostname, Subdomain: s.opts.subdomain, ClientId: s.id, Version: version.Proto, MmVersion: version.MajorMinor(), User: s.opts.authtoken, }) if err != nil { panic(err) } // wait for the server to ack our register var regAck msg.RegAckMsg if err = msg.ReadMsgInto(conn, ®Ack); err != nil { panic(err) } if regAck.Error != "" { emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", regAck.Error) ctl.Cmds <- ui.CmdQuit{Message: emsg} return } // update UI state s.publicUrl = regAck.Url conn.Info("Tunnel established at %v", s.GetPublicUrl()) s.status = "online" s.serverVersion = regAck.MmVersion ctl.Update(s) SaveAuthToken(s.opts.authtoken) // start the heartbeat lastPong := time.Now().UnixNano() go heartbeat(&lastPong, conn) // main control loop for { var m msg.Message if m, err = msg.ReadMsg(conn); err != nil { panic(err) } switch m.(type) { case *msg.ReqProxyMsg: go proxy(regAck.ProxyAddr, s, ctl) case *msg.PongMsg: atomic.StoreInt64(&lastPong, time.Now().UnixNano()) } } }
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) { t = &Tunnel{ regMsg: m, start: time.Now(), ctl: ctl, proxies: make(chan conn.Conn), Logger: log.NewPrefixLogger(), } failReg := func(err error) { t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()} } var err error switch t.regMsg.Protocol { case "tcp": var port int = 0 // try to return to you the same port you had before cachedUrl := tunnels.GetCachedRegistration(t) if cachedUrl != "" { 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) // continue with zero port = 0 } } // Bind for TCP connections t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}) // If we failed with a custom port, try with a random one if err != nil && port != 0 { t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err) t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0}) } // we tried to bind with a random port and failed (no more ports available?) if err != nil { failReg(t.ctl.conn.Error("Error binding TCP listener: %v", err)) return } // create the url addr := t.listener.Addr().(*net.TCPAddr) t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port) // register it if err = tunnels.RegisterAndCache(t.url, t); err != nil { // This should never be possible because the OS will // only assign available ports to us. t.listener.Close() failReg(fmt.Errorf("TCP listener bound, but failed to register %s", t.url)) return } go t.listenTcp(t.listener) case "http": if strings.TrimSpace(t.regMsg.Hostname) != "" { t.url = fmt.Sprintf("http://%s", t.regMsg.Hostname) } else if strings.TrimSpace(t.regMsg.Subdomain) != "" { t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, domain) } if t.url != "" { if err := tunnels.Register(t.url, t); err != nil { failReg(err) return } } else { t.url, err = tunnels.RegisterRepeat(func() string { return fmt.Sprintf("http://%x.%s", rand.Int31(), domain) }, t) if err != nil { failReg(err) return } } } if m.Version != version.Proto { failReg(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)) return } // pre-encode the http basic auth for fast comparisons later if m.HttpAuth != "" { m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth)) } t.ctl.conn.AddLogPrefix(t.Id()) t.AddLogPrefix(t.Id()) t.Info("Registered new tunnel") t.ctl.out <- &msg.RegAckMsg{ Url: t.url, ProxyAddr: fmt.Sprintf("%s", proxyAddr), Version: version.Proto, MmVersion: version.MajorMinor(), } metrics.OpenTunnel(t) return }
func newTunnel(m *msg.RegMsg, ctl *Control) (t *Tunnel) { t = &Tunnel{ regMsg: m, start: time.Now(), ctl: ctl, proxies: make(chan conn.Conn), Logger: log.NewPrefixLogger(), } failReg := func(err error) { t.ctl.stop <- &msg.RegAckMsg{Error: err.Error()} } switch t.regMsg.Protocol { case "tcp": var err error t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0}) if err != nil { t.ctl.conn.Error("Failed to create tunnel. Error binding TCP listener: %v", err) t.ctl.stop <- &msg.RegAckMsg{Error: "Internal server error"} } addr := t.listener.Addr().(*net.TCPAddr) t.url = fmt.Sprintf("tcp://%s:%d", domain, addr.Port) if err = tunnels.Register(t.url, t); err != nil { // This should never be possible because the OS will only assign // available ports to us. t.Error("TCP listener bound, but failed to register: %s", err.Error()) t.listener.Close() failReg(err) return } go t.listenTcp(t.listener) case "http": if strings.TrimSpace(t.regMsg.Hostname) != "" { t.url = fmt.Sprintf("http://%s", t.regMsg.Hostname) } else if strings.TrimSpace(t.regMsg.Subdomain) != "" { t.url = fmt.Sprintf("http://%s.%s", t.regMsg.Subdomain, domain) } if t.url != "" { if err := tunnels.Register(t.url, t); err != nil { failReg(err) return } } else { t.url = tunnels.RegisterRepeat(func() string { return fmt.Sprintf("http://%x.%s", rand.Int31(), domain) }, t) } } if m.Version != version.Proto { failReg(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), m.Version)) return } // pre-encode the http basic auth for fast comparisons later if m.HttpAuth != "" { m.HttpAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(m.HttpAuth)) } t.ctl.conn.AddLogPrefix(t.Id()) t.AddLogPrefix(t.Id()) t.Info("Registered new tunnel") t.ctl.out <- &msg.RegAckMsg{ Url: t.url, ProxyAddr: fmt.Sprintf("%s", proxyAddr), Version: version.Proto, MmVersion: version.MajorMinor(), } metrics.OpenTunnel(t) return }
func autoUpdate(s mvc.State, token string) { tryAgain := true params := make(url.Values) params.Add("version", version.MajorMinor()) params.Add("os", runtime.GOOS) params.Add("arch", runtime.GOARCH) params.Add("user", token) updateUrl := updateEndpoint + "?" + params.Encode() checkUrl := checkEndpoint + "?" + params.Encode() update := func() { log.Info("Checking for update") available, err := update.NewDownload(checkUrl).Check() if err != nil { log.Error("Error while checking for update: %v", err) return } if !available { log.Info("No update available") return } // stop trying after a single download attempt // XXX: improve this so the we can: // 1. safely update multiple times // 2. only retry after a network connection failure tryAgain = false download := update.NewDownload(updateUrl) downloadComplete := make(chan int) go progressWatcher(s, download.Progress, downloadComplete) log.Info("Trying to update . . .") err, errRecover := download.GetAndUpdate() <-downloadComplete if err != nil { // log error to console 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()) params.Add("errorRecover", errRecover.Error()) } // log error to ngrok.com's servers for debugging purposes params.Add("error", err.Error()) resp, reportErr := http.PostForm("https://dl.ngrok.com/update/error", params) if err != nil { log.Error("Error while reporting update error: %v, %v", err, reportErr) } resp.Body.Close() // tell the user to update manually s.SetUpdateStatus(mvc.UpdateAvailable) } else { if !download.Available { // this is the way the server tells us to update manually log.Info("Server wants us to update manually") s.SetUpdateStatus(mvc.UpdateAvailable) } else { // tell the user the update is ready log.Info("Update ready!") s.SetUpdateStatus(mvc.UpdateReady) } } return } // try to update immediately and then at a set interval update() for _ = range time.Tick(updateCheckInterval) { if !tryAgain { break } update() } }
func autoUpdate(s *State, ctl *ui.Controller, token string) { update := func() (updateSuccessful bool) { params := make(url.Values) params.Add("version", version.MajorMinor()) params.Add("os", runtime.GOOS) params.Add("arch", runtime.GOARCH) download := update.NewDownload() downloadComplete := make(chan int) go func() { for { select { case progress, ok := <-download.Progress: if !ok { close(downloadComplete) return } else if progress == 100 { s.update = ui.UpdateInstalling ctl.Update(s) close(downloadComplete) return } else { if progress%25 == 0 { log.Info("Downloading update %d%% complete", progress) } s.update = ui.UpdateStatus(progress) ctl.Update(s) } } } }() log.Info("Checking for update") err := download.UpdateFromUrl(updateEndpoint + "?" + params.Encode()) <-downloadComplete if err != nil { log.Error("Error while updating ngrok: %v", err) if download.Available { s.update = ui.UpdateError } else { s.update = ui.UpdateNone } // record the error to ngrok.com's servers for debugging purposes params.Add("error", err.Error()) params.Add("user", token) resp, err := http.PostForm("https://dl.ngrok.com/update/error", params) if err != nil { log.Error("Error while reporting update error") } resp.Body.Close() } else { if download.Available { log.Info("Marked update ready") s.update = ui.UpdateReady updateSuccessful = true } else { log.Info("No update available at this time") s.update = ui.UpdateNone } } ctl.Update(s) return } // try to update immediately and then at a set interval update() for _ = range time.Tick(updateCheckInterval) { if update() { // stop trying to update if the update function is successful // XXX: improve this by trying to download versions newer than // the last one we downloaded return } } }