diff --git a/internal/random/random.go b/internal/random/random.go index a67c9bf..929cc7b 100644 --- a/internal/random/random.go +++ b/internal/random/random.go @@ -1,18 +1,18 @@ package random -import ( - mathrand "math/rand" - "strings" - "time" -) +import "crypto/rand" -func GenerateRandomString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - seededRand := mathrand.New(mathrand.NewSource(time.Now().UnixNano() + int64(mathrand.Intn(9999)))) - var result strings.Builder - for i := 0; i < length; i++ { - randomIndex := seededRand.Intn(len(charset)) - result.WriteString(string(charset[randomIndex])) +func GenerateRandomString(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, length) + + if _, err := rand.Read(b); err != nil { + return "", err } - return result.String() + + for i := range b { + b[i] = charset[int(b[i])%len(charset)] + } + + return string(b), nil } diff --git a/server/tls.go b/server/tls.go index 8cc8afe..fc67733 100644 --- a/server/tls.go +++ b/server/tls.go @@ -301,22 +301,16 @@ func (tm *tlsManager) initCertMagic() error { func (tm *tlsManager) getTLSConfig() *tls.Config { return &tls.Config{ GetCertificate: tm.getCertificate, - MinVersion: tls.VersionTLS13, - MaxVersion: tls.VersionTLS13, - SessionTicketsDisabled: false, - - CipherSuites: []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - }, + MinVersion: tls.VersionTLS13, + MaxVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{ tls.X25519, }, - ClientAuth: tls.NoClientCert, - NextProtos: nil, + SessionTicketsDisabled: false, + ClientAuth: tls.NoClientCert, } } diff --git a/session/interaction/coming_soon.go b/session/interaction/coming_soon.go new file mode 100644 index 0000000..006dd33 --- /dev/null +++ b/session/interaction/coming_soon.go @@ -0,0 +1,83 @@ +package interaction + +import ( + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +func (m *model) comingSoonUpdate(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + m.showingComingSoon = false + return m, tea.Batch(tea.ClearScreen, textinput.Blink) +} + +func (m *model) comingSoonView() string { + isCompact := shouldUseCompactLayout(m.width, 60) + + var boxPadding int + var boxMargin int + if isCompact { + boxPadding = 1 + boxMargin = 1 + } else { + boxPadding = 3 + boxMargin = 2 + } + + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")). + PaddingTop(1). + PaddingBottom(1) + + messageBoxWidth := getResponsiveWidth(m.width, 10, 30, 60) + messageBoxStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#1A1A2E")). + Bold(true). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#7D56F4")). + Padding(1, boxPadding). + MarginTop(boxMargin). + MarginBottom(boxMargin). + Width(messageBoxWidth). + Align(lipgloss.Center) + + helpStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#666666")). + Italic(true). + MarginTop(1) + + var b strings.Builder + b.WriteString("\n\n") + + var title string + if shouldUseCompactLayout(m.width, 40) { + title = "Coming Soon" + } else { + title = "⏳ Coming Soon" + } + b.WriteString(titleStyle.Render(title)) + b.WriteString("\n\n") + + var message string + if shouldUseCompactLayout(m.width, 50) { + message = "Coming soon!\nStay tuned." + } else { + message = "🚀 This feature is coming very soon!\n Stay tuned for updates." + } + b.WriteString(messageBoxStyle.Render(message)) + b.WriteString("\n\n") + + var helpText string + if shouldUseCompactLayout(m.width, 60) { + helpText = "Press any key..." + } else { + helpText = "This message will disappear in 5 seconds or press any key..." + } + b.WriteString(helpStyle.Render(helpText)) + + return b.String() +} diff --git a/session/interaction/commands.go b/session/interaction/commands.go new file mode 100644 index 0000000..e884aeb --- /dev/null +++ b/session/interaction/commands.go @@ -0,0 +1,83 @@ +package interaction + +import ( + "strings" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +func (m *model) commandsUpdate(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch { + case key.Matches(msg, m.keymap.quit): + m.showingCommands = false + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + case msg.String() == "enter": + selectedItem := m.commandList.SelectedItem() + if selectedItem != nil { + item := selectedItem.(commandItem) + if item.name == "slug" { + m.showingCommands = false + m.editingSlug = true + m.slugInput.SetValue(m.interaction.slug.String()) + m.slugInput.Focus() + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + } else if item.name == "tunnel-type" { + m.showingCommands = false + m.showingComingSoon = true + return m, tea.Batch(tickCmd(5*time.Second), tea.ClearScreen, textinput.Blink) + } + m.showingCommands = false + return m, nil + } + case msg.String() == "esc": + m.showingCommands = false + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + } + m.commandList, cmd = m.commandList.Update(msg) + return m, cmd +} + +func (m *model) commandsView() string { + isCompact := shouldUseCompactLayout(m.width, 60) + + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")). + PaddingTop(1). + PaddingBottom(1) + + helpStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#666666")). + Italic(true). + MarginTop(1) + + var b strings.Builder + b.WriteString("\n") + + var title string + if shouldUseCompactLayout(m.width, 40) { + title = "Commands" + } else { + title = "⚡ Commands" + } + b.WriteString(titleStyle.Render(title)) + b.WriteString("\n\n") + b.WriteString(m.commandList.View()) + b.WriteString("\n") + + var helpText string + if isCompact { + helpText = "↑/↓ Nav • Enter Select • Esc Cancel" + } else { + helpText = "↑/↓ Navigate • Enter Select • Esc Cancel" + } + b.WriteString(helpStyle.Render(helpText)) + + return b.String() +} diff --git a/session/interaction/dashboard.go b/session/interaction/dashboard.go new file mode 100644 index 0000000..eee08db --- /dev/null +++ b/session/interaction/dashboard.go @@ -0,0 +1,186 @@ +package interaction + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +func (m *model) dashboardUpdate(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch { + case key.Matches(msg, m.keymap.quit): + m.quitting = true + return m, tea.Batch(tea.ClearScreen, textinput.Blink, tea.Quit) + case key.Matches(msg, m.keymap.command): + m.showingCommands = true + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + } + return m, nil +} + +func (m *model) dashboardView() string { + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")). + PaddingTop(1) + + subtitleStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Italic(true) + + urlStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Underline(true). + Italic(true) + + urlBoxStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#04B575")). + Bold(true). + Italic(true) + + keyHintStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Bold(true) + + var b strings.Builder + + isCompact := shouldUseCompactLayout(m.width, 85) + + var asciiArtMargin int + if isCompact { + asciiArtMargin = 0 + } else { + asciiArtMargin = 1 + } + + asciiArtStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")). + MarginBottom(asciiArtMargin) + + var asciiArt string + if shouldUseCompactLayout(m.width, 50) { + asciiArt = "TUNNEL PLS" + } else if isCompact { + asciiArt = ` + ▀█▀ █ █ █▄ █ █▄ █ ██▀ █ ▄▀▀ █ ▄▀▀ + █ ▀▄█ █ ▀█ █ ▀█ █▄▄ █▄▄ ▄█▀ █▄▄ ▄█▀` + } else { + asciiArt = ` + ████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗ ██████╗ ██╗ ███████╗ + ╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║ ██╔══██╗██║ ██╔════╝ + ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║ ██████╔╝██║ ███████╗ + ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║ ██╔═══╝ ██║ ╚════██║ + ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗ ██║ ███████╗███████║ + ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝` + } + + b.WriteString(asciiArtStyle.Render(asciiArt)) + b.WriteString("\n") + + if !shouldUseCompactLayout(m.width, 60) { + b.WriteString(subtitleStyle.Render("Secure tunnel service by Bagas • ")) + b.WriteString(urlStyle.Render("https://fossy.my.id")) + b.WriteString("\n\n") + } else { + b.WriteString("\n") + } + + boxMaxWidth := getResponsiveWidth(m.width, 10, 40, 80) + var boxPadding int + var boxMargin int + if isCompact { + boxPadding = 1 + boxMargin = 1 + } else { + boxPadding = 2 + boxMargin = 2 + } + + responsiveInfoBox := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#7D56F4")). + Padding(1, boxPadding). + MarginTop(boxMargin). + MarginBottom(boxMargin). + Width(boxMaxWidth) + + authenticatedUser := m.interaction.lifecycle.User() + + userInfoStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")). + Bold(true) + + sectionHeaderStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Bold(true) + + addressStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")) + + var infoContent string + if shouldUseCompactLayout(m.width, 70) { + infoContent = fmt.Sprintf("👤 %s\n\n%s\n%s", + userInfoStyle.Render(authenticatedUser), + sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"), + addressStyle.Render(fmt.Sprintf(" %s", urlBoxStyle.Render(m.getTunnelURL())))) + } else { + infoContent = fmt.Sprintf("👤 Authenticated as: %s\n\n%s\n %s", + userInfoStyle.Render(authenticatedUser), + sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"), + addressStyle.Render(urlBoxStyle.Render(m.getTunnelURL()))) + } + + b.WriteString(responsiveInfoBox.Render(infoContent)) + b.WriteString("\n") + + var quickActionsTitle string + if shouldUseCompactLayout(m.width, 50) { + quickActionsTitle = "Actions" + } else if isCompact { + quickActionsTitle = "Quick Actions" + } else { + quickActionsTitle = "✨ Quick Actions" + } + b.WriteString(titleStyle.Render(quickActionsTitle)) + b.WriteString("\n") + + var featureMargin int + if isCompact { + featureMargin = 1 + } else { + featureMargin = 2 + } + + compactFeatureStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")). + MarginLeft(featureMargin) + + var commandsText string + var quitText string + if shouldUseCompactLayout(m.width, 60) { + commandsText = fmt.Sprintf(" %s Commands", keyHintStyle.Render("[C]")) + quitText = fmt.Sprintf(" %s Quit", keyHintStyle.Render("[Q]")) + } else { + commandsText = fmt.Sprintf(" %s Open commands menu", keyHintStyle.Render("[C]")) + quitText = fmt.Sprintf(" %s Quit application", keyHintStyle.Render("[Q]")) + } + + b.WriteString(compactFeatureStyle.Render(commandsText)) + b.WriteString("\n") + b.WriteString(compactFeatureStyle.Render(quitText)) + + if !shouldUseCompactLayout(m.width, 70) { + b.WriteString("\n\n") + footerStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#666666")). + Italic(true) + b.WriteString(footerStyle.Render("Press 'C' to customize your tunnel settings")) + } + + return b.String() +} diff --git a/session/interaction/interaction.go b/session/interaction/interaction.go index 86bbec7..c3bfc8a 100644 --- a/session/interaction/interaction.go +++ b/session/interaction/interaction.go @@ -2,12 +2,8 @@ package interaction import ( "context" - "fmt" "log" - "strings" - "time" "tunnel_pls/internal/config" - "tunnel_pls/internal/random" "tunnel_pls/session/slug" "tunnel_pls/types" @@ -84,45 +80,6 @@ func (i *interaction) SetWH(w, h int) { } } -type commandItem struct { - name string - desc string -} - -type model struct { - domain string - protocol string - tunnelType types.TunnelType - port uint16 - keymap keymap - help help.Model - quitting bool - showingCommands bool - editingSlug bool - showingComingSoon bool - commandList list.Model - slugInput textinput.Model - slugError string - interaction *interaction - width int - height int -} - -func (m *model) getTunnelURL() string { - if m.tunnelType == types.HTTP { - return buildURL(m.protocol, m.interaction.slug.String(), m.domain) - } - return fmt.Sprintf("tcp://%s:%d", m.domain, m.port) -} - -type keymap struct { - quit key.Binding - command key.Binding - random key.Binding -} - -type tickMsg time.Time - func New(slug slug.Slug, forwarder Forwarder) Interaction { ctx, cancel := context.WithCancel(context.Background()) return &interaction{ @@ -159,47 +116,7 @@ func (i *interaction) Stop() { } } -func getResponsiveWidth(screenWidth, padding, minWidth, maxWidth int) int { - width := screenWidth - padding - if width > maxWidth { - width = maxWidth - } - if width < minWidth { - width = minWidth - } - return width -} - -func shouldUseCompactLayout(width int, threshold int) bool { - return width < threshold -} - -func truncateString(s string, maxLength int) string { - if len(s) <= maxLength { - return s - } - if maxLength < 4 { - return s[:maxLength] - } - return s[:maxLength-3] + "..." -} - -func (i commandItem) FilterValue() string { return i.name } -func (i commandItem) Title() string { return i.name } -func (i commandItem) Description() string { return i.desc } - -func tickCmd(d time.Duration) tea.Cmd { - return tea.Tick(d, func(t time.Time) tea.Msg { - return tickMsg(t) - }) -} - -func (m *model) Init() tea.Cmd { - return tea.Batch(textinput.Blink, tea.WindowSize()) -} - func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd switch msg := msg.(type) { case tickMsg: @@ -225,93 +142,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: if m.showingComingSoon { - m.showingComingSoon = false - return m, tea.Batch(tea.ClearScreen, textinput.Blink) + return m.comingSoonUpdate(msg) } if m.editingSlug { - if m.tunnelType != types.HTTP { - m.editingSlug = false - m.slugError = "" - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - } - switch msg.String() { - case "esc": - m.editingSlug = false - m.slugError = "" - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - case "enter": - inputValue := m.slugInput.Value() - if err := m.interaction.sessionRegistry.Update(m.interaction.lifecycle.User(), types.SessionKey{ - Id: m.interaction.slug.String(), - Type: types.HTTP, - }, types.SessionKey{ - Id: inputValue, - Type: types.HTTP, - }); err != nil { - m.slugError = err.Error() - return m, nil - } - m.editingSlug = false - m.slugError = "" - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - case "ctrl+c": - m.editingSlug = false - m.slugError = "" - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - default: - if key.Matches(msg, m.keymap.random) { - newSubdomain := generateRandomSubdomain() - m.slugInput.SetValue(newSubdomain) - m.slugError = "" - m.slugInput, cmd = m.slugInput.Update(msg) - return m, cmd - } - m.slugError = "" - m.slugInput, cmd = m.slugInput.Update(msg) - return m, cmd - } + return m.slugUpdate(msg) } if m.showingCommands { - switch { - case key.Matches(msg, m.keymap.quit): - m.showingCommands = false - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - case msg.String() == "enter": - selectedItem := m.commandList.SelectedItem() - if selectedItem != nil { - item := selectedItem.(commandItem) - if item.name == "slug" { - m.showingCommands = false - m.editingSlug = true - m.slugInput.SetValue(m.interaction.slug.String()) - m.slugInput.Focus() - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - } else if item.name == "tunnel-type" { - m.showingCommands = false - m.showingComingSoon = true - return m, tea.Batch(tickCmd(5*time.Second), tea.ClearScreen, textinput.Blink) - } - m.showingCommands = false - return m, nil - } - case msg.String() == "esc": - m.showingCommands = false - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - } - m.commandList, cmd = m.commandList.Update(msg) - return m, cmd + return m.commandsUpdate(msg) } - switch { - case key.Matches(msg, m.keymap.quit): - m.quitting = true - return m, tea.Batch(tea.ClearScreen, textinput.Blink, tea.Quit) - case key.Matches(msg, m.keymap.command): - m.showingCommands = true - return m, tea.Batch(tea.ClearScreen, textinput.Blink) - } + return m.dashboardUpdate(msg) } return m, nil @@ -323,448 +165,24 @@ func (i *interaction) Redraw() { } } -func (m *model) helpView() string { - return "\n" + m.help.ShortHelpView([]key.Binding{ - m.keymap.command, - m.keymap.quit, - }) -} - func (m *model) View() string { if m.quitting { return "" } if m.showingComingSoon { - isCompact := shouldUseCompactLayout(m.width, 60) - - var boxPadding int - var boxMargin int - if isCompact { - boxPadding = 1 - boxMargin = 1 - } else { - boxPadding = 3 - boxMargin = 2 - } - - titleStyle := lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#7D56F4")). - PaddingTop(1). - PaddingBottom(1) - - messageBoxWidth := getResponsiveWidth(m.width, 10, 30, 60) - messageBoxStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FAFAFA")). - Background(lipgloss.Color("#1A1A2E")). - Bold(true). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#7D56F4")). - Padding(1, boxPadding). - MarginTop(boxMargin). - MarginBottom(boxMargin). - Width(messageBoxWidth). - Align(lipgloss.Center) - - helpStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#666666")). - Italic(true). - MarginTop(1) - - var b strings.Builder - b.WriteString("\n\n") - - var title string - if shouldUseCompactLayout(m.width, 40) { - title = "Coming Soon" - } else { - title = "⏳ Coming Soon" - } - b.WriteString(titleStyle.Render(title)) - b.WriteString("\n\n") - - var message string - if shouldUseCompactLayout(m.width, 50) { - message = "Coming soon!\nStay tuned." - } else { - message = "🚀 This feature is coming very soon!\n Stay tuned for updates." - } - b.WriteString(messageBoxStyle.Render(message)) - b.WriteString("\n\n") - - var helpText string - if shouldUseCompactLayout(m.width, 60) { - helpText = "Press any key..." - } else { - helpText = "This message will disappear in 5 seconds or press any key..." - } - b.WriteString(helpStyle.Render(helpText)) - - return b.String() + return m.comingSoonView() } if m.editingSlug { - isCompact := shouldUseCompactLayout(m.width, 70) - isVeryCompact := shouldUseCompactLayout(m.width, 50) - - var boxPadding int - var boxMargin int - if isVeryCompact { - boxPadding = 1 - boxMargin = 1 - } else if isCompact { - boxPadding = 1 - boxMargin = 1 - } else { - boxPadding = 2 - boxMargin = 2 - } - - titleStyle := lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#7D56F4")). - PaddingTop(1). - PaddingBottom(1) - - instructionStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FAFAFA")). - MarginTop(1) - - inputBoxStyle := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#7D56F4")). - Padding(1, boxPadding). - MarginTop(boxMargin). - MarginBottom(boxMargin) - - helpStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#666666")). - Italic(true). - MarginTop(1) - - errorBoxStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF0000")). - Background(lipgloss.Color("#3D0000")). - Bold(true). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FF0000")). - Padding(0, boxPadding). - MarginTop(1). - MarginBottom(1) - - rulesBoxWidth := getResponsiveWidth(m.width, 10, 30, 60) - rulesBoxStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FAFAFA")). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#7D56F4")). - Padding(0, boxPadding). - MarginTop(1). - MarginBottom(1). - Width(rulesBoxWidth) - - var b strings.Builder - var title string - if isVeryCompact { - title = "Edit Subdomain" - } else { - title = "🔧 Edit Subdomain" - } - b.WriteString(titleStyle.Render(title)) - b.WriteString("\n\n") - - if m.tunnelType != types.HTTP { - warningBoxWidth := getResponsiveWidth(m.width, 10, 30, 60) - warningBoxStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFA500")). - Background(lipgloss.Color("#3D2000")). - Bold(true). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FFA500")). - Padding(1, boxPadding). - MarginTop(boxMargin). - MarginBottom(boxMargin). - Width(warningBoxWidth) - - var warningText string - if isVeryCompact { - warningText = "⚠️ TCP tunnels don't support custom subdomains." - } else { - warningText = "⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization." - } - b.WriteString(warningBoxStyle.Render(warningText)) - b.WriteString("\n\n") - - var helpText string - if isVeryCompact { - helpText = "Press any key to go back" - } else { - helpText = "Press Enter or Esc to go back" - } - b.WriteString(helpStyle.Render(helpText)) - return b.String() - } - - var rulesContent string - if isVeryCompact { - rulesContent = "Rules:\n3-20 chars\na-z, 0-9, -\nNo leading/trailing -" - } else if isCompact { - rulesContent = "📋 Rules:\n • 3-20 chars\n • a-z, 0-9, -\n • No leading/trailing -" - } else { - rulesContent = "📋 Rules: \n\t• 3-20 chars \n\t• a-z, 0-9, - \n\t• No leading/trailing -" - } - b.WriteString(rulesBoxStyle.Render(rulesContent)) - b.WriteString("\n") - - var instruction string - if isVeryCompact { - instruction = "Custom subdomain:" - } else { - instruction = "Enter your custom subdomain:" - } - b.WriteString(instructionStyle.Render(instruction)) - b.WriteString("\n") - - if m.slugError != "" { - errorInputBoxStyle := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#FF0000")). - Padding(1, boxPadding). - MarginTop(boxMargin). - MarginBottom(1) - b.WriteString(errorInputBoxStyle.Render(m.slugInput.View())) - b.WriteString("\n") - b.WriteString(errorBoxStyle.Render("❌ " + m.slugError)) - b.WriteString("\n") - } else { - b.WriteString(inputBoxStyle.Render(m.slugInput.View())) - b.WriteString("\n") - } - - previewURL := buildURL(m.protocol, m.slugInput.Value(), m.domain) - previewWidth := getResponsiveWidth(m.width, 10, 30, 80) - - if len(previewURL) > previewWidth-10 { - previewURL = truncateString(previewURL, previewWidth-10) - } - - previewStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#04B575")). - Italic(true). - Width(previewWidth) - b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL))) - b.WriteString("\n") - - var helpText string - if isVeryCompact { - helpText = "Enter: save • CTRL+R: random • Esc: cancel" - } else { - helpText = "Press Enter to save • CTRL+R for random • Esc to cancel" - } - b.WriteString(helpStyle.Render(helpText)) - - return b.String() + return m.slugView() } if m.showingCommands { - isCompact := shouldUseCompactLayout(m.width, 60) - - titleStyle := lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#7D56F4")). - PaddingTop(1). - PaddingBottom(1) - - helpStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#666666")). - Italic(true). - MarginTop(1) - - var b strings.Builder - b.WriteString("\n") - - var title string - if shouldUseCompactLayout(m.width, 40) { - title = "Commands" - } else { - title = "⚡ Commands" - } - b.WriteString(titleStyle.Render(title)) - b.WriteString("\n\n") - b.WriteString(m.commandList.View()) - b.WriteString("\n") - - var helpText string - if isCompact { - helpText = "↑/↓ Nav • Enter Select • Esc Cancel" - } else { - helpText = "↑/↓ Navigate • Enter Select • Esc Cancel" - } - b.WriteString(helpStyle.Render(helpText)) - - return b.String() + return m.commandsView() } - titleStyle := lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#7D56F4")). - PaddingTop(1) - - subtitleStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#888888")). - Italic(true) - - urlStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7D56F4")). - Underline(true). - Italic(true) - - urlBoxStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#04B575")). - Bold(true). - Italic(true) - - keyHintStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7D56F4")). - Bold(true) - - var b strings.Builder - - isCompact := shouldUseCompactLayout(m.width, 85) - - var asciiArtMargin int - if isCompact { - asciiArtMargin = 0 - } else { - asciiArtMargin = 1 - } - - asciiArtStyle := lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#7D56F4")). - MarginBottom(asciiArtMargin) - - var asciiArt string - if shouldUseCompactLayout(m.width, 50) { - asciiArt = "TUNNEL PLS" - } else if isCompact { - asciiArt = ` - ▀█▀ █ █ █▄ █ █▄ █ ██▀ █ ▄▀▀ █ ▄▀▀ - █ ▀▄█ █ ▀█ █ ▀█ █▄▄ █▄▄ ▄█▀ █▄▄ ▄█▀` - } else { - asciiArt = ` - ████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗ ██████╗ ██╗ ███████╗ - ╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║ ██╔══██╗██║ ██╔════╝ - ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║ ██████╔╝██║ ███████╗ - ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║ ██╔═══╝ ██║ ╚════██║ - ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗ ██║ ███████╗███████║ - ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝` - } - - b.WriteString(asciiArtStyle.Render(asciiArt)) - b.WriteString("\n") - - if !shouldUseCompactLayout(m.width, 60) { - b.WriteString(subtitleStyle.Render("Secure tunnel service by Bagas • ")) - b.WriteString(urlStyle.Render("https://fossy.my.id")) - b.WriteString("\n\n") - } else { - b.WriteString("\n") - } - - boxMaxWidth := getResponsiveWidth(m.width, 10, 40, 80) - var boxPadding int - var boxMargin int - if isCompact { - boxPadding = 1 - boxMargin = 1 - } else { - boxPadding = 2 - boxMargin = 2 - } - - responsiveInfoBox := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#7D56F4")). - Padding(1, boxPadding). - MarginTop(boxMargin). - MarginBottom(boxMargin). - Width(boxMaxWidth) - - authenticatedUser := m.interaction.lifecycle.User() - - userInfoStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FAFAFA")). - Bold(true) - - sectionHeaderStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#888888")). - Bold(true) - - addressStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FAFAFA")) - - var infoContent string - if shouldUseCompactLayout(m.width, 70) { - infoContent = fmt.Sprintf("👤 %s\n\n%s\n%s", - userInfoStyle.Render(authenticatedUser), - sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"), - addressStyle.Render(fmt.Sprintf(" %s", urlBoxStyle.Render(m.getTunnelURL())))) - } else { - infoContent = fmt.Sprintf("👤 Authenticated as: %s\n\n%s\n %s", - userInfoStyle.Render(authenticatedUser), - sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"), - addressStyle.Render(urlBoxStyle.Render(m.getTunnelURL()))) - } - - b.WriteString(responsiveInfoBox.Render(infoContent)) - b.WriteString("\n") - - var quickActionsTitle string - if shouldUseCompactLayout(m.width, 50) { - quickActionsTitle = "Actions" - } else if isCompact { - quickActionsTitle = "Quick Actions" - } else { - quickActionsTitle = "✨ Quick Actions" - } - b.WriteString(titleStyle.Render(quickActionsTitle)) - b.WriteString("\n") - - var featureMargin int - if isCompact { - featureMargin = 1 - } else { - featureMargin = 2 - } - - compactFeatureStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FAFAFA")). - MarginLeft(featureMargin) - - var commandsText string - var quitText string - if shouldUseCompactLayout(m.width, 60) { - commandsText = fmt.Sprintf(" %s Commands", keyHintStyle.Render("[C]")) - quitText = fmt.Sprintf(" %s Quit", keyHintStyle.Render("[Q]")) - } else { - commandsText = fmt.Sprintf(" %s Open commands menu", keyHintStyle.Render("[C]")) - quitText = fmt.Sprintf(" %s Quit application", keyHintStyle.Render("[Q]")) - } - - b.WriteString(compactFeatureStyle.Render(commandsText)) - b.WriteString("\n") - b.WriteString(compactFeatureStyle.Render(quitText)) - - if !shouldUseCompactLayout(m.width, 70) { - b.WriteString("\n\n") - footerStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#666666")). - Italic(true) - b.WriteString(footerStyle.Render("Press 'C' to customize your tunnel settings")) - } - - return b.String() + return m.dashboardView() } func (i *interaction) Start() { @@ -848,11 +266,3 @@ func (i *interaction) Start() { log.Printf("Cannot close session: %s \n", err) } } - -func buildURL(protocol, subdomain, domain string) string { - return fmt.Sprintf("%s://%s.%s", protocol, subdomain, domain) -} - -func generateRandomSubdomain() string { - return random.GenerateRandomString(20) -} diff --git a/session/interaction/model.go b/session/interaction/model.go new file mode 100644 index 0000000..24b4d26 --- /dev/null +++ b/session/interaction/model.go @@ -0,0 +1,95 @@ +package interaction + +import ( + "fmt" + "time" + "tunnel_pls/types" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type commandItem struct { + name string + desc string +} + +func (i commandItem) FilterValue() string { return i.name } +func (i commandItem) Title() string { return i.name } +func (i commandItem) Description() string { return i.desc } + +type model struct { + domain string + protocol string + tunnelType types.TunnelType + port uint16 + keymap keymap + help help.Model + quitting bool + showingCommands bool + editingSlug bool + showingComingSoon bool + commandList list.Model + slugInput textinput.Model + slugError string + interaction *interaction + width int + height int +} + +func (m *model) getTunnelURL() string { + if m.tunnelType == types.HTTP { + return buildURL(m.protocol, m.interaction.slug.String(), m.domain) + } + return fmt.Sprintf("tcp://%s:%d", m.domain, m.port) +} + +type keymap struct { + quit key.Binding + command key.Binding + random key.Binding +} + +type tickMsg time.Time + +func (m *model) Init() tea.Cmd { + return tea.Batch(textinput.Blink, tea.WindowSize()) +} + +func getResponsiveWidth(screenWidth, padding, minWidth, maxWidth int) int { + width := screenWidth - padding + if width > maxWidth { + width = maxWidth + } + if width < minWidth { + width = minWidth + } + return width +} + +func shouldUseCompactLayout(width int, threshold int) bool { + return width < threshold +} + +func truncateString(s string, maxLength int) string { + if len(s) <= maxLength { + return s + } + if maxLength < 4 { + return s[:maxLength] + } + return s[:maxLength-3] + "..." +} + +func tickCmd(d time.Duration) tea.Cmd { + return tea.Tick(d, func(t time.Time) tea.Msg { + return tickMsg(t) + }) +} + +func buildURL(protocol, subdomain, domain string) string { + return fmt.Sprintf("%s://%s.%s", protocol, subdomain, domain) +} diff --git a/session/interaction/slug.go b/session/interaction/slug.go new file mode 100644 index 0000000..7a7bdaa --- /dev/null +++ b/session/interaction/slug.go @@ -0,0 +1,224 @@ +package interaction + +import ( + "fmt" + "strings" + "tunnel_pls/internal/random" + "tunnel_pls/types" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +func (m *model) slugUpdate(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + if m.tunnelType != types.HTTP { + m.editingSlug = false + m.slugError = "" + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + } + + switch msg.String() { + case "esc": + m.editingSlug = false + m.slugError = "" + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + case "enter": + inputValue := m.slugInput.Value() + if err := m.interaction.sessionRegistry.Update(m.interaction.lifecycle.User(), types.SessionKey{ + Id: m.interaction.slug.String(), + Type: types.HTTP, + }, types.SessionKey{ + Id: inputValue, + Type: types.HTTP, + }); err != nil { + m.slugError = err.Error() + return m, nil + } + m.editingSlug = false + m.slugError = "" + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + case "ctrl+c": + m.editingSlug = false + m.slugError = "" + return m, tea.Batch(tea.ClearScreen, textinput.Blink) + default: + if key.Matches(msg, m.keymap.random) { + newSubdomain, err := random.GenerateRandomString(20) + if err != nil { + return m, cmd + } + m.slugInput.SetValue(newSubdomain) + m.slugError = "" + m.slugInput, cmd = m.slugInput.Update(msg) + } + m.slugError = "" + m.slugInput, cmd = m.slugInput.Update(msg) + return m, cmd + } +} + +func (m *model) slugView() string { + isCompact := shouldUseCompactLayout(m.width, 70) + isVeryCompact := shouldUseCompactLayout(m.width, 50) + + var boxPadding int + var boxMargin int + if isVeryCompact { + boxPadding = 1 + boxMargin = 1 + } else if isCompact { + boxPadding = 1 + boxMargin = 1 + } else { + boxPadding = 2 + boxMargin = 2 + } + + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7D56F4")). + PaddingTop(1). + PaddingBottom(1) + + instructionStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")). + MarginTop(1) + + inputBoxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#7D56F4")). + Padding(1, boxPadding). + MarginTop(boxMargin). + MarginBottom(boxMargin) + + helpStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#666666")). + Italic(true). + MarginTop(1) + + errorBoxStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF0000")). + Background(lipgloss.Color("#3D0000")). + Bold(true). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FF0000")). + Padding(0, boxPadding). + MarginTop(1). + MarginBottom(1) + + rulesBoxWidth := getResponsiveWidth(m.width, 10, 30, 60) + rulesBoxStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#7D56F4")). + Padding(0, boxPadding). + MarginTop(1). + MarginBottom(1). + Width(rulesBoxWidth) + + var b strings.Builder + var title string + if isVeryCompact { + title = "Edit Subdomain" + } else { + title = "🔧 Edit Subdomain" + } + b.WriteString(titleStyle.Render(title)) + b.WriteString("\n\n") + + if m.tunnelType != types.HTTP { + warningBoxWidth := getResponsiveWidth(m.width, 10, 30, 60) + warningBoxStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFA500")). + Background(lipgloss.Color("#3D2000")). + Bold(true). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FFA500")). + Padding(1, boxPadding). + MarginTop(boxMargin). + MarginBottom(boxMargin). + Width(warningBoxWidth) + + var warningText string + if isVeryCompact { + warningText = "⚠️ TCP tunnels don't support custom subdomains." + } else { + warningText = "⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization." + } + b.WriteString(warningBoxStyle.Render(warningText)) + b.WriteString("\n\n") + + var helpText string + if isVeryCompact { + helpText = "Press any key to go back" + } else { + helpText = "Press Enter or Esc to go back" + } + b.WriteString(helpStyle.Render(helpText)) + return b.String() + } + + var rulesContent string + if isVeryCompact { + rulesContent = "Rules:\n3-20 chars\na-z, 0-9, -\nNo leading/trailing -" + } else if isCompact { + rulesContent = "📋 Rules:\n • 3-20 chars\n • a-z, 0-9, -\n • No leading/trailing -" + } else { + rulesContent = "📋 Rules: \n\t• 3-20 chars \n\t• a-z, 0-9, - \n\t• No leading/trailing -" + } + b.WriteString(rulesBoxStyle.Render(rulesContent)) + b.WriteString("\n") + + var instruction string + if isVeryCompact { + instruction = "Custom subdomain:" + } else { + instruction = "Enter your custom subdomain:" + } + b.WriteString(instructionStyle.Render(instruction)) + b.WriteString("\n") + + if m.slugError != "" { + errorInputBoxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#FF0000")). + Padding(1, boxPadding). + MarginTop(boxMargin). + MarginBottom(1) + b.WriteString(errorInputBoxStyle.Render(m.slugInput.View())) + b.WriteString("\n") + b.WriteString(errorBoxStyle.Render("❌ " + m.slugError)) + b.WriteString("\n") + } else { + b.WriteString(inputBoxStyle.Render(m.slugInput.View())) + b.WriteString("\n") + } + + previewURL := buildURL(m.protocol, m.slugInput.Value(), m.domain) + previewWidth := getResponsiveWidth(m.width, 10, 30, 80) + + if len(previewURL) > previewWidth-10 { + previewURL = truncateString(previewURL, previewWidth-10) + } + + previewStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#04B575")). + Italic(true). + Width(previewWidth) + b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL))) + b.WriteString("\n") + + var helpText string + if isVeryCompact { + helpText = "Enter: save • CTRL+R: random • Esc: cancel" + } else { + helpText = "Press Enter to save • CTRL+R for random • Esc to cancel" + } + b.WriteString(helpStyle.Render(helpText)) + + return b.String() +} diff --git a/session/session.go b/session/session.go index 82e6916..f0fd5be 100644 --- a/session/session.go +++ b/session/session.go @@ -291,7 +291,11 @@ func (s *session) HandleHTTPForward(req *ssh.Request, portToBind uint16) { } } - randomString := random.GenerateRandomString(20) + randomString, err := random.GenerateRandomString(20) + if err != nil { + fail(fmt.Sprintf("Failed to create slug: %s", err), nil) + return + } key := types.SessionKey{Id: randomString, Type: types.HTTP} if !s.registry.Register(key, s) { fail(fmt.Sprintf("Failed to register client with slug: %s", randomString), nil) @@ -299,7 +303,7 @@ func (s *session) HandleHTTPForward(req *ssh.Request, portToBind uint16) { } buf := new(bytes.Buffer) - err := binary.Write(buf, binary.BigEndian, uint32(portToBind)) + err = binary.Write(buf, binary.BigEndian, uint32(portToBind)) if err != nil { fail(fmt.Sprintf("Failed to write port to buffer: %v", err), &key) return