16 Commits

Author SHA1 Message Date
e3988b339f Merge pull request 'fix(deps): update module github.com/caddyserver/certmagic to v0.25.1' (#61) from renovate/github.com-caddyserver-certmagic-0.x into main
All checks were successful
Docker Build and Push / build-and-push-tags (push) Has been skipped
Docker Build and Push / build-and-push-branches (push) Successful in 3m21s
Reviewed-on: #61
2026-01-09 12:15:05 +00:00
336948a397 fix(deps): update module github.com/caddyserver/certmagic to v0.25.1 2026-01-09 10:00:35 +00:00
50ae422de8 Merge pull request 'staging' (#60) from staging into main
All checks were successful
Docker Build and Push / build-and-push-tags (push) Has been skipped
Docker Build and Push / build-and-push-branches (push) Successful in 3m20s
Reviewed-on: #60
2026-01-09 09:33:28 +00:00
8467ed555e revert 01ddc76f7e
Some checks failed
Docker Build and Push / build-and-push-tags (push) Has been skipped
Docker Build and Push / build-and-push-branches (push) Has been cancelled
revert Merge pull request 'fix(deps): update module github.com/caddyserver/certmagic to v0.25.1' (#58) from renovate/github.com-caddyserver-certmagic-0.x into main
2026-01-09 09:33:04 +00:00
01ddc76f7e Merge pull request 'fix(deps): update module github.com/caddyserver/certmagic to v0.25.1' (#58) from renovate/github.com-caddyserver-certmagic-0.x into main
Some checks are pending
Docker Build and Push / build-and-push-branches (push) Waiting to run
Docker Build and Push / build-and-push-tags (push) Has been skipped
2026-01-09 09:30:23 +00:00
ffb3565ff5 fix(deps): update module github.com/caddyserver/certmagic to v0.25.1 2026-01-09 09:30:18 +00:00
6d700ef6dd Merge pull request 'feat/grpc-integration' (#59) from feat/grpc-integration into staging
All checks were successful
Docker Build and Push / build-and-push-branches (push) Successful in 5m25s
Docker Build and Push / build-and-push-tags (push) Has been skipped
Reviewed-on: #59
2026-01-09 09:24:20 +00:00
b8acb6da4c ci: remove renovate 2026-01-08 13:03:02 +07:00
6b4127f0ef feat: add authenticated user info and restructure handleConnection
All checks were successful
Docker Build and Push / build-and-push-branches (push) Has been skipped
Docker Build and Push / build-and-push-tags (push) Successful in 3m21s
- Display authenticated username in welcome page information box
- Refactor handleConnection function for better structure and clarity
2026-01-07 23:07:02 +07:00
16d48ff906 refactor(grpc/client): simplify processEventStream with per-event handlers
All checks were successful
Docker Build and Push / build-and-push-branches (push) Has been skipped
Docker Build and Push / build-and-push-tags (push) Successful in 3m20s
- Extract eventHandlers dispatch table
- Add per-event handlers: handleSlugChange, handleGetSessions, handleTerminateSession
- Introduce sendNode helper to centralize send/error handling and preserve connection-error propagation
- Add protoToTunnelType for tunnel-type validation
- Map unknown proto.TunnelType to types.UNKNOWN in protoToTunnelType and return a descriptive error
- Reduce boilerplate and improve readability of processEventStream
2026-01-06 20:14:56 +07:00
6213ff8a30 feat: implement forwarder session termination
All checks were successful
Docker Build and Push / build-and-push-branches (push) Has been skipped
Docker Build and Push / build-and-push-tags (push) Successful in 3m36s
2026-01-06 18:32:48 +07:00
4ffaec9d9a refactor: inject SessionRegistry interface instead of individual functions
All checks were successful
Docker Build and Push / build-and-push-branches (push) Has been skipped
Docker Build and Push / build-and-push-tags (push) Successful in 4m16s
2026-01-05 16:49:17 +07:00
5ceade81db Merge pull request 'staging' (#57) from staging into main
Some checks failed
Docker Build and Push / build-and-push-tags (push) Has been skipped
Docker Build and Push / build-and-push-branches (push) Successful in 3m57s
renovate / renovate (push) Failing after 34s
Reviewed-on: #57
2026-01-03 13:07:49 +00:00
8a456d2cde Merge pull request 'staging' (#55) from staging into main
All checks were successful
Docker Build and Push / build-and-push-tags (push) Has been skipped
Docker Build and Push / build-and-push-branches (push) Successful in 5m50s
renovate / renovate (push) Successful in 35s
Reviewed-on: #55
2025-12-31 08:51:25 +00:00
8841230653 Merge pull request 'fix: prevent subdomain change to already-in-use subdomains' (#54) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 5m20s
renovate / renovate (push) Successful in 38s
Reviewed-on: #54
2025-12-30 12:42:05 +00:00
4d0a7deaf2 Merge pull request 'staging' (#53) from staging into main
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m33s
renovate / renovate (push) Successful in 22s
Reviewed-on: #53
2025-12-29 17:18:25 +00:00
11 changed files with 280 additions and 229 deletions

View File

@@ -1,21 +0,0 @@
name: renovate
on:
schedule:
- cron: "0 0 * * *"
push:
branches:
- staging
jobs:
renovate:
runs-on: ubuntu-latest
container: git.fossy.my.id/renovate-clanker/renovate:latest
steps:
- uses: actions/checkout@v6
- run: renovate
env:
RENOVATE_CONFIG_FILE: ${{ gitea.workspace }}/renovate-config.js
LOG_LEVEL: "debug"
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
GITHUB_COM_TOKEN: ${{ secrets.COM_TOKEN }}

6
go.mod
View File

@@ -3,8 +3,8 @@ module tunnel_pls
go 1.25.5 go 1.25.5
require ( require (
git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0 git.fossy.my.id/bagas/tunnel-please-grpc v1.5.0
github.com/caddyserver/certmagic v0.25.0 github.com/caddyserver/certmagic v0.25.1
github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
@@ -19,7 +19,7 @@ require (
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect github.com/caddyserver/zerossl v0.1.4 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect

8
go.sum
View File

@@ -1,5 +1,9 @@
git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0 h1:RhcBKUG41/om4jgN+iF/vlY/RojTeX1QhBa4p4428ec= git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0 h1:RhcBKUG41/om4jgN+iF/vlY/RojTeX1QhBa4p4428ec=
git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY= git.fossy.my.id/bagas/tunnel-please-grpc v1.3.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY=
git.fossy.my.id/bagas/tunnel-please-grpc v1.4.0 h1:tpJSKjaSmV+vxxbVx6qnStjxFVXjj2M0rygWXxLb99o=
git.fossy.my.id/bagas/tunnel-please-grpc v1.4.0/go.mod h1:fG+VkArdkceGB0bNA7IFQus9GetLAwdF5Oi4jdMlXtY=
git.fossy.my.id/bagas/tunnel-please-grpc v1.5.0 h1:3xszIhck4wo9CoeRq9vnkar4PhY7kz9QrR30qj2XszA=
git.fossy.my.id/bagas/tunnel-please-grpc v1.5.0/go.mod h1:Weh6ZujgWmT8XxD3Qba7sJ6r5eyUMB9XSWynqdyOoLo=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@@ -8,8 +12,12 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY=
github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/caddyserver/zerossl v0.1.4 h1:CVJOE3MZeFisCERZjkxIcsqIH4fnFdlYWnPYeFtBHRw=
github.com/caddyserver/zerossl v0.1.4/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=

View File

@@ -210,114 +210,152 @@ func (c *Client) SubscribeEvents(ctx context.Context, identity, authToken string
} }
func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events]) error { func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events]) error {
handlers := c.eventHandlers(subscribe)
for { for {
recv, err := subscribe.Recv() recv, err := subscribe.Recv()
if err != nil { if err != nil {
return err return err
} }
switch recv.GetType() {
case proto.EventType_SLUG_CHANGE: handler, ok := handlers[recv.GetType()]
oldSlug := recv.GetSlugEvent().GetOld() if !ok {
newSlug := recv.GetSlugEvent().GetNew()
var userSession *session.SSHSession
userSession, err = c.sessionRegistry.Get(types.SessionKey{
Id: oldSlug,
Type: types.HTTP,
})
if err != nil {
errSend := subscribe.Send(&proto.Node{
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
Payload: &proto.Node_SlugEventResponse{
SlugEventResponse: &proto.SlugChangeEventResponse{
Success: false,
Message: err.Error(),
},
},
})
if errSend != nil {
if c.isConnectionError(errSend) {
return errSend
}
log.Printf("non-connection send error for slug change failure: %v", errSend)
}
continue
}
err = c.sessionRegistry.Update(types.SessionKey{
Id: oldSlug,
Type: types.HTTP,
}, types.SessionKey{
Id: newSlug,
Type: types.HTTP,
})
if err != nil {
errSend := subscribe.Send(&proto.Node{
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
Payload: &proto.Node_SlugEventResponse{
SlugEventResponse: &proto.SlugChangeEventResponse{
Success: false,
Message: err.Error(),
},
},
})
if errSend != nil {
if c.isConnectionError(errSend) {
return errSend
}
log.Printf("non-connection send error for slug change failure: %v", errSend)
}
continue
}
userSession.GetInteraction().Redraw()
err = subscribe.Send(&proto.Node{
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
Payload: &proto.Node_SlugEventResponse{
SlugEventResponse: &proto.SlugChangeEventResponse{
Success: true,
Message: "",
},
},
})
if err != nil {
if c.isConnectionError(err) {
log.Printf("connection error sending slug change success: %v", err)
return err
}
log.Printf("non-connection send error for slug change success: %v", err)
continue
}
case proto.EventType_GET_SESSIONS:
sessions := c.sessionRegistry.GetAllSessionFromUser(recv.GetGetSessionsEvent().GetIdentity())
var details []*proto.Detail
for _, ses := range sessions {
detail := ses.Detail()
details = append(details, &proto.Detail{
Node: config.Getenv("DOMAIN", "localhost"),
ForwardingType: detail.ForwardingType,
Slug: detail.Slug,
UserId: detail.UserID,
Active: detail.Active,
StartedAt: timestamppb.New(detail.StartedAt),
})
}
err = subscribe.Send(&proto.Node{
Type: proto.EventType_GET_SESSIONS,
Payload: &proto.Node_GetSessionsEvent{
GetSessionsEvent: &proto.GetSessionsResponse{
Details: details,
},
},
})
if err != nil {
if c.isConnectionError(err) {
log.Printf("connection error sending sessions success: %v", err)
return err
}
log.Printf("non-connection send error for sessions success: %v", err)
continue
}
default:
log.Printf("Unknown event type received: %v", recv.GetType()) log.Printf("Unknown event type received: %v", recv.GetType())
continue
} }
if err = handler(recv); err != nil {
return err
}
}
}
func (c *Client) eventHandlers(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events]) map[proto.EventType]func(*proto.Events) error {
return map[proto.EventType]func(*proto.Events) error{
proto.EventType_SLUG_CHANGE: func(evt *proto.Events) error { return c.handleSlugChange(subscribe, evt) },
proto.EventType_GET_SESSIONS: func(evt *proto.Events) error { return c.handleGetSessions(subscribe, evt) },
proto.EventType_TERMINATE_SESSION: func(evt *proto.Events) error { return c.handleTerminateSession(subscribe, evt) },
}
}
func (c *Client) handleSlugChange(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events], evt *proto.Events) error {
slugEvent := evt.GetSlugEvent()
user := slugEvent.GetUser()
oldSlug := slugEvent.GetOld()
newSlug := slugEvent.GetNew()
userSession, err := c.sessionRegistry.Get(types.SessionKey{Id: oldSlug, Type: types.HTTP})
if err != nil {
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
Payload: &proto.Node_SlugEventResponse{
SlugEventResponse: &proto.SlugChangeEventResponse{Success: false, Message: err.Error()},
},
}, "slug change failure response")
}
if err = c.sessionRegistry.Update(user, types.SessionKey{Id: oldSlug, Type: types.HTTP}, types.SessionKey{Id: newSlug, Type: types.HTTP}); err != nil {
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
Payload: &proto.Node_SlugEventResponse{
SlugEventResponse: &proto.SlugChangeEventResponse{Success: false, Message: err.Error()},
},
}, "slug change failure response")
}
userSession.GetInteraction().Redraw()
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
Payload: &proto.Node_SlugEventResponse{
SlugEventResponse: &proto.SlugChangeEventResponse{Success: true, Message: ""},
},
}, "slug change success response")
}
func (c *Client) handleGetSessions(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events], evt *proto.Events) error {
sessions := c.sessionRegistry.GetAllSessionFromUser(evt.GetGetSessionsEvent().GetIdentity())
var details []*proto.Detail
for _, ses := range sessions {
detail := ses.Detail()
details = append(details, &proto.Detail{
Node: config.Getenv("DOMAIN", "localhost"),
ForwardingType: detail.ForwardingType,
Slug: detail.Slug,
UserId: detail.UserID,
Active: detail.Active,
StartedAt: timestamppb.New(detail.StartedAt),
})
}
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_GET_SESSIONS,
Payload: &proto.Node_GetSessionsEvent{
GetSessionsEvent: &proto.GetSessionsResponse{Details: details},
},
}, "send get sessions response")
}
func (c *Client) handleTerminateSession(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events], evt *proto.Events) error {
terminate := evt.GetTerminateSessionEvent()
user := terminate.GetUser()
slug := terminate.GetSlug()
tunnelType, err := c.protoToTunnelType(terminate.GetTunnelType())
if err != nil {
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_TERMINATE_SESSION,
Payload: &proto.Node_TerminateSessionEventResponse{
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{Success: false, Message: err.Error()},
},
}, "terminate session invalid tunnel type")
}
userSession, err := c.sessionRegistry.GetWithUser(user, types.SessionKey{Id: slug, Type: tunnelType})
if err != nil {
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_TERMINATE_SESSION,
Payload: &proto.Node_TerminateSessionEventResponse{
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{Success: false, Message: err.Error()},
},
}, "terminate session fetch failed")
}
if err = userSession.GetLifecycle().Close(); err != nil {
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_TERMINATE_SESSION,
Payload: &proto.Node_TerminateSessionEventResponse{
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{Success: false, Message: err.Error()},
},
}, "terminate session close failed")
}
return c.sendNode(subscribe, &proto.Node{
Type: proto.EventType_TERMINATE_SESSION,
Payload: &proto.Node_TerminateSessionEventResponse{
TerminateSessionEventResponse: &proto.TerminateSessionEventResponse{Success: true, Message: ""},
},
}, "terminate session success response")
}
func (c *Client) sendNode(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events], node *proto.Node, context string) error {
if err := subscribe.Send(node); err != nil {
if c.isConnectionError(err) {
return err
}
log.Printf("%s: %v", context, err)
}
return nil
}
func (c *Client) protoToTunnelType(t proto.TunnelType) (types.TunnelType, error) {
switch t {
case proto.TunnelType_HTTP:
return types.HTTP, nil
case proto.TunnelType_TCP:
return types.TCP, nil
default:
return types.UNKNOWN, fmt.Errorf("unknown tunnel type received")
} }
} }

