Files
tunnel-please/session/handler.go
2025-04-05 23:27:32 +07:00

513 lines
16 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package session
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"golang.org/x/crypto/ssh"
"golang.org/x/net/context"
"io"
"log"
"net"
"strconv"
"strings"
"time"
"tunnel_pls/utils"
)
type UserConnection struct {
Reader io.Reader
Writer net.Conn
Context context.Context
}
func (s *Session) handleGlobalRequest() {
for {
select {
case req := <-s.GlobalRequest:
if req == nil {
return
}
if req.Type == "tcpip-forward" {
s.handleTCPIPForward(req)
continue
} else {
req.Reply(false, nil)
}
case <-s.Done:
break
}
}
}
func (s *Session) handleTCPIPForward(req *ssh.Request) {
log.Println("Port forwarding request detected")
reader := bytes.NewReader(req.Payload)
addr, err := readSSHString(reader)
if err != nil {
log.Println("Failed to read address from payload:", err)
req.Reply(false, nil)
return
}
var portToBind uint32
if err := binary.Read(reader, binary.BigEndian, &portToBind); err != nil {
log.Println("Failed to read port from payload:", err)
req.Reply(false, nil)
return
}
if portToBind == 80 || portToBind == 443 {
s.TunnelType = HTTP
s.ForwardedPort = uint16(portToBind)
var slug string
for {
slug = utils.GenerateRandomString(32)
if _, ok := Clients[slug]; ok {
return
}
break
}
Clients[slug] = s
s.Slug = slug
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 {
s.ConnChannels[0].Write([]byte(fmt.Sprintf("Forwarding your traffic to http://%s.%s \r\n", slug, utils.Getenv("domain"))))
}
req.Reply(true, buf.Bytes())
} else {
s.TunnelType = TCP
log.Printf("Requested forwarding on %s:%d", addr, portToBind)
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", portToBind))
if err != nil {
log.Printf("Failed to bind to port %d: %v", portToBind, err)
req.Reply(false, nil)
return
}
s.Listener = listener
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) {
return
}
log.Printf("Error accepting connection: %v", err)
continue
}
go s.HandleForwardedConnection(UserConnection{
Reader: nil,
Writer: conn,
Context: context.Background(),
}, s.Connection, portToBind)
}
}()
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, uint32(portToBind))
log.Printf("Forwarding approved on port: %d", portToBind)
req.Reply(true, buf.Bytes())
}
}
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)
if err != nil {
log.Printf("Could not accept channel: %s", err)
return
}
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)
connection.Write([]byte("\b \b"))
}
continue
}
if char == '/' {
commandBuffer.Reset()
commandBuffer.WriteByte(char)
continue
}
if commandBuffer.Len() > 0 {
if char == 13 {
command := commandBuffer.String()
fmt.Println("User entered command:", command, "<>")
if command == "/bye" {
fmt.Println("Closing connection...")
s.Close()
break
} else if command == "/debug" {
fmt.Println(Clients)
} else if command == "/help" {
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("\r\nUnknown command"))
}
commandBuffer.Reset()
continue
}
commandBuffer.WriteByte(char)
continue
}
}
if err != nil {
if err != io.EOF {
log.Printf("Error reading from client: %s", err)
}
break
}
}
}()
go func() {
connection.Write([]byte("\033[H\033[2J"))
showWelcomeMessage(connection)
s.Status = RUNNING
go s.handleGlobalRequest()
for req := range requests {
switch req.Type {
case "shell", "pty-req", "window-change":
req.Reply(true, nil)
default:
fmt.Println("Unknown request type of : ", req.Type)
req.Reply(false, nil)
}
}
}()
}
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())
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)
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 {
s.ConnChannels[0].Write([]byte("Could not forward request to the tunnel addr 1\r\n"))
return
}
io.Copy(conn.Writer, reader)
}
func writeSSHString(buffer *bytes.Buffer, str string) {
binary.Write(buffer, binary.BigEndian, uint32(len(str)))
buffer.WriteString(str)
}
func ParseAddr(addr string) (string, uint32) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
log.Printf("Failed to parse origin address: %s from address %s", err.Error(), addr)
return "0.0.0.0", uint32(0)
}
port, _ := strconv.Atoi(portStr)
return host, uint32(port)
}
func createForwardedTCPIPPayload(host string, originPort, port uint16) []byte {
var buf bytes.Buffer
writeSSHString(&buf, "localhost")
binary.Write(&buf, binary.BigEndian, uint32(port))
writeSSHString(&buf, host)
binary.Write(&buf, binary.BigEndian, uint32(originPort))
return buf.Bytes()
}
func readSSHString(reader *bytes.Reader) (string, error) {
var length uint32
if err := binary.Read(reader, binary.BigEndian, &length); err != nil {
return "", err
}
strBytes := make([]byte, length)
if _, err := reader.Read(strBytes); err != nil {
return "", err
}
return string(strBytes), nil
}