From 4912aafe65f737a51ec29a314bf21abb769f32af Mon Sep 17 00:00:00 2001 From: bagas Date: Sat, 5 Apr 2025 23:27:32 +0700 Subject: [PATCH] update: implement tls server --- certs/localhost.direct.SS.crt | 21 ++ certs/localhost.direct.SS.key | 28 +++ server/http.go | 81 +++++++- server/https.go | 84 ++++++++ server/server.go | 12 ++ session/handler.go | 359 ++++++++++++++++++++++++---------- session/session.go | 12 +- 7 files changed, 484 insertions(+), 113 deletions(-) create mode 100644 certs/localhost.direct.SS.crt create mode 100644 certs/localhost.direct.SS.key create mode 100644 server/https.go diff --git a/certs/localhost.direct.SS.crt b/certs/localhost.direct.SS.crt new file mode 100644 index 0000000..d36fa1f --- /dev/null +++ b/certs/localhost.direct.SS.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDjDCCAnSgAwIBAgIUPpuvw4ZdpnDBbwOZBt2ALfZykuYwDQYJKoZIhvcNAQEL +BQAwPDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u +ZG9uMQswCQYDVQQLDAJIUTAeFw0yNDExMTkxNjQxNTNaFw0zNDExMTcxNjQxNTNa +MDwxCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRv +bjELMAkGA1UECwwCSFEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD +L/aeaBzgkYxDNiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5E +sHptIpu5/uTBFaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgA +HKryOwMcB5DheBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9r +WSJllNHOFk+RH17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t +8Ik74GeNrM6f+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIk +tOot0YiZGQAoOedtGSEnAgMBAAGjgYUwgYIwCQYDVR0TBAIwADALBgNVHQ8EBAMC +BeAwSQYDVR0RBEIwQIIQbG9jYWxob3N0LmRpcmVjdIISKi5sb2NhbGhvc3QuZGly +ZWN0ghhTUy5jZXJ0LmxvY2FsaG9zdC5kaXJlY3QwHQYDVR0OBBYEFKBVeirQGZ4D +4AKVPd7LGfCF1wEZMA0GCSqGSIb3DQEBCwUAA4IBAQCRpvsc5DrQBo8yATmUS0OK +xfUXfZR28u3xYY+qMHi+ngIVT2TKJ1yoBJezV6WwQLkcGdWacULvPYt3jCFUtaP7 ++hzfs5y1FssFsXDx+r3pQxYyE6BX3BhlrJPJhLRyG1siTTgN439Qu40/TsTzNgAT +A9GbAro+W6+qA4H92mBlyfQEOBossID/Kk95uvDnQguUOp0ZBLgFNRfE6Ra9+yC+ +ufAOksYDrisPE6kZId0Ra4Ln/GmrIXKjjmLCitq2q2REC/70JSCnaJcBYeThSJLZ +AZ24AF+JteakSJ8FEgRGxvSu0wdZfnMobNoelsjai1p5l5mCTiD8GH9sQCkslnp3 +-----END CERTIFICATE----- diff --git a/certs/localhost.direct.SS.key b/certs/localhost.direct.SS.key new file mode 100644 index 0000000..0e0aa47 --- /dev/null +++ b/certs/localhost.direct.SS.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDL/aeaBzgkYxD +NiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5EsHptIpu5/uTB +FaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgAHKryOwMcB5Dh +eBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9rWSJllNHOFk+R +H17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t8Ik74GeNrM6f ++vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIktOot0YiZGQAo +OedtGSEnAgMBAAECggEASXibb2MnQ4zl7ELL+HGYb5sNpLrHJU5M4ujmuMn6jNjh ++C2dbs2KYlMtpMcAweMD8Y0weDYnwiplNx0KSYJECpNnJehTqrn33J0EAyXz3CWX +eWXxBUXpkp2hfoSEQSTth3VDD60Q7ZMXgRdvi2EtBmLKPNLADHGu/aoM5ENdwE9/ +E80X68E+MT2czOY/sEI/w2Tf/S2ZVOHRvFOsmvTLFlbiElWG8pmguajpJwdae7uX +c5VD87b0oYicSUvaHe6xOCCyyeBVq06sWk+vh6Tzrw6K/B+q0SYvzXdrdsJxbzUD +PvhVi9rf9AC1Ncb6lFOP2ZjGfqYQvfgGXKqaVxXWQQKBgQD8frATynMSeR15oERa ++Maa7r3GlwWV3tkUblX7/FxBo9UhAsivnWZprccZR43YPowbWNeU3AQppf/YDxmx +70/RVTCMjXPGyspSS2iFtcp+K2KnIZA4BuAG/s1EBrAUW2KrtaTaXgYu9usgb+Fq +dJUEBDWrL59XXtQUwSM1laH0BwKBgQDF5Z31VY96xOS7flmolq8Ag+frSMH0G4np +3nUnTlUkgpFXE5FkmUccbYZv9QialAVriBBUNANPDBQH4PRrNnX8Z6B2HJbpAy30 +II9jPMTNKnXT3RKFcJCamNkOQhT0sBAwm4gTsx+7gbpdZxinxH0Cr/08sfU4lbee +EtMV/J1h4QKBgC4MLLBvS20jCW0U/WJZ3F6FC7cb87jRW2WOeb/q1ihiaIwMpezh +F7xOJPFHS2cUgRi7qxVKyreNvor4tgbtTfEvSBtZ8LNgaGV5uyYncTZxUxyH0nVl +S5X7AhRV4+bSg7ws9FOesiH+hgL0ZHe1qzeATQlbNgQJF0RxtKohD9ghAoGAcM0N +WIZInoYUivreSEZ7wiNt0qNKSsZXukLfLGRuC72Q8r1opprn+cBEXRSirtmorT6F +cDmlmS0dTdBgAaytXA4FXM23B2KUkw7sLHi7BOcq+nSM1hrvke+F6aapI0AoOkyt +J+12LP8pJ4xYdWh+iUWfZzVYvcQ5QZUhVOsFGoECgYA9llKmc/cFaXrCr97g7ls8 +ZWW1kQKLawAv6+90dwECJl+zpyQN/TURyEfz6oFJDbNEL0xAAcm9thDah177Tq95 +pcHbVn/pAI4h4CLzM2ExSe8Ybvy6iPZaBiCVXq3ms1PK0EDyYLH1p3FZqm+UStZh +/6fYspyivrQEivRK3jGuWA== +-----END PRIVATE KEY----- diff --git a/server/http.go b/server/http.go index 8fb94e3..1676cec 100644 --- a/server/http.go +++ b/server/http.go @@ -2,20 +2,28 @@ package server import ( "bufio" + "bytes" "errors" "fmt" + "golang.org/x/net/context" "log" "net" - "net/http" "strings" + "time" "tunnel_pls/session" + "tunnel_pls/utils" ) +var redirectTLS bool = false + func NewHTTPServer() error { - listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:80")) + listener, err := net.Listen("tcp", ":80") if err != nil { return errors.New("Error listening: " + err.Error()) } + if utils.Getenv("tls_enabled") == "true" && utils.Getenv("tls_redirect") == "true" { + redirectTLS = true + } go func() { for { conn, err := listener.Accept() @@ -34,30 +42,83 @@ func NewHTTPServer() error { } func Handler(conn net.Conn) { + //TODO: Determain deadline time/set custom timeout on env + ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) reader := bufio.NewReader(conn) - request, err := http.ReadRequest(reader) + headers, err := peekUntilHeaders(reader, 512) if err != nil { - fmt.Println("Error reading request:", err) + fmt.Println("Failed to peek headers:", err) + return + } + + host := strings.Split(parseHostFromHeader(headers), ".") + if len(host) < 1 { + conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n")) + fmt.Println("Bad Request") + conn.Close() return } - host := strings.Split(request.Host, ".") if len(host) < 1 { conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n")) + fmt.Println("Bad Request") + conn.Close() + return + } + slug := host[0] + + if redirectTLS { + conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" + + fmt.Sprintf("Location: https://%s.%s/\r\n", slug, utils.Getenv("domain")) + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n")) conn.Close() return } - slug := host[0] sshSession, ok := session.Clients[slug] if !ok { conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n")) + fmt.Println("Bad Request 1") conn.Close() return } - request.Header.Set("Connection", "keep-alive") - request.Header.Set("Keep-Alive", "timeout=60") - - go sshSession.HandleForwardedConnectionHTTP(conn, sshSession.Connection, request) + sshSession.HandleForwardedConnection(session.UserConnection{ + Reader: reader, + Writer: conn, + Context: ctx, + }, sshSession.Connection, 80) + return +} + +func peekUntilHeaders(reader *bufio.Reader, maxBytes int) ([]byte, error) { + var buf []byte + for { + n := len(buf) + 1 + if n > maxBytes { + return buf, nil + } + + peek, err := reader.Peek(n) + if err != nil { + return nil, err + } + buf = peek + + if bytes.Contains(buf, []byte("\r\n\r\n")) { + return buf, nil + } + } +} + +func parseHostFromHeader(data []byte) string { + lines := strings.Split(string(data), "\r\n") + for _, line := range lines { + if strings.HasPrefix(strings.ToLower(line), "host:") { + return strings.TrimSpace(strings.TrimPrefix(line, "Host:")) + } + } + return "" } diff --git a/server/https.go b/server/https.go new file mode 100644 index 0000000..53b0a7d --- /dev/null +++ b/server/https.go @@ -0,0 +1,84 @@ +package server + +import ( + "bufio" + "crypto/tls" + "errors" + "fmt" + "golang.org/x/net/context" + "log" + "net" + "strings" + "time" + "tunnel_pls/session" +) + +func NewHTTPSServer() error { + cert, err := tls.LoadX509KeyPair("certs/localhost.direct.SS.crt", "certs/localhost.direct.SS.key") + if err != nil { + return err + } + + config := &tls.Config{Certificates: []tls.Certificate{cert}} + ln, err := tls.Listen("tcp", ":443", config) + if err != nil { + return err + } + + go func() { + for { + conn, err := ln.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + log.Println("https server closed") + } + log.Printf("Error accepting connection: %v", err) + continue + } + + go HandlerTLS(conn) + } + }() + return nil +} + +func HandlerTLS(conn net.Conn) { + ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second)) + reader := bufio.NewReader(conn) + headers, err := peekUntilHeaders(reader, 512) + if err != nil { + fmt.Println("Failed to peek headers:", err) + return + } + + host := strings.Split(parseHostFromHeader(headers), ".") + if len(host) < 1 { + conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n")) + fmt.Println("Bad Request") + conn.Close() + return + } + + if len(host) < 1 { + conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n")) + fmt.Println("Bad Request") + conn.Close() + return + } + slug := host[0] + + sshSession, ok := session.Clients[slug] + if !ok { + conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n")) + fmt.Println("Bad Request 1") + conn.Close() + return + } + + sshSession.HandleForwardedConnection(session.UserConnection{ + Reader: reader, + Writer: conn, + Context: ctx, + }, sshSession.Connection, 80) + return +} diff --git a/server/server.go b/server/server.go index e98b8d1..bc5079b 100644 --- a/server/server.go +++ b/server/server.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "tunnel_pls/utils" ) type Server struct { @@ -20,6 +21,17 @@ func NewServer(config ssh.ServerConfig) *Server { log.Fatalf("failed to listen on port 2200: %v", err) return nil } + if utils.Getenv("tls_enabled") == "true" { + go func() { + err := NewHTTPSServer() + if err != nil { + if err != nil { + log.Fatalf("failed to start https server: %v", err) + } + return + } + }() + } go func() { err := NewHTTPServer() if err != nil { diff --git a/session/handler.go b/session/handler.go index 1a2762d..ecb56d5 100644 --- a/session/handler.go +++ b/session/handler.go @@ -7,18 +7,20 @@ import ( "errors" "fmt" "golang.org/x/crypto/ssh" + "golang.org/x/net/context" "io" "log" "net" - "net/http" "strconv" + "strings" "time" "tunnel_pls/utils" ) type UserConnection struct { - Reader io.Reader - Writer net.Conn + Reader io.Reader + Writer net.Conn + Context context.Context } func (s *Session) handleGlobalRequest() { @@ -76,6 +78,11 @@ func (s *Session) handleTCPIPForward(req *ssh.Request) { buf := new(bytes.Buffer) binary.Write(buf, binary.BigEndian, uint32(80)) log.Printf("Forwarding approved on port: %d", 80) + //TODO: fix status checking later + for s.Status != RUNNING { + time.Sleep(500 * time.Millisecond) + } + if utils.Getenv("tls_enabled") == "true" { s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to https://%s.%s \r\n", slug, utils.Getenv("domain")))) } else { @@ -97,6 +104,7 @@ func (s *Session) handleTCPIPForward(req *ssh.Request) { s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to %s:%d \r\n", utils.Getenv("domain"), portToBind))) go func() { for { + fmt.Println("jalan di bawah") conn, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { @@ -107,8 +115,9 @@ func (s *Session) handleTCPIPForward(req *ssh.Request) { } go s.HandleForwardedConnection(UserConnection{ - Reader: nil, - Writer: conn, + Reader: nil, + Writer: conn, + Context: context.Background(), }, s.Connection, portToBind) } }() @@ -122,6 +131,47 @@ func (s *Session) handleTCPIPForward(req *ssh.Request) { } +func showWelcomeMessage(connection ssh.Channel) { + fmt.Println("jalan nih") + asciiArt := []string{ + ` _______ ____ `, + `|_ __| | | | __ \| | `, + ` | |_ __ _ ___| | | |__) | |___ `, + ` | | | | | '_ \| '_ \ / \ | | __/| / __|`, + ` | | |_| | | | | | | | __/ | | | | \__ \`, + ` |_|\__,_|_| |_|_| |_|\___|_| |_| |_|___/`, + ``, + ` "Tunnel Pls" - Project by Bagas`, + ` https://fossy.my.id`, + ``, + ` Welcome to Tunnel! Available commands:`, + ` - '/bye' : Exit the tunnel`, + ` - '/help' : Show this help message`, + ` - '/clear' : Clear the current line`, + ` - '/slug' : Set custom subdomain`, + } + + for _, line := range asciiArt { + connection.Write([]byte("\r\n" + line)) + } + connection.Write([]byte("\r\n\r\n")) +} + +func displaySlugEditor(connection ssh.Channel, currentSlug string) { + connection.Write([]byte("\r\n\r\n")) + connection.Write([]byte(" ╔══════════════════════════════════════════════╗\r\n")) + connection.Write([]byte(" ║ SUBDOMAIN EDITOR ║\r\n")) + connection.Write([]byte(" ╠══════════════════════════════════════════════╣\r\n")) + connection.Write([]byte(" ║ ║\r\n")) + connection.Write([]byte(" ║ Current: " + currentSlug + "." + utils.Getenv("domain") + strings.Repeat(" ", max(0, 30-len(currentSlug)-len(utils.Getenv("domain")))) + "║\r\n")) + connection.Write([]byte(" ║ ║\r\n")) + connection.Write([]byte(" ║ New: ║\r\n")) + connection.Write([]byte(" ║ ║\r\n")) + connection.Write([]byte(" ╠══════════════════════════════════════════════╣\r\n")) + connection.Write([]byte(" ║ [Enter] Save | [Esc] Cancel ║\r\n")) + connection.Write([]byte(" ╚══════════════════════════════════════════════╝\r\n\r\n")) +} + func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) { connection, requests, err := newChannel.Accept() s.ConnChannels = append(s.ConnChannels, connection) @@ -132,11 +182,152 @@ func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) { go func() { var commandBuffer bytes.Buffer buf := make([]byte, 1) + inSlugEditMode := false + editSlug := s.Slug + for { n, err := connection.Read(buf) if n > 0 { char := buf[0] + + if inSlugEditMode { + if char == 13 { + isValid := true + if len(editSlug) < 3 || len(editSlug) > 20 { + isValid = false + } else { + for _, c := range editSlug { + if !((c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-') { + isValid = false + break + } + } + if editSlug[0] == '-' || editSlug[len(editSlug)-1] == '-' { + isValid = false + } + } + + connection.Write([]byte("\033[H\033[2J")) + + if isValid { + oldSlug := s.Slug + newSlug := editSlug + + client, ok := Clients[oldSlug] + if !ok { + connection.Write([]byte("\r\n\r\n❌ SERVER ERROR ❌\r\n\r\n")) + connection.Write([]byte("Failed to update subdomain. You will be disconnected in 5 seconds.\r\n\r\n")) + + for i := 5; i > 0; i-- { + connection.Write([]byte(fmt.Sprintf("Disconnecting in %d...\r\n", i))) + time.Sleep(1 * time.Second) + } + + s.Close() + return + } + + if _, exists := Clients[newSlug]; exists && newSlug != oldSlug { + connection.Write([]byte("\r\n\r\n❌ SUBDOMAIN ALREADY IN USE ❌\r\n\r\n")) + connection.Write([]byte("This subdomain is already taken. Please try another one.\r\n\r\n")) + connection.Write([]byte("Press any key to continue...\r\n")) + + waitForKeyPress := true + for waitForKeyPress { + keyBuf := make([]byte, 1) + _, err := connection.Read(keyBuf) + if err == nil { + waitForKeyPress = false + } + } + + connection.Write([]byte("\033[H\033[2J")) + inSlugEditMode = true + editSlug = oldSlug + + displaySlugEditor(connection, oldSlug) + connection.Write([]byte("➤ " + editSlug + "." + utils.Getenv("domain"))) + continue + } + + delete(Clients, oldSlug) + client.Slug = newSlug + //TODO: uneceserry channel + client.SlugChannel <- true + Clients[newSlug] = client + + connection.Write([]byte("\r\n\r\n✅ SUBDOMAIN UPDATED ✅\r\n\r\n")) + connection.Write([]byte("Your new address is: " + newSlug + "." + utils.Getenv("domain") + "\r\n\r\n")) + connection.Write([]byte("Press any key to continue...\r\n")) + } else { + connection.Write([]byte("\r\n\r\n❌ INVALID SUBDOMAIN ❌\r\n\r\n")) + connection.Write([]byte("Use only lowercase letters, numbers, and hyphens.\r\n")) + connection.Write([]byte("Length must be 3-20 characters and cannot start or end with a hyphen.\r\n\r\n")) + connection.Write([]byte("Press any key to continue...\r\n")) + } + + waitForKeyPress := true + for waitForKeyPress { + keyBuf := make([]byte, 1) + _, err := connection.Read(keyBuf) + if err == nil { + waitForKeyPress = false + } + } + + connection.Write([]byte("\033[H\033[2J")) + showWelcomeMessage(connection) + if utils.Getenv("tls_enabled") == "true" { + s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to https://%s.%s \r\n", s.Slug, utils.Getenv("domain")))) + } else { + s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to http://%s.%s \r\n", s.Slug, utils.Getenv("domain")))) + } + + inSlugEditMode = false + commandBuffer.Reset() + continue + } else if char == 27 { + inSlugEditMode = false + connection.Write([]byte("\033[H\033[2J")) + connection.Write([]byte("\r\n\r\n⚠️ SUBDOMAIN EDIT CANCELLED ⚠️\r\n\r\n")) + connection.Write([]byte("Press any key to continue...\r\n")) + + waitForKeyPress := true + for waitForKeyPress { + keyBuf := make([]byte, 1) + _, err := connection.Read(keyBuf) + if err == nil { + waitForKeyPress = false + } + } + + connection.Write([]byte("\033[H\033[2J")) + showWelcomeMessage(connection) + + commandBuffer.Reset() + continue + } else if char == 8 || char == 127 { + if len(editSlug) > 0 { + editSlug = editSlug[:len(editSlug)-1] + connection.Write([]byte("\r\033[K")) + connection.Write([]byte("➤ " + editSlug + "." + utils.Getenv("domain"))) + } + continue + } else if char >= 32 && char <= 126 { + if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' { + editSlug += string(char) + connection.Write([]byte("\r\033[K")) + connection.Write([]byte("➤ " + editSlug + "." + utils.Getenv("domain"))) + } + continue + } + continue + } + connection.Write(buf[:n]) + if char == 8 || char == 127 { if commandBuffer.Len() > 0 { commandBuffer.Truncate(commandBuffer.Len() - 1) @@ -160,13 +351,38 @@ func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) { fmt.Println("Closing connection...") s.Close() break + } else if command == "/debug" { + fmt.Println(Clients) } else if command == "/help" { - connection.Write([]byte("Available commands: /bye, /help, /clear")) - + connection.Write([]byte("\r\nAvailable commands: /bye, /help, /clear, /slug")) } else if command == "/clear" { connection.Write([]byte("\033[H\033[2J")) + } else if command == "/slug" { + if s.TunnelType != HTTP { + connection.Write([]byte(fmt.Sprintf("%s cannot be edited", s.TunnelType))) + continue + } + inSlugEditMode = true + editSlug = s.Slug + + connection.Write([]byte("\033[H\033[2J")) + + connection.Write([]byte("\r\n\r\n")) + connection.Write([]byte(" ╔══════════════════════════════════════════════╗\r\n")) + connection.Write([]byte(" ║ SUBDOMAIN EDITOR ║\r\n")) + connection.Write([]byte(" ╠══════════════════════════════════════════════╣\r\n")) + connection.Write([]byte(" ║ ║\r\n")) + connection.Write([]byte(" ║ Current: " + s.Slug + "." + utils.Getenv("domain") + "║\r\n")) + connection.Write([]byte(" ║ ║\r\n")) + connection.Write([]byte(" ║ New: ║\r\n")) + connection.Write([]byte(" ║ ║\r\n")) + connection.Write([]byte(" ╠══════════════════════════════════════════════╣\r\n")) + connection.Write([]byte(" ║ [Enter] Save | [Esc] Cancel ║\r\n")) + connection.Write([]byte(" ╚══════════════════════════════════════════════╝\r\n\r\n")) + + connection.Write([]byte("➤ " + editSlug + "." + utils.Getenv("domain"))) } else { - connection.Write([]byte("Unknown command")) + connection.Write([]byte("\r\nUnknown command")) } commandBuffer.Reset() @@ -188,30 +404,10 @@ func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) { }() go func() { - asciiArt := []string{ - ` _______ _ _____ _ `, - `|__ __| | | | __ \| | `, - ` | |_ _ _ __ _ __ ___| | | |__) | |___ `, - ` | | | | | '_ \| '_ \ / _ \ | | ___/| / __|`, - ` | | |_| | | | | | | | __/ | | | | \__ \`, - ` |_|\__,_|_| |_|_| |_|\___|_| |_| |_|___/`, - ``, - ` "Tunnel Pls" - Project by Bagas`, - ` https://fossy.my.id`, - ``, - ` Welcome to Tunnel! Available commands:`, - ` - '/bye' : Exit the tunnel`, - ` - '/help' : Show this help message`, - ` - '/clear' : Clear the current line`, - } - connection.Write([]byte("\033[H\033[2J")) + showWelcomeMessage(connection) + s.Status = RUNNING - for _, line := range asciiArt { - connection.Write([]byte("\r\n" + line)) - } - - connection.Write([]byte("\r\n\r\n")) go s.handleGlobalRequest() for req := range requests { @@ -228,96 +424,55 @@ func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) { func (s *Session) HandleForwardedConnection(conn UserConnection, sshConn *ssh.ServerConn, port uint32) { defer conn.Writer.Close() + log.Printf("Handling new forwarded connection from %s", conn.Writer.RemoteAddr()) host, originPort := ParseAddr(conn.Writer.RemoteAddr().String()) - payload := createForwardedTCPIPPayload(host, originPort, port) + s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -> [%s] TUNNEL ADDRESS -- \"%s\" \r\n \033[0m", conn.Writer.RemoteAddr().String(), s.TunnelType, time.Now().Format("02/Jan/2006 15:04:05")))) + + payload := createForwardedTCPIPPayload(host, uint16(originPort), uint16(port)) channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", payload) - go func() { - for req := range reqs { - req.Reply(false, nil) - } - }() if err != nil { log.Printf("Failed to open forwarded-tcpip channel: %v", err) + io.Copy(conn.Writer, bytes.NewReader([]byte("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 11\r\nContent-Type: text/plain\r\n\r\nBad Gateway"))) return } + defer channel.Close() + + go func() { + select { + case <-reqs: + for req := range reqs { + req.Reply(false, nil) + } + case <-conn.Context.Done(): + conn.Writer.Close() + channel.Close() + fmt.Println("cancel by timeout") + return + case <-s.SlugChannel: + conn.Writer.Close() + channel.Close() + fmt.Println("cancel by slug") + return + } + }() + defer channel.Close() if conn.Reader == nil { conn.Reader = bufio.NewReader(conn.Writer) } + go io.Copy(channel, conn.Reader) reader := bufio.NewReader(channel) _, err = reader.Peek(1) if err == io.EOF { - fmt.Println("error babi") + s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr 1\r\n")) + return } + io.Copy(conn.Writer, reader) } -func (s *Session) HandleForwardedConnectionHTTP(conn net.Conn, sshConn *ssh.ServerConn, request *http.Request) { - defer conn.Close() - fmt.Println(request) - channelPayload := createForwardedTCPIPPayload(request.Host, 80, 80) - channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", channelPayload) - go func() { - for req := range reqs { - req.Reply(false, nil) - } - }() - - var requestBuffer bytes.Buffer - if err := request.Write(&requestBuffer); err != nil { - fmt.Println("Error serializing request:", err) - channel.Close() - conn.Close() - return - } - channel.Write(requestBuffer.Bytes()) - - reader := bufio.NewReader(channel) - _, err = reader.Peek(1) - if err == io.EOF { - io.Copy(conn, bytes.NewReader([]byte("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 11\r\nContent-Type: text/plain\r\n\r\nBad Gateway"))) - s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr\r\n")) - return - } else { - s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -- [%s] \"%s %s %s\" \r\n \033[0m", request.Host, time.Now().Format("02/Jan/2006 15:04:05"), request.Method, request.RequestURI, request.Proto))) - io.Copy(conn, reader) - } -} - -//TODO: Implement HTTPS forwarding -//func (s *Session) GetForwardedConnectionTLS(host string, sshConn *ssh.ServerConn, payload []byte, originPort, port uint32, path, method, proto string) (*http.Response, error) { -// channelPayload := createForwardedTCPIPPayload(host, originPort, port) -// channel, reqs, err := sshConn.OpenChannel("forwarded-tcpip", channelPayload) -// if err != nil { -// return nil, err -// } -// defer channel.Close() -// -// initalPayload := bytes.NewReader(payload) -// io.Copy(channel, initalPayload) -// -// go func() { -// for req := range reqs { -// req.Reply(false, nil) -// } -// }() -// -// reader := bufio.NewReader(channel) -// _, err = reader.Peek(1) -// if err == io.EOF { -// return nil, err -// } else { -// s.ConnChannels[0].Write([]byte(fmt.Sprintf("\033[32m %s -- [%s] \"%s %s %s\" \r\n \033[0m", host, time.Now().Format("02/Jan/2006 15:04:05"), method, path, proto))) -// response, err := http.ReadResponse(reader, nil) -// if err != nil { -// return nil, err -// } -// return response, err -// } -//} - func writeSSHString(buffer *bytes.Buffer, str string) { binary.Write(buffer, binary.BigEndian, uint32(len(str))) buffer.WriteString(str) @@ -333,7 +488,7 @@ func ParseAddr(addr string) (string, uint32) { return host, uint32(port) } -func createForwardedTCPIPPayload(host string, originPort, port uint32) []byte { +func createForwardedTCPIPPayload(host string, originPort, port uint16) []byte { var buf bytes.Buffer writeSSHString(&buf, "localhost") diff --git a/session/session.go b/session/session.go index 3d308b9..ce565b2 100644 --- a/session/session.go +++ b/session/session.go @@ -7,9 +7,17 @@ import ( "net" ) +type STATUS string + +const ( + RUNNING STATUS = "running" + SETUP STATUS = "setup" +) + type Session struct { ID uuid.UUID Slug string + Status STATUS ConnChannels []ssh.Channel Connection *ssh.ServerConn GlobalRequest <-chan *ssh.Request @@ -18,6 +26,7 @@ type Session struct { ForwardedPort uint16 Done chan bool ForwardedChannel ssh.Channel + SlugChannel chan bool } type TunnelType string @@ -38,11 +47,13 @@ func init() { func New(conn *ssh.ServerConn, sshChannel <-chan ssh.NewChannel, req <-chan *ssh.Request) *Session { session := &Session{ ID: uuid.New(), + Status: SETUP, Slug: "", ConnChannels: []ssh.Channel{}, Connection: conn, GlobalRequest: req, TunnelType: UNKNOWN, + SlugChannel: make(chan bool), Done: make(chan bool), } @@ -69,7 +80,6 @@ func (session *Session) Close() { continue } } - if err := session.Connection.Close(); err != nil { fmt.Println("Error closing connection : ", err.Error()) }