diff --git a/go.mod b/go.mod index 11dbda1..0806c93 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,19 @@ module tunnel_pls -go 1.24.4 +go 1.25.5 require ( + git.fossy.my.id/bagas/tunnel-please-grpc v1.0.0 github.com/caddyserver/certmagic v0.25.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/golang/protobuf v1.5.4 github.com/joho/godotenv v1.5.1 github.com/libdns/cloudflare v0.2.2 + github.com/muesli/termenv v0.16.0 golang.org/x/crypto v0.46.0 + google.golang.org/grpc v1.78.0 ) require ( @@ -16,7 +21,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect @@ -31,7 +35,6 @@ require ( github.com/miekg/dns v1.1.68 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect @@ -45,4 +48,8 @@ require ( golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.39.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/protobuf v1.36.11 // indirect ) + +replace git.fossy.my.id/bagas/tunnel-please-grpc => ../tunnel-please-grpc diff --git a/go.sum b/go.sum index 98717ab..84a22e0 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -75,6 +83,18 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -103,5 +123,13 @@ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/grpc/client/client.go b/internal/grpc/client/client.go index 710f023..da76a55 100644 --- a/internal/grpc/client/client.go +++ b/internal/grpc/client/client.go @@ -1,4 +1,4 @@ -package grpc +package client import ( "context" @@ -6,15 +6,20 @@ import ( "fmt" "log" "time" + "tunnel_pls/session" "git.fossy.my.id/bagas/tunnel-please-grpc/gen" + "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/status" ) -type ClientConfig struct { +type GrpcConfig struct { Address string UseTLS bool InsecureSkipVerify bool @@ -25,12 +30,14 @@ type ClientConfig struct { type Client struct { conn *grpc.ClientConn - config *ClientConfig + config *GrpcConfig + sessionRegistry session.Registry IdentityService gen.IdentityClient + eventService gen.EventServiceClient } -func DefaultConfig() *ClientConfig { - return &ClientConfig{ +func DefaultConfig() *GrpcConfig { + return &GrpcConfig{ Address: "localhost:50051", UseTLS: false, InsecureSkipVerify: false, @@ -40,7 +47,7 @@ func DefaultConfig() *ClientConfig { } } -func NewClient(config *ClientConfig) (*Client, error) { +func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error) { if config == nil { config = DefaultConfig() } @@ -61,15 +68,15 @@ func NewClient(config *ClientConfig) (*Client, error) { kaParams := keepalive.ClientParameters{ Time: 10 * time.Second, Timeout: 3 * time.Second, - PermitWithoutStream: true, + PermitWithoutStream: false, } opts = append(opts, grpc.WithKeepaliveParams(kaParams)) } opts = append(opts, grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(4*1024*1024), // 4MB - grpc.MaxCallSendMsgSize(4*1024*1024), // 4MB + grpc.MaxCallRecvMsgSize(4*1024*1024), + grpc.MaxCallSendMsgSize(4*1024*1024), ), ) @@ -78,15 +85,98 @@ func NewClient(config *ClientConfig) (*Client, error) { return nil, fmt.Errorf("failed to connect to gRPC server at %s: %w", config.Address, err) } - log.Printf("Successfully connected to gRPC server at %s", config.Address) identityService := gen.NewIdentityClient(conn) + eventService := gen.NewEventServiceClient(conn) + return &Client{ conn: conn, config: config, IdentityService: identityService, + eventService: eventService, + sessionRegistry: sessionRegistry, }, nil } +func (c *Client) SubscribeEvents(ctx context.Context) error { + for { + if ctx.Err() != nil { + log.Println("Context cancelled, stopping event subscription") + return ctx.Err() + } + + log.Println("Subscribing to events...") + stream, err := c.eventService.Subscribe(ctx, &empty.Empty{}) + if err != nil { + log.Printf("Failed to subscribe: %v. Retrying in 10 seconds...", err) + select { + case <-time.After(10 * time.Second): + case <-ctx.Done(): + return ctx.Err() + } + continue + } + + if err := c.processEventStream(ctx, stream); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + log.Printf("Stream error: %v. Reconnecting in 10 seconds...", err) + select { + case <-time.After(10 * time.Second): + case <-ctx.Done(): + return ctx.Err() + } + } + } +} + +func (c *Client) processEventStream(ctx context.Context, stream gen.EventService_SubscribeClient) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + event, err := stream.Recv() + if err != nil { + st, ok := status.FromError(err) + if !ok { + return fmt.Errorf("non-gRPC error: %w", err) + } + + switch st.Code() { + case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded: + return fmt.Errorf("stream closed [%s]: %s", st.Code(), st.Message()) + default: + return fmt.Errorf("gRPC error [%s]: %s", st.Code(), st.Message()) + } + } + + if event != nil { + dataEvent := event.GetDataEvent() + if dataEvent != nil { + oldSlug := dataEvent.GetOld() + newSlug := dataEvent.GetNew() + + userSession, exist := c.sessionRegistry.Get(oldSlug) + if !exist { + log.Printf("Session with slug '%s' not found, ignoring event", oldSlug) + continue + } + success := c.sessionRegistry.Update(oldSlug, newSlug) + + if success { + log.Printf("Successfully updated session slug from '%s' to '%s'", oldSlug, newSlug) + userSession.GetInteraction().Redraw() + } else { + log.Printf("Failed to update session slug from '%s' to '%s'", oldSlug, newSlug) + } + } + } + } +} + func (c *Client) GetConnection() *grpc.ClientConn { return c.conn } @@ -99,88 +189,23 @@ func (c *Client) Close() error { return nil } -func (c *Client) IsConnected() bool { - if c.conn == nil { - return false - } - state := c.conn.GetState() - return state.String() == "READY" || state.String() == "IDLE" -} +func (c *Client) CheckServerHealth(ctx context.Context) error { + healthClient := grpc_health_v1.NewHealthClient(c.GetConnection()) + resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{ + Service: "", + }) -func (c *Client) Reconnect() error { - if err := c.Close(); err != nil { - log.Printf("Warning: error closing existing connection: %v", err) - } - - var opts []grpc.DialOption - - if c.config.UseTLS { - tlsConfig := &tls.Config{ - InsecureSkipVerify: c.config.InsecureSkipVerify, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - if c.config.KeepAlive { - kaParams := keepalive.ClientParameters{ - Time: 10 * time.Second, - Timeout: 3 * time.Second, - PermitWithoutStream: true, - } - opts = append(opts, grpc.WithKeepaliveParams(kaParams)) - } - - opts = append(opts, - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(4*1024*1024), - grpc.MaxCallSendMsgSize(4*1024*1024), - ), - ) - - conn, err := grpc.NewClient(c.config.Address, opts...) if err != nil { - return fmt.Errorf("failed to reconnect to gRPC server at %s: %w", c.config.Address, err) + return fmt.Errorf("health check failed: %w", err) } - c.conn = conn - log.Printf("Successfully reconnected to gRPC server at %s", c.config.Address) - return nil -} - -func (c *Client) WaitForReady(ctx context.Context) error { - if c.conn == nil { - return fmt.Errorf("connection is nil") - } - - _, ok := ctx.Deadline() - if !ok { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, c.config.Timeout) - defer cancel() - } - - currentState := c.conn.GetState() - for currentState.String() != "READY" { - if !c.conn.WaitForStateChange(ctx, currentState) { - return fmt.Errorf("timeout waiting for connection to be ready") - } - currentState = c.conn.GetState() - - if currentState.String() == "READY" || currentState.String() == "IDLE" { - return nil - } - - if currentState.String() == "SHUTDOWN" || currentState.String() == "TRANSIENT_FAILURE" { - return fmt.Errorf("connection is in %s state", currentState.String()) - } + if resp.Status != grpc_health_v1.HealthCheckResponse_SERVING { + return fmt.Errorf("server not serving: %v", resp.Status) } return nil } -func (c *Client) GetConfig() *ClientConfig { +func (c *Client) GetConfig() *GrpcConfig { return c.config } diff --git a/main.go b/main.go index 60a55f1..5f7a82b 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,15 @@ package main import ( + "context" "fmt" "log" "net/http" _ "net/http/pprof" "os" + "time" "tunnel_pls/internal/config" + "tunnel_pls/internal/grpc/client" "tunnel_pls/internal/key" "tunnel_pls/server" "tunnel_pls/session" @@ -61,9 +64,49 @@ func main() { sshConfig.AddHostKey(private) sessionRegistry := session.NewRegistry() - app, err := server.NewServer(sshConfig, sessionRegistry) + grpcClient, err := client.New(&client.GrpcConfig{ + Address: "localhost:8080", + UseTLS: false, + InsecureSkipVerify: false, + Timeout: 10 * time.Second, + KeepAlive: true, + MaxRetries: 3, + }, sessionRegistry) + if err != nil { + return + } + defer func(grpcClient *client.Client) { + err := grpcClient.Close() + if err != nil { + + } + }(grpcClient) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + err = grpcClient.CheckServerHealth(ctx) + if err != nil { + log.Fatalf("gRPC health check failed: %s", err) + return + } + cancel() + + ctx, cancel = context.WithCancel(context.Background()) + //go func(err error) { + // if !errors.Is(err, ctx.Err()) { + // log.Fatalf("Event subscription error: %s", err) + // } + //}(grpcClient.SubscribeEvents(ctx)) + go func() { + err := grpcClient.SubscribeEvents(ctx) + if err != nil { + return + } + }() + + app, err := server.NewServer(sshConfig, sessionRegistry, grpcClient) if err != nil { log.Fatalf("Failed to start server: %s", err) } app.Start() + cancel() } diff --git a/server/server.go b/server/server.go index 2b9fda4..53e8e3f 100644 --- a/server/server.go +++ b/server/server.go @@ -5,6 +5,7 @@ import ( "log" "net" "tunnel_pls/internal/config" + "tunnel_pls/internal/grpc/client" "tunnel_pls/session" "golang.org/x/crypto/ssh" @@ -14,9 +15,10 @@ type Server struct { conn *net.Listener config *ssh.ServerConfig sessionRegistry session.Registry + grpcClient *client.Client } -func NewServer(sshConfig *ssh.ServerConfig, sessionRegistry session.Registry) (*Server, error) { +func NewServer(sshConfig *ssh.ServerConfig, sessionRegistry session.Registry, grpcClient *client.Client) (*Server, error) { listener, err := net.Listen("tcp", fmt.Sprintf(":%s", config.Getenv("PORT", "2200"))) if err != nil { log.Fatalf("failed to listen on port 2200: %v", err) @@ -42,6 +44,7 @@ func NewServer(sshConfig *ssh.ServerConfig, sessionRegistry session.Registry) (* conn: &listener, config: sshConfig, sessionRegistry: sessionRegistry, + grpcClient: grpcClient, }, nil } @@ -69,9 +72,13 @@ func (s *Server) handleConnection(conn net.Conn) { } return } - - log.Println("SSH connection established:", sshConn.User()) - + //ctx := context.Background() + //log.Println("SSH connection established:", sshConn.User()) + //get, err := s.grpcClient.IdentityService.Get(ctx, &gen.IdentifierRequest{Id: sshConn.User()}) + //if err != nil { + // return + //} + //fmt.Println(get) sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry) err = sshSession.Start() if err != nil { diff --git a/session/interaction/interaction.go b/session/interaction/interaction.go index 2b24e60..9356a3a 100644 --- a/session/interaction/interaction.go +++ b/session/interaction/interaction.go @@ -31,6 +31,7 @@ type Controller interface { SetSlugModificator(func(oldSlug, newSlug string) bool) Start() SetWH(w, h int) + Redraw() } type Forwarder interface { @@ -65,7 +66,6 @@ type commandItem struct { } type model struct { - tunnelURL string domain string protocol string tunnelType types.TunnelType @@ -84,6 +84,13 @@ type model struct { height int } +func (m *model) getTunnelURL() string { + if m.tunnelType == types.HTTP { + return buildURL(m.protocol, m.interaction.slugManager.Get(), m.domain) + } + return fmt.Sprintf("tcp://%s:%d", m.domain, m.port) +} + type keymap struct { quit key.Binding command key.Binding @@ -163,11 +170,11 @@ func tickCmd(d time.Duration) tea.Cmd { }) } -func (m model) Init() tea.Cmd { +func (m *model) Init() tea.Cmd { return tea.Batch(textinput.Blink, tea.WindowSize()) } -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { @@ -225,7 +232,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - m.tunnelURL = buildURL(m.protocol, inputValue, m.domain) m.editingSlug = false m.slugError = "" return m, tea.Batch(tea.ClearScreen, textinput.Blink) @@ -291,14 +297,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m model) helpView() string { +func (i *Interaction) Redraw() { + if i.program != nil { + i.program.Send(tea.ClearScreen()) + } +} + +func (m *model) helpView() string { return "\n" + m.help.ShortHelpView([]key.Binding{ m.keymap.command, m.keymap.quit, }) } -func (m model) View() string { +func (m *model) View() string { if m.quitting { return "" } @@ -659,11 +671,11 @@ func (m model) View() string { MarginBottom(boxMargin). Width(boxMaxWidth) - urlDisplay := m.tunnelURL - if shouldUseCompactLayout(m.width, 80) && len(m.tunnelURL) > m.width-20 { + urlDisplay := m.getTunnelURL() + if shouldUseCompactLayout(m.width, 80) && len(urlDisplay) > m.width-20 { maxLen := m.width - 25 if maxLen > 10 { - urlDisplay = truncateString(m.tunnelURL, maxLen) + urlDisplay = truncateString(urlDisplay, maxLen) } } @@ -737,13 +749,6 @@ func (i *Interaction) Start() { tunnelType := i.forwarder.GetTunnelType() port := i.forwarder.GetForwardedPort() - var tunnelURL string - if tunnelType == types.HTTP { - tunnelURL = buildURL(protocol, i.slugManager.Get(), domain) - } else { - tunnelURL = fmt.Sprintf("tcp://%s:%d", domain, port) - } - items := []list.Item{ commandItem{name: "slug", desc: "Set custom subdomain"}, commandItem{name: "tunnel-type", desc: "Change tunnel type (Coming Soon)"}, @@ -764,8 +769,7 @@ func (i *Interaction) Start() { ti.CharLimit = 20 ti.Width = 50 - m := model{ - tunnelURL: tunnelURL, + m := &model{ domain: domain, protocol: protocol, tunnelType: tunnelType,