Files
tunnel-please/session/interaction/interaction.go
bagas ba5f702e36
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 4m38s
feat: add droping conn command
2025-12-07 15:26:37 +07:00

479 lines
12 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 interaction
import (
"bytes"
"fmt"
"io"
"log"
"strings"
"time"
"tunnel_pls/session/slug"
"tunnel_pls/types"
"tunnel_pls/utils"
"golang.org/x/crypto/ssh"
)
var forbiddenSlug = []string{
"ping",
}
type Lifecycle interface {
Close() error
}
type Controller interface {
SendMessage(message string)
HandleUserInput()
HandleCommand(command string)
HandleSlugEditMode(char byte)
HandleSlugSave()
HandleSlugCancel()
HandleSlugUpdateError()
ShowWelcomeMessage()
DisplaySlugEditor()
SetChannel(channel ssh.Channel)
SetLifecycle(lifecycle Lifecycle)
SetSlugModificator(func(oldSlug, newSlug string) bool)
WaitForKeyPress()
ShowForwardingMessage()
}
type Forwarder interface {
Close() error
GetTunnelType() types.TunnelType
GetForwardedPort() uint16
DropAllForwarder() int
GetForwarderCount() int
}
type Interaction struct {
InputLength int
CommandBuffer *bytes.Buffer
InteractiveMode bool
InteractionType types.InteractionType
EditSlug string
channel ssh.Channel
SlugManager slug.Manager
Forwarder Forwarder
Lifecycle Lifecycle
pendingExit bool
updateClientSlug func(oldSlug, newSlug string) bool
}
func (i *Interaction) SetLifecycle(lifecycle Lifecycle) {
i.Lifecycle = lifecycle
}
func (i *Interaction) SetChannel(channel ssh.Channel) {
i.channel = channel
}
func (i *Interaction) SendMessage(message string) {
if i.channel != nil {
_, err := i.channel.Write([]byte(message))
if err != nil && err != io.EOF {
log.Printf("Error writing to channel: %v", err)
return
}
}
}
func (i *Interaction) HandleUserInput() {
buf := make([]byte, 1)
i.InteractiveMode = false
for {
n, err := i.channel.Read(buf)
if err != nil {
if err != io.EOF {
log.Printf("Error reading from client: %s", err)
}
break
}
if n > 0 {
char := buf[0]
if i.InteractiveMode {
if i.InteractionType == types.Slug {
i.HandleSlugEditMode(char)
} else if i.InteractionType == types.Drop {
i.HandleDropMode(char)
}
continue
}
if i.pendingExit {
if char != 3 {
i.pendingExit = false
i.SendMessage("Operation canceled.\r\n")
}
}
if char == 3 {
if i.pendingExit {
i.SendMessage("Closing connection...\r\n")
err = i.Lifecycle.Close()
if err != nil {
log.Printf("failed to close session: %v", err)
return
}
return
}
i.SendMessage("Please press Ctrl+C again to disconnect.\r\n")
i.pendingExit = true
}
i.SendMessage(string(buf[:n]))
if char == 8 || char == 127 {
if i.InputLength > 0 {
i.SendMessage("\b \b")
}
if i.CommandBuffer.Len() > 0 {
i.CommandBuffer.Truncate(i.CommandBuffer.Len() - 1)
}
continue
}
i.InputLength += n
if char == '/' {
i.CommandBuffer.Reset()
i.CommandBuffer.WriteByte(char)
continue
}
if i.CommandBuffer.Len() > 0 {
if char == 13 {
i.SendMessage("\033[K")
i.HandleCommand(i.CommandBuffer.String())
continue
}
i.CommandBuffer.WriteByte(char)
}
if char == 13 {
i.SendMessage("\033[K")
}
}
}
}
func (i *Interaction) HandleSlugEditMode(char byte) {
if char == 13 {
i.HandleSlugSave()
} else if char == 27 || char == 3 {
i.HandleSlugCancel()
} else if char == 8 || char == 127 {
if len(i.EditSlug) > 0 {
i.EditSlug = (i.EditSlug)[:len(i.EditSlug)-1]
i.SendMessage("\r\033[K")
i.SendMessage("➤ " + i.EditSlug + "." + utils.Getenv("domain"))
}
} else if char >= 32 && char <= 126 {
if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' {
i.EditSlug += string(char)
i.SendMessage("\r\033[K")
i.SendMessage("➤ " + i.EditSlug + "." + utils.Getenv("domain"))
}
}
}
func (i *Interaction) HandleSlugSave() {
isValid := isValidSlug(i.EditSlug)
i.SendMessage("\033[H\033[2J")
if isValid {
oldSlug := i.SlugManager.Get()
newSlug := i.EditSlug
if !i.updateClientSlug(oldSlug, newSlug) {
i.HandleSlugUpdateError()
return
}
i.SendMessage("\r\n\r\n✅ SUBDOMAIN UPDATED ✅\r\n\r\n")
i.SendMessage("Your new address is: " + newSlug + "." + utils.Getenv("domain") + "\r\n\r\n")
i.SendMessage("Press any key to continue...\r\n")
} else if isForbiddenSlug(i.EditSlug) {
i.SendMessage("\r\n\r\n❌ FORBIDDEN SUBDOMAIN ❌\r\n\r\n")
i.SendMessage("This subdomain is not allowed.\r\n")
i.SendMessage("Please try a different subdomain.\r\n\r\n")
i.SendMessage("Press any key to continue...\r\n")
} else {
i.SendMessage("\r\n\r\n❌ INVALID SUBDOMAIN ❌\r\n\r\n")
i.SendMessage("Use only lowercase letters, numbers, and hyphens.\r\n")
i.SendMessage("Length must be 3-20 characters and cannot start or end with a hyphen.\r\n\r\n")
i.SendMessage("Press any key to continue...\r\n")
}
i.WaitForKeyPress()
i.SendMessage("\033[H\033[2J")
i.ShowWelcomeMessage()
domain := utils.Getenv("domain")
protocol := "http"
if utils.Getenv("tls_enabled") == "true" {
protocol = "https"
}
i.SendMessage(fmt.Sprintf("Forwarding your traffic to %s://%s.%s \r\n", protocol, i.SlugManager.Get(), domain))
i.InteractiveMode = false
i.CommandBuffer.Reset()
}
func (i *Interaction) HandleSlugCancel() {
i.InteractiveMode = false
i.SendMessage("\033[H\033[2J")
i.SendMessage("\r\n\r\n⚠ SUBDOMAIN EDIT CANCELLED ⚠️\r\n\r\n")
i.SendMessage("Press any key to continue...\r\n")
i.WaitForKeyPress()
i.SendMessage("\033[H\033[2J")
i.ShowWelcomeMessage()
i.ShowForwardingMessage()
i.CommandBuffer.Reset()
}
func (i *Interaction) HandleSlugUpdateError() {
i.SendMessage("\r\n\r\n❌ SERVER ERROR ❌\r\n\r\n")
i.SendMessage("Failed to update subdomain. You will be disconnected in 5 seconds.\r\n\r\n")
for iter := 5; iter > 0; iter-- {
i.SendMessage(fmt.Sprintf("Disconnecting in %d...\r\n", iter))
time.Sleep(1 * time.Second)
}
err := i.Lifecycle.Close()
if err != nil {
log.Printf("failed to close session: %v", err)
return
}
}
func (i *Interaction) HandleCommand(command string) {
switch command {
case "/bye":
i.SendMessage("Closing connection...\r\n")
err := i.Lifecycle.Close()
if err != nil {
log.Printf("failed to close session: %v", err)
return
}
return
case "/help":
i.SendMessage("\r\nAvailable commands: /bye, /help, /clear, /slug\r\n")
case "/clear":
i.SendMessage("\033[H\033[2J")
i.ShowWelcomeMessage()
i.ShowForwardingMessage()
case "/slug":
if i.Forwarder.GetTunnelType() != types.HTTP {
i.SendMessage(fmt.Sprintf("\r\n%s tunnels cannot have custom subdomains", i.Forwarder.GetTunnelType()))
} else {
i.InteractiveMode = true
i.InteractionType = types.Slug
i.EditSlug = i.SlugManager.Get()
i.SendMessage("\033[H\033[2J")
i.DisplaySlugEditor()
i.SendMessage("➤ " + i.EditSlug + "." + utils.Getenv("domain"))
}
case "/drop":
i.InteractiveMode = true
i.InteractionType = types.Drop
i.SendMessage("\033[H\033[2J")
i.ShowDropMessage()
default:
i.SendMessage("Unknown command\r\n")
}
i.CommandBuffer.Reset()
}
func (i *Interaction) ShowForwardingMessage() {
domain := utils.Getenv("domain")
if i.Forwarder.GetTunnelType() == types.HTTP {
protocol := "http"
if utils.Getenv("tls_enabled") == "true" {
protocol = "https"
}
i.SendMessage(fmt.Sprintf("Forwarding your traffic to %s://%s.%s \r\n", protocol, i.SlugManager.Get(), domain))
} else {
i.SendMessage(fmt.Sprintf("Forwarding your traffic to tcp://%s:%d \r\n", domain, i.Forwarder.GetForwardedPort()))
}
}
func (i *Interaction) HandleDropMode(char byte) {
if char == 13 || char == 121 || char == 89 {
count := i.Forwarder.DropAllForwarder()
i.SendMessage("\033[H\033[2J")
i.SendMessage(fmt.Sprintf("Dropped %d forwarders\r\n", count))
i.SendMessage("Press any key to continue...\r\n")
i.InteractiveMode = false
i.InteractionType = ""
i.WaitForKeyPress()
i.SendMessage("\033[H\033[2J")
i.ShowWelcomeMessage()
i.ShowForwardingMessage()
} else if char == 27 || char == 110 || char == 78 || char == 3 {
i.SendMessage("\033[H\033[2J")
i.SendMessage(fmt.Sprintf("Dropping canceled.\r\n"))
i.SendMessage("Press any key to continue...\r\n")
i.InteractiveMode = false
i.InteractionType = ""
i.WaitForKeyPress()
i.SendMessage("\033[H\033[2J")
i.ShowWelcomeMessage()
i.ShowForwardingMessage()
}
}
func (i *Interaction) ShowDropMessage() {
const paddingRight = 4
confirmText := fmt.Sprintf(" ║ Drop ALL %d active connections?", i.Forwarder.GetForwarderCount())
boxWidth := len(confirmText) + paddingRight + 1
if boxWidth < 50 {
boxWidth = 50
}
topBorder := " ╔" + strings.Repeat("═", boxWidth-4) + "╗\r\n"
title := centerText("DROP CONFIRMATION", boxWidth-4)
header := " ║" + title + "║\r\n"
midBorder := " ╠" + strings.Repeat("═", boxWidth-4) + "╣\r\n"
emptyLine := " ║" + strings.Repeat(" ", boxWidth-4) + "║\r\n"
confirmLine := confirmText + strings.Repeat(" ", boxWidth-len(confirmText)+1) + "║\r\n"
controlText := " ║ [Enter/Y] Confirm [N/Esc] Cancel"
controlLine := controlText + strings.Repeat(" ", boxWidth-len(controlText)+1) + "║\r\n"
bottomBorder := " ╚" + strings.Repeat("═", boxWidth-4) + "╝\r\n"
asciiArt := topBorder +
header +
midBorder +
emptyLine +
confirmLine +
emptyLine +
controlLine +
emptyLine +
bottomBorder
i.SendMessage("\r\n" + asciiArt)
i.SendMessage("\r\n\r\n")
}
func (i *Interaction) ShowWelcomeMessage() {
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`,
` - '/drop' : Drop all active forwarders`,
}
for _, line := range asciiArt {
i.SendMessage("\r\n" + line)
}
i.SendMessage("\r\n\r\n")
}
func (i *Interaction) DisplaySlugEditor() {
domain := utils.Getenv("domain")
fullDomain := i.SlugManager.Get() + "." + domain
const paddingRight = 4
contentLine := " ║ Current: " + fullDomain
boxWidth := len(contentLine) + paddingRight + 1
if boxWidth < 50 {
boxWidth = 50
}
topBorder := " ╔" + strings.Repeat("═", boxWidth-4) + "╗\r\n"
title := centerText("SUBDOMAIN EDITOR", boxWidth-4)
header := " ║" + title + "║\r\n"
midBorder := " ╠" + strings.Repeat("═", boxWidth-4) + "╣\r\n"
emptyLine := " ║" + strings.Repeat(" ", boxWidth-4) + "║\r\n"
currentLineContent := fmt.Sprintf(" ║ Current: %s", fullDomain)
currentLine := currentLineContent + strings.Repeat(" ", boxWidth-len(currentLineContent)+1) + "║\r\n"
saveCancel := " ║ [Enter] Save | [Esc] Cancel" + strings.Repeat(" ", boxWidth-35) + "║\r\n"
bottomBorder := " ╚" + strings.Repeat("═", boxWidth-4) + "╝\r\n"
i.SendMessage("\r\n\r\n")
i.SendMessage(topBorder)
i.SendMessage(header)
i.SendMessage(midBorder)
i.SendMessage(emptyLine)
i.SendMessage(currentLine)
i.SendMessage(emptyLine)
i.SendMessage(emptyLine)
i.SendMessage(midBorder)
i.SendMessage(saveCancel)
i.SendMessage(bottomBorder)
i.SendMessage("\r\n\r\n")
}
func (i *Interaction) SetSlugModificator(modificator func(oldSlug, newSlug string) bool) {
i.updateClientSlug = modificator
}
func (i *Interaction) WaitForKeyPress() {
keyBuf := make([]byte, 1)
for {
_, err := i.channel.Read(keyBuf)
if err == nil {
break
}
}
}
func centerText(text string, width int) string {
padding := (width - len(text)) / 2
if padding < 0 {
padding = 0
}
return strings.Repeat(" ", padding) + text + strings.Repeat(" ", width-len(text)-padding)
}
func isValidSlug(slug string) bool {
if len(slug) < 3 || len(slug) > 20 {
return false
}
if slug[0] == '-' || slug[len(slug)-1] == '-' {
return false
}
for _, c := range slug {
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
return false
}
}
return true
}
func isForbiddenSlug(slug string) bool {
for _, s := range forbiddenSlug {
if slug == s {
return true
}
}
return false
}