func (whv *WebHttpView) update() { // open channels for incoming http state changes // and broadbasts txnUpdates := whv.httpProto.Txns.Reg() for { select { case txn := <-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) // XXX: golang, why do I have to do this to make DumpRequestOut work later? htxn.Req.URL.Scheme = "http" if htxn.Resp == nil { whtxn := &WebHttpTxn{Id: util.RandId(), HttpTxn: htxn} // 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) } } } }
func (whv *WebHttpView) updateHttp() { // open channels for incoming http state changes // and broadbasts 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.UserData == nil { id, err := util.RandId(8) if err != nil { log.Error("Failed to generate txn identifier for web storage: %v", err) continue } rawReq, err := httputil.DumpRequest(htxn.Req.Request, true) if err != nil { log.Error("Failed to dump request: %v", err) continue } body := makeBody(htxn.Req.Header, htxn.Req.BodyBytes) whtxn := &SerializedTxn{ Id: id, 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(), } htxn.UserData = 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 { log.Error("Failed to dump response: %v", err) continue } txn := htxn.UserData.(*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 { log.Error("Failed to serialized txn payload for websocket: %v", err) } whv.webview.wsMessages.In() <- payload } } }
// 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 Main() { // parse options opts := parseArgs() // set up logging log.LogTo(opts.logto) // init client state s := &State{ status: "connecting", // unique client id id: util.RandId(), // command-line options opts: opts, // metrics metrics: NewClientMetrics(), } switch opts.protocol { case "http": s.protocol = proto.NewHttp() case "tcp": s.protocol = proto.NewTcp() } // init ui ctl := ui.NewController() web.NewWebView(ctl, s, opts.webport) if opts.logto != "stdout" { term.New(ctl, s) } go reconnectingControl(s, ctl) go versionCheck(s, ctl) quitMessage := "" ctl.Wait.Add(1) go func() { defer ctl.Wait.Done() for { select { case obj := <-ctl.Cmds: switch cmd := obj.(type) { case ui.CmdQuit: quitMessage = cmd.Message ctl.DoShutdown() return case ui.CmdRequest: go func() { var localConn conn.Conn localConn, err := conn.Dial(s.opts.localaddr, "prv") if err != nil { log.Warn("Failed to open private leg %s: %v", s.opts.localaddr, err) return } //defer localConn.Close() localConn = s.protocol.WrapConn(localConn) localConn.Write(cmd.Payload) ioutil.ReadAll(localConn) }() } } } }() ctl.Wait.Wait() fmt.Println(quitMessage) }