You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tty-proxy/server.go

335 lines
8.1 KiB
Go

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/<session id>/<path> over this TCP connection
// - HTTP connections
// - GET /s/<ID>/ - forwards this GET connection to the TCP connection
// - GET /ws/<DI>/ - 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()
}