View File

@@ -1,8 +0,0 @@
module.exports = {
"endpoint": "https://git.fossy.my.id/api/v1",
"gitAuthor": "Renovate-Clanker <renovate-bot@fossy.my.id>",
"platform": "gitea",
"onboardingConfigFileName": "renovate.json",
"autodiscover": true,
"optimizeForDisabled": true,
};

View File

@@ -2,9 +2,11 @@ package server
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log" "log"
"net" "net"
"time"
"tunnel_pls/internal/config" "tunnel_pls/internal/config"
"tunnel_pls/internal/grpc/client" "tunnel_pls/internal/grpc/client"
"tunnel_pls/session" "tunnel_pls/session"
@@ -64,30 +66,32 @@ func (s *Server) Start() {
func (s *Server) handleConnection(conn net.Conn) { func (s *Server) handleConnection(conn net.Conn) {
sshConn, chans, forwardingReqs, err := ssh.NewServerConn(conn, s.config) sshConn, chans, forwardingReqs, err := ssh.NewServerConn(conn, s.config)
defer func(sshConn *ssh.ServerConn) {
err = sshConn.Close()
if err != nil {
log.Printf("failed to close SSH server: %v", err)
}
}(sshConn)
if err != nil { if err != nil {
log.Printf("failed to establish SSH connection: %v", err) log.Printf("failed to establish SSH connection: %v", err)
err := conn.Close() err = conn.Close()
if err != nil { if err != nil {
log.Printf("failed to close SSH connection: %v", err) log.Printf("failed to close SSH connection: %v", err)
return return
} }
return return
} }
ctx := context.Background()
log.Println("SSH connection established:", sshConn.User()) defer func(sshConn *ssh.ServerConn) {
err = sshConn.Close()
if err != nil && !errors.Is(err, net.ErrClosed) {
log.Printf("failed to close SSH server: %v", err)
}
}(sshConn)
user := "UNAUTHORIZED" user := "UNAUTHORIZED"
if s.grpcClient != nil { if s.grpcClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
_, u, _ := s.grpcClient.AuthorizeConn(ctx, sshConn.User()) _, u, _ := s.grpcClient.AuthorizeConn(ctx, sshConn.User())
user = u user = u
cancel()
} }
log.Println("SSH connection established:", sshConn.User())
sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry, user) sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry, user)
err = sshSession.Start() err = sshSession.Start()
if err != nil { if err != nil {

View File

@@ -23,15 +23,20 @@ import (
type Lifecycle interface { type Lifecycle interface {
Close() error Close() error
GetUser() string
}
type SessionRegistry interface {
Update(user string, oldKey, newKey types.SessionKey) error
} }
type Controller interface { type Controller interface {
SetChannel(channel ssh.Channel) SetChannel(channel ssh.Channel)
SetLifecycle(lifecycle Lifecycle) SetLifecycle(lifecycle Lifecycle)
SetSlugModificator(func(oldSlug, newSlug string) error)
Start() Start()
SetWH(w, h int) SetWH(w, h int)
Redraw() Redraw()
SetSessionRegistry(registry SessionRegistry)
} }
type Forwarder interface { type Forwarder interface {
@@ -41,14 +46,14 @@ type Forwarder interface {
} }
type Interaction struct { type Interaction struct {
channel ssh.Channel channel ssh.Channel
slugManager slug.Manager slugManager slug.Manager
forwarder Forwarder forwarder Forwarder
lifecycle Lifecycle lifecycle Lifecycle
updateClientSlug func(oldSlug, newSlug string) error sessionRegistry SessionRegistry
program *tea.Program program *tea.Program
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
func (i *Interaction) SetWH(w, h int) { func (i *Interaction) SetWH(w, h int) {
@@ -102,17 +107,21 @@ type tickMsg time.Time
func NewInteraction(slugManager slug.Manager, forwarder Forwarder) *Interaction { func NewInteraction(slugManager slug.Manager, forwarder Forwarder) *Interaction {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Interaction{ return &Interaction{
channel: nil, channel: nil,
slugManager: slugManager, slugManager: slugManager,
forwarder: forwarder, forwarder: forwarder,
lifecycle: nil, lifecycle: nil,
updateClientSlug: nil, sessionRegistry: nil,
program: nil, program: nil,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
} }
} }
func (i *Interaction) SetSessionRegistry(registry SessionRegistry) {
i.sessionRegistry = registry
}
func (i *Interaction) SetLifecycle(lifecycle Lifecycle) { func (i *Interaction) SetLifecycle(lifecycle Lifecycle) {
i.lifecycle = lifecycle i.lifecycle = lifecycle
} }
@@ -121,10 +130,6 @@ func (i *Interaction) SetChannel(channel ssh.Channel) {
i.channel = channel i.channel = channel
} }
func (i *Interaction) SetSlugModificator(modificator func(oldSlug, newSlug string) error) {
i.updateClientSlug = modificator
}
func (i *Interaction) Stop() { func (i *Interaction) Stop() {
if i.cancel != nil { if i.cancel != nil {
i.cancel() i.cancel()
@@ -218,7 +223,13 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(tea.ClearScreen, textinput.Blink) return m, tea.Batch(tea.ClearScreen, textinput.Blink)
case "enter": case "enter":
inputValue := m.slugInput.Value() inputValue := m.slugInput.Value()
if err := m.interaction.updateClientSlug(m.interaction.slugManager.Get(), inputValue); err != nil { if err := m.interaction.sessionRegistry.Update(m.interaction.lifecycle.GetUser(), types.SessionKey{
Id: m.interaction.slugManager.Get(),
Type: types.HTTP,
}, types.SessionKey{
Id: inputValue,
Type: types.HTTP,
}); err != nil {
m.slugError = err.Error() m.slugError = err.Error()
return m, nil return m, nil
} }
@@ -661,22 +672,32 @@ func (m *model) View() string {
MarginBottom(boxMargin). MarginBottom(boxMargin).
Width(boxMaxWidth) Width(boxMaxWidth)
urlDisplay := m.getTunnelURL() authenticatedUser := m.interaction.lifecycle.GetUser()
if shouldUseCompactLayout(m.width, 80) && len(urlDisplay) > m.width-20 {
maxLen := m.width - 25 userInfoStyle := lipgloss.NewStyle().
if maxLen > 10 { Foreground(lipgloss.Color("#FAFAFA")).
urlDisplay = truncateString(urlDisplay, maxLen) Bold(true)
}
} sectionHeaderStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888")).
Bold(true)
addressStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FAFAFA"))
var infoContent string var infoContent string
if shouldUseCompactLayout(m.width, 70) { if shouldUseCompactLayout(m.width, 70) {
infoContent = fmt.Sprintf("🌐 %s", urlBoxStyle.Render(urlDisplay)) infoContent = fmt.Sprintf("👤 %s\n\n%s\n%s",
} else if isCompact { userInfoStyle.Render(authenticatedUser),
infoContent = fmt.Sprintf("🌐 Forwarding to:\n\n %s", urlBoxStyle.Render(urlDisplay)) sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"),
addressStyle.Render(fmt.Sprintf(" %s", urlBoxStyle.Render(m.getTunnelURL()))))
} else { } else {
infoContent = fmt.Sprintf("🌐 F O R W A R D I N G T O:\n\n %s", urlBoxStyle.Render(urlDisplay)) 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(responsiveInfoBox.Render(infoContent))
b.WriteString("\n") b.WriteString("\n")

View File

@@ -19,30 +19,36 @@ type Forwarder interface {
GetForwardedPort() uint16 GetForwardedPort() uint16
} }
type Lifecycle struct { type SessionRegistry interface {
status types.Status Remove(key types.SessionKey)
conn ssh.Conn
channel ssh.Channel
forwarder Forwarder
slugManager slug.Manager
unregisterClient func(key types.SessionKey)
startedAt time.Time
} }
func NewLifecycle(conn ssh.Conn, forwarder Forwarder, slugManager slug.Manager) *Lifecycle { type Lifecycle struct {
status types.Status
conn ssh.Conn
channel ssh.Channel
forwarder Forwarder
sessionRegistry SessionRegistry
slugManager slug.Manager
startedAt time.Time
user string
}
func NewLifecycle(conn ssh.Conn, forwarder Forwarder, slugManager slug.Manager, user string) *Lifecycle {
return &Lifecycle{ return &Lifecycle{
status: types.INITIALIZING, status: types.INITIALIZING,
conn: conn, conn: conn,
channel: nil, channel: nil,
forwarder: forwarder, forwarder: forwarder,
slugManager: slugManager, slugManager: slugManager,
unregisterClient: nil, sessionRegistry: nil,
startedAt: time.Now(), startedAt: time.Now(),
user: user,
} }
} }
func (l *Lifecycle) SetUnregisterClient(unregisterClient func(key types.SessionKey)) { func (l *Lifecycle) SetSessionRegistry(registry SessionRegistry) {
l.unregisterClient = unregisterClient l.sessionRegistry = registry
} }
type SessionLifecycle interface { type SessionLifecycle interface {
@@ -50,12 +56,17 @@ type SessionLifecycle interface {
SetStatus(status types.Status) SetStatus(status types.Status)
GetConnection() ssh.Conn GetConnection() ssh.Conn
GetChannel() ssh.Channel GetChannel() ssh.Channel
GetUser() string
SetChannel(channel ssh.Channel) SetChannel(channel ssh.Channel)
SetUnregisterClient(unregisterClient func(key types.SessionKey)) SetSessionRegistry(registry SessionRegistry)
IsActive() bool IsActive() bool
StartedAt() time.Time StartedAt() time.Time
} }
func (l *Lifecycle) GetUser() string {
return l.user
}
func (l *Lifecycle) GetChannel() ssh.Channel { func (l *Lifecycle) GetChannel() ssh.Channel {
return l.channel return l.channel
} }
@@ -94,13 +105,13 @@ func (l *Lifecycle) Close() error {
} }
clientSlug := l.slugManager.Get() clientSlug := l.slugManager.Get()
if clientSlug != "" && l.unregisterClient != nil { if clientSlug != "" && l.sessionRegistry.Remove != nil {
key := types.SessionKey{Id: clientSlug, Type: l.forwarder.GetTunnelType()} key := types.SessionKey{Id: clientSlug, Type: l.forwarder.GetTunnelType()}
l.unregisterClient(key) l.sessionRegistry.Remove(key)
} }
if l.forwarder.GetTunnelType() == types.TCP { if l.forwarder.GetTunnelType() == types.TCP {
err := portUtil.Default.SetPortStatus(l.forwarder.GetForwardedPort(), false) err = portUtil.Default.SetPortStatus(l.forwarder.GetForwardedPort(), false)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -10,7 +10,8 @@ type Key = types.SessionKey
type Registry interface { type Registry interface {
Get(key Key) (session *SSHSession, err error) Get(key Key) (session *SSHSession, err error)
Update(oldKey, newKey Key) error GetWithUser(user string, key Key) (session *SSHSession, err error)
Update(user string, oldKey, newKey Key) error
Register(key Key, session *SSHSession) (success bool) Register(key Key, session *SSHSession) (success bool)
Remove(key Key) Remove(key Key)
GetAllSessionFromUser(user string) []*SSHSession GetAllSessionFromUser(user string) []*SSHSession
@@ -44,7 +45,18 @@ func (r *registry) Get(key Key) (session *SSHSession, err error) {
return client, nil return client, nil
} }
func (r *registry) Update(oldKey, newKey Key) error { func (r *registry) GetWithUser(user string, key Key) (session *SSHSession, err error) {
r.mu.RLock()
defer r.mu.RUnlock()
client, ok := r.byUser[user][key]
if !ok {
return nil, fmt.Errorf("session not found")
}
return client, nil
}
func (r *registry) Update(user string, oldKey, newKey Key) error {
if oldKey.Type != newKey.Type { if oldKey.Type != newKey.Type {
return fmt.Errorf("tunnel type cannot change") return fmt.Errorf("tunnel type cannot change")
} }
@@ -64,30 +76,24 @@ func (r *registry) Update(oldKey, newKey Key) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
userID, ok := r.slugIndex[oldKey]
if !ok {
return fmt.Errorf("session not found")
}
if _, exists := r.slugIndex[newKey]; exists && newKey != oldKey { if _, exists := r.slugIndex[newKey]; exists && newKey != oldKey {
return fmt.Errorf("someone already uses this subdomain") return fmt.Errorf("someone already uses this subdomain")
} }
client, ok := r.byUser[user][oldKey]
client, ok := r.byUser[userID][oldKey]
if !ok { if !ok {
return fmt.Errorf("session not found") return fmt.Errorf("session not found")
} }
delete(r.byUser[userID], oldKey) delete(r.byUser[user], oldKey)
delete(r.slugIndex, oldKey) delete(r.slugIndex, oldKey)
client.slugManager.Set(newKey.Id) client.slugManager.Set(newKey.Id)
r.slugIndex[newKey] = userID r.slugIndex[newKey] = user
if r.byUser[userID] == nil { if r.byUser[user] == nil {
r.byUser[userID] = make(map[Key]*SSHSession) r.byUser[user] = make(map[Key]*SSHSession)
} }
r.byUser[userID][newKey] = client r.byUser[user][newKey] = client
return nil return nil
} }
@@ -99,7 +105,7 @@ func (r *registry) Register(key Key, session *SSHSession) (success bool) {
return false return false
} }
userID := session.userID userID := session.lifecycle.GetUser()
if r.byUser[userID] == nil { if r.byUser[userID] == nil {
r.byUser[userID] = make(map[Key]*SSHSession) r.byUser[userID] = make(map[Key]*SSHSession)
} }

View File

@@ -9,7 +9,6 @@ import (
"tunnel_pls/session/interaction" "tunnel_pls/session/interaction"
"tunnel_pls/session/lifecycle" "tunnel_pls/session/lifecycle"
"tunnel_pls/session/slug" "tunnel_pls/session/slug"
"tunnel_pls/types"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@@ -29,7 +28,6 @@ type SSHSession struct {
forwarder forwarder.ForwardingController forwarder forwarder.ForwardingController
slugManager slug.Manager slugManager slug.Manager
registry Registry registry Registry
userID string
} }
func (s *SSHSession) GetLifecycle() lifecycle.SessionLifecycle { func (s *SSHSession) GetLifecycle() lifecycle.SessionLifecycle {
@@ -48,22 +46,16 @@ func (s *SSHSession) GetSlugManager() slug.Manager {
return s.slugManager return s.slugManager
} }
func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan ssh.NewChannel, sessionRegistry Registry, userID string) *SSHSession { func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan ssh.NewChannel, sessionRegistry Registry, user string) *SSHSession {
slugManager := slug.NewManager() slugManager := slug.NewManager()
forwarderManager := forwarder.NewForwarder(slugManager) forwarderManager := forwarder.NewForwarder(slugManager)
interactionManager := interaction.NewInteraction(slugManager, forwarderManager) interactionManager := interaction.NewInteraction(slugManager, forwarderManager)
lifecycleManager := lifecycle.NewLifecycle(conn, forwarderManager, slugManager) lifecycleManager := lifecycle.NewLifecycle(conn, forwarderManager, slugManager, user)
interactionManager.SetLifecycle(lifecycleManager) interactionManager.SetLifecycle(lifecycleManager)
interactionManager.SetSlugModificator(func(oldSlug, newSlug string) error {
oldKey := types.SessionKey{Id: oldSlug, Type: forwarderManager.GetTunnelType()}
newKey := types.SessionKey{Id: newSlug, Type: forwarderManager.GetTunnelType()}
return sessionRegistry.Update(oldKey, newKey)
})
forwarderManager.SetLifecycle(lifecycleManager) forwarderManager.SetLifecycle(lifecycleManager)
lifecycleManager.SetUnregisterClient(func(key types.SessionKey) { interactionManager.SetSessionRegistry(sessionRegistry)
sessionRegistry.Remove(key) lifecycleManager.SetSessionRegistry(sessionRegistry)
})
return &SSHSession{ return &SSHSession{
initialReq: forwardingReq, initialReq: forwardingReq,
@@ -73,7 +65,6 @@ func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan
forwarder: forwarderManager, forwarder: forwarderManager,
slugManager: slugManager, slugManager: slugManager,
registry: sessionRegistry, registry: sessionRegistry,
userID: userID,
} }
} }
@@ -89,7 +80,7 @@ func (s *SSHSession) Detail() Detail {
return Detail{ return Detail{
ForwardingType: string(s.forwarder.GetTunnelType()), ForwardingType: string(s.forwarder.GetTunnelType()),
Slug: s.slugManager.Get(), Slug: s.slugManager.Get(),
UserID: s.userID, UserID: s.lifecycle.GetUser(),
Active: s.lifecycle.IsActive(), Active: s.lifecycle.IsActive(),
StartedAt: s.lifecycle.StartedAt(), StartedAt: s.lifecycle.StartedAt(),
} }

View File

@@ -11,8 +11,9 @@ const (
type TunnelType string type TunnelType string
const ( const (
HTTP TunnelType = "HTTP" UNKNOWN TunnelType = "UNKNOWN"
TCP TunnelType = "TCP" HTTP TunnelType = "HTTP"
TCP TunnelType = "TCP"
) )
type SessionKey struct { type SessionKey struct {