package main import ( "io" "mime" "net" "net/http" "path/filepath" "sync" "time" "crypto/rand" "encoding/base64" "encoding/json" "github.com/gorilla/mux" "github.com/hashicorp/yamux" log "github.com/sirupsen/logrus" ) // a proxy-server that accepts: // - TCP connection // - creates a session ID for each new TCP connection // - creates a mux and is ready to proxy all HTTP connectios of type: host// over this TCP connection // - HTTP connections // - GET /s// - forwards this GET connection to the TCP connection // - GET /ws// - forwards this WS connection to the TCP connection type HelloClient struct { Version string Data string } type HelloServer struct { Version string SessionID string PublicURL string Data string } type sessionWrapper struct { ySession *yamux.Session sessionID string } type serverConfig struct { publicURL string backListenAddress string frontListenAddress string } type server struct { config serverConfig httpServer *http.Server activeSessions map[string]*sessionWrapper activeSessionsRWLock sync.RWMutex backListener net.Listener } func errToString(err error) string { if err != nil { return err.Error() } return "nil" } func generateNewSessionID() string { binID := make([]byte, 50) _, err := rand.Read(binID) if err != nil { log.Fatalf(err.Error()) } return base64.RawURLEncoding.EncodeToString([]byte(binID)) } func pipeConnectionsAndWait(backConn, frontConn net.Conn) error { errChan := make(chan error, 2) backConnAddr := backConn.RemoteAddr().String() frontConnAddr := frontConn.RemoteAddr().String() log.Debugf("Piping the two conn %s <-> %s ..", backConnAddr, frontConnAddr) copyAndNotify := func(dst, src net.Conn, info string) { n, err := io.Copy(dst, src) log.Debugf("%s: piping done with %d bytes, and err %s", info, n, errToString(err)) errChan <- err // Close both connections when done with copying. Yeah, both will beclosed two // times, but it doesn't matter. By closing them both, we unblock the other copy // call which would block indefinitely otherwise dst.Close() src.Close() } go copyAndNotify(backConn, frontConn, "front->back") go copyAndNotify(frontConn, backConn, "back->front") err1 := <-errChan err2 := <-errChan log.Debugf("Piping finished for %s <-> %s .", backConnAddr, frontConnAddr) // Return one of the two error that is not nil if err1 != nil { return err1 } return err2 } func mainHandler(w http.ResponseWriter, r *http.Request, backConn net.Conn) { // write this request further to the connection // hijack the connection comming with the request // hook the two one to the other defer backConn.Close() hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "Can't proxy this connection", http.StatusInternalServerError) return } frontConn, _, err := hj.Hijack() // TODO: what about the buffer - might have data inside? if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer frontConn.Close() err = r.Write(backConn) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } pipeConnectionsAndWait(backConn, frontConn) log.Debugf("Done with proxying.") } func performHandshake(conn net.Conn, publicBaseURL string, sessionID string) error { var helloC HelloClient dec := json.NewDecoder(conn) err := dec.Decode(&helloC) if err != nil { return err } helloS := HelloServer{ Version: "1", SessionID: sessionID, PublicURL: publicBaseURL + "/s/" + sessionID + "/", Data: "", } enc := json.NewEncoder(conn) err = enc.Encode(helloS) log.Debugf("Handshake: client version=%s. Sent session %s", helloC.Version, helloS.SessionID) return err } func newServer(c serverConfig) *server { s := &server{ config: c, } s.activeSessions = make(map[string]*sessionWrapper) return s } func addNewSession(s *server, session *sessionWrapper) { s.activeSessionsRWLock.Lock() s.activeSessions[session.sessionID] = session s.activeSessionsRWLock.Unlock() } func removeSession(s *server, session *sessionWrapper) { s.activeSessionsRWLock.Lock() delete(s.activeSessions, session.sessionID) s.activeSessionsRWLock.Unlock() } func getSession(s *server, sessionID string) (wrapper *sessionWrapper) { s.activeSessionsRWLock.RLock() wrapper = s.activeSessions[sessionID] s.activeSessionsRWLock.RUnlock() return } func (s *server) serveContent(w http.ResponseWriter, r *http.Request, name string) { file, err := Asset(name) if err != nil { w.WriteHeader(http.StatusNotFound) return } ctype := mime.TypeByExtension(filepath.Ext(name)) if ctype == "" { ctype = http.DetectContentType(file) } w.Header().Set("Content-Type", ctype) w.Write(file) } func (s *server) handleFrontConnections() error { router := mux.NewRouter() router.PathPrefix("/s/{sessionID}/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { startTime := time.Now() log.Infof("New front client connection: %s, from %s", r.URL.Path, r.RemoteAddr) vars := mux.Vars(r) sessionID := vars["sessionID"] wrapper := getSession(s, sessionID) if wrapper == nil { log.Warnf("Invalid session: %s, from %s", sessionID, r.RemoteAddr) s.serveContent(w, r, "invalid-session.html") return } backConn, err := wrapper.ySession.Open() defer backConn.Close() if err != nil { log.Warnf("Cannot serve session %s for %s, back request error: ", sessionID, r.RemoteAddr, err.Error()) s.serveContent(w, r, "invalid-session.html") return } mainHandler(w, r, backConn) duration := time.Now().Sub(startTime) log.Infof("Front client request %s from %s proxied for %.2f sec", r.URL.Path, r.RemoteAddr, duration.Seconds()) }) router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.serveContent(w, r, r.URL.Path) }))) router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.serveContent(w, r, "404.html") }) router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://tty-share.com", http.StatusMovedPermanently) }) s.httpServer = &http.Server{ Addr: s.config.frontListenAddress, Handler: router, } log.Debugf("Listening for HTTP on: http://%s", s.config.frontListenAddress) return s.httpServer.ListenAndServe() } func (s *server) handleBackConnections() (err error) { s.backListener, err = net.Listen("tcp", s.config.backListenAddress) if err != nil { log.Fatalf(err.Error()) } log.Debugf("Listening for proxy-client connections on %s", s.config.backListenAddress) for { conn, err := s.backListener.Accept() if err != nil { log.Error(err.Error()) break } log.Debugf("New back connection: %s", conn.RemoteAddr().String()) sessionID := generateNewSessionID() err = performHandshake(conn, s.config.publicURL, sessionID) if err != nil { log.Warn("Cannot perform handshake on the back connection: %s", err.Error()) conn.Close() continue } ymuxServerSession, err := yamux.Client(conn, nil) if err != nil { log.Warn("Cannot create back tunnel: %s", err.Error()) conn.Close() continue } wrapper := &sessionWrapper{ sessionID: sessionID, ySession: ymuxServerSession, } addNewSession(s, wrapper) log.Infof("New tty-share session %s from for %s", sessionID, conn.RemoteAddr().String()) // Open a "monitoring" connection to detect when the underlying TCP connection // dies. Note that this "monitoring" connection is a virtual one, over the // multiplexer, so it should have minimal resources impact. go func() { _, err := ymuxServerSession.Accept() if err != nil { log.Infof("tty-share session %s closed: %s", sessionID, err.Error()) removeSession(s, wrapper) conn.Close() return } }() } return } func (s *server) Run() (err error) { var wg sync.WaitGroup wg.Add(1) go func() { err = s.handleBackConnections() wg.Done() }() err2 := s.handleFrontConnections() wg.Wait() if err2 != nil { return err2 } return } func (s *server) Stop() { s.httpServer.Close() s.backListener.Close() }