mirror of https://github.com/elisescu/tty-proxy/
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.
335 lines
8.1 KiB
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()
|
|
}
|