feat/grpc-integration #59
@@ -3,13 +3,14 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
"tunnel_pls/session"
|
"tunnel_pls/session"
|
||||||
|
|
||||||
"git.fossy.my.id/bagas/tunnel-please-grpc/gen"
|
proto "git.fossy.my.id/bagas/tunnel-please-grpc/gen"
|
||||||
"github.com/golang/protobuf/ptypes/empty"
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
@@ -26,14 +27,17 @@ type GrpcConfig struct {
|
|||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
KeepAlive bool
|
KeepAlive bool
|
||||||
MaxRetries int
|
MaxRetries int
|
||||||
|
KeepAliveTime time.Duration
|
||||||
|
KeepAliveTimeout time.Duration
|
||||||
|
PermitWithoutStream bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
config *GrpcConfig
|
config *GrpcConfig
|
||||||
sessionRegistry session.Registry
|
sessionRegistry session.Registry
|
||||||
IdentityService gen.IdentityClient
|
slugService proto.SlugChangeClient
|
||||||
eventService gen.EventServiceClient
|
eventService proto.EventServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *GrpcConfig {
|
func DefaultConfig() *GrpcConfig {
|
||||||
@@ -44,12 +48,32 @@ func DefaultConfig() *GrpcConfig {
|
|||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
KeepAlive: true,
|
KeepAlive: true,
|
||||||
MaxRetries: 3,
|
MaxRetries: 3,
|
||||||
|
KeepAliveTime: 2 * time.Minute,
|
||||||
|
KeepAliveTimeout: 10 * time.Second,
|
||||||
|
PermitWithoutStream: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error) {
|
func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error) {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = DefaultConfig()
|
config = DefaultConfig()
|
||||||
|
} else {
|
||||||
|
defaults := DefaultConfig()
|
||||||
|
if config.Address == "" {
|
||||||
|
config.Address = defaults.Address
|
||||||
|
}
|
||||||
|
if config.Timeout == 0 {
|
||||||
|
config.Timeout = defaults.Timeout
|
||||||
|
}
|
||||||
|
if config.MaxRetries == 0 {
|
||||||
|
config.MaxRetries = defaults.MaxRetries
|
||||||
|
}
|
||||||
|
if config.KeepAliveTime == 0 {
|
||||||
|
config.KeepAliveTime = defaults.KeepAliveTime
|
||||||
|
}
|
||||||
|
if config.KeepAliveTimeout == 0 {
|
||||||
|
config.KeepAliveTimeout = defaults.KeepAliveTimeout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts []grpc.DialOption
|
var opts []grpc.DialOption
|
||||||
@@ -66,9 +90,9 @@ func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error)
|
|||||||
|
|
||||||
if config.KeepAlive {
|
if config.KeepAlive {
|
||||||
kaParams := keepalive.ClientParameters{
|
kaParams := keepalive.ClientParameters{
|
||||||
Time: 10 * time.Second,
|
Time: config.KeepAliveTime,
|
||||||
Timeout: 3 * time.Second,
|
Timeout: config.KeepAliveTimeout,
|
||||||
PermitWithoutStream: false,
|
PermitWithoutStream: config.PermitWithoutStream,
|
||||||
}
|
}
|
||||||
opts = append(opts, grpc.WithKeepaliveParams(kaParams))
|
opts = append(opts, grpc.WithKeepaliveParams(kaParams))
|
||||||
}
|
}
|
||||||
@@ -85,94 +109,120 @@ func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error)
|
|||||||
return nil, fmt.Errorf("failed to connect to gRPC server at %s: %w", config.Address, err)
|
return nil, fmt.Errorf("failed to connect to gRPC server at %s: %w", config.Address, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
identityService := gen.NewIdentityClient(conn)
|
slugService := proto.NewSlugChangeClient(conn)
|
||||||
eventService := gen.NewEventServiceClient(conn)
|
eventService := proto.NewEventServiceClient(conn)
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
config: config,
|
config: config,
|
||||||
IdentityService: identityService,
|
slugService: slugService,
|
||||||
eventService: eventService,
|
|
||||||
sessionRegistry: sessionRegistry,
|
sessionRegistry: sessionRegistry,
|
||||||
|
eventService: eventService,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SubscribeEvents(ctx context.Context) error {
|
func (c *Client) SubscribeEvents(ctx context.Context, identity string) error {
|
||||||
for {
|
subscribe, err := c.eventService.Subscribe(ctx)
|
||||||
if ctx.Err() != nil {
|
if err != nil {
|
||||||
log.Println("Context cancelled, stopping event subscription")
|
return err
|
||||||
return ctx.Err()
|
}
|
||||||
|
err = subscribe.Send(&proto.Client{
|
||||||
|
Type: proto.EventType_AUTHENTICATION,
|
||||||
|
Payload: &proto.Client_AuthEvent{
|
||||||
|
AuthEvent: &proto.Authentication{
|
||||||
|
Identity: identity,
|
||||||
|
AuthToken: "test_auth_key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Authentication failed to send to gRPC server:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("Authentication Successfully sent to gRPC server")
|
||||||
|
err = c.processEventStream(subscribe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Subscribing to events...")
|
func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Client, proto.Controller]) error {
|
||||||
stream, err := c.eventService.Subscribe(ctx, &empty.Empty{})
|
for {
|
||||||
|
recv, err := subscribe.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to subscribe: %v. Retrying in 10 seconds...", err)
|
if isConnectionError(err) {
|
||||||
select {
|
log.Printf("connection error receiving from gRPC server: %v", err)
|
||||||
case <-time.After(10 * time.Second):
|
return err
|
||||||
case <-ctx.Done():
|
}
|
||||||
return ctx.Err()
|
log.Printf("non-connection receive error from gRPC server: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch recv.GetType() {
|
||||||
|
case proto.EventType_SLUG_CHANGE:
|
||||||
|
oldSlug := recv.GetSlugEvent().GetOld()
|
||||||
|
newSlug := recv.GetSlugEvent().GetNew()
|
||||||
|
session, err := c.sessionRegistry.Get(oldSlug)
|
||||||
|
if err != nil {
|
||||||
|
errSend := subscribe.Send(&proto.Client{
|
||||||
|
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||||
|
Payload: &proto.Client_SlugEventResponse{
|
||||||
|
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if errSend != nil {
|
||||||
|
if isConnectionError(errSend) {
|
||||||
|
log.Printf("connection error sending slug change failure: %v", errSend)
|
||||||
|
return errSend
|
||||||
|
}
|
||||||
|
log.Printf("non-connection send error for slug change failure: %v", errSend)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
err = c.sessionRegistry.Update(oldSlug, newSlug)
|
||||||
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 {
|
if err != nil {
|
||||||
st, ok := status.FromError(err)
|
errSend := subscribe.Send(&proto.Client{
|
||||||
if !ok {
|
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||||
return fmt.Errorf("non-gRPC error: %w", err)
|
Payload: &proto.Client_SlugEventResponse{
|
||||||
|
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if errSend != nil {
|
||||||
|
if isConnectionError(errSend) {
|
||||||
|
log.Printf("connection error sending slug change failure: %v", errSend)
|
||||||
|
return errSend
|
||||||
}
|
}
|
||||||
|
log.Printf("non-connection send error for slug change failure: %v", errSend)
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
success := c.sessionRegistry.Update(oldSlug, newSlug)
|
session.GetInteraction().Redraw()
|
||||||
|
err = subscribe.Send(&proto.Client{
|
||||||
if success {
|
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||||
log.Printf("Successfully updated session slug from '%s' to '%s'", oldSlug, newSlug)
|
Payload: &proto.Client_SlugEventResponse{
|
||||||
userSession.GetInteraction().Redraw()
|
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||||
} else {
|
Success: true,
|
||||||
log.Printf("Failed to update session slug from '%s' to '%s'", oldSlug, newSlug)
|
Message: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if 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
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown event type received: %v", recv.GetType())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,3 +259,18 @@ func (c *Client) CheckServerHealth(ctx context.Context) error {
|
|||||||
func (c *Client) GetConfig() *GrpcConfig {
|
func (c *Client) GetConfig() *GrpcConfig {
|
||||||
return c.config
|
return c.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isConnectionError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch status.Code(err) {
|
||||||
|
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
8
main.go
8
main.go
@@ -91,13 +91,9 @@ func main() {
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
ctx, cancel = context.WithCancel(context.Background())
|
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() {
|
go func() {
|
||||||
err := grpcClient.SubscribeEvents(ctx)
|
identity := config.Getenv("DOMAIN", "localhost")
|
||||||
|
err = grpcClient.SubscribeEvents(ctx, identity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,8 +313,8 @@ func (hs *httpServer) handler(conn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sshSession, exist := hs.sessionRegistry.Get(slug)
|
sshSession, err := hs.sessionRegistry.Get(slug)
|
||||||
if !exist {
|
if err != nil {
|
||||||
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
||||||
fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) +
|
fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) +
|
||||||
"Content-Length: 0\r\n" +
|
"Content-Length: 0\r\n" +
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ func (hs *httpServer) handlerTLS(conn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sshSession, exist := hs.sessionRegistry.Get(slug)
|
sshSession, err := hs.sessionRegistry.Get(slug)
|
||||||
if !exist {
|
if err != nil {
|
||||||
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
_, err = conn.Write([]byte("HTTP/1.1 301 Moved Permanently\r\n" +
|
||||||
fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) +
|
fmt.Sprintf("Location: https://tunnl.live/tunnel-not-found?slug=%s\r\n", slug) +
|
||||||
"Content-Length: 0\r\n" +
|
"Content-Length: 0\r\n" +
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
package interaction
|
|
||||||
|
|
||||||
const (
|
|
||||||
backspaceChar = 8
|
|
||||||
deleteChar = 127
|
|
||||||
enterChar = 13
|
|
||||||
escapeChar = 27
|
|
||||||
ctrlC = 3
|
|
||||||
forwardSlash = '/'
|
|
||||||
minPrintableChar = 32
|
|
||||||
maxPrintableChar = 126
|
|
||||||
|
|
||||||
minSlugLength = 3
|
|
||||||
maxSlugLength = 20
|
|
||||||
|
|
||||||
clearScreen = "\033[H\033[2J"
|
|
||||||
clearLine = "\033[K"
|
|
||||||
clearToLineEnd = "\r\033[K"
|
|
||||||
backspaceSeq = "\b \b"
|
|
||||||
|
|
||||||
minBoxWidth = 50
|
|
||||||
paddingRight = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
var forbiddenSlugs = map[string]struct{}{
|
|
||||||
"ping": {},
|
|
||||||
"staging": {},
|
|
||||||
"admin": {},
|
|
||||||
"root": {},
|
|
||||||
"api": {},
|
|
||||||
"www": {},
|
|
||||||
"support": {},
|
|
||||||
"help": {},
|
|
||||||
"status": {},
|
|
||||||
"health": {},
|
|
||||||
"login": {},
|
|
||||||
"logout": {},
|
|
||||||
"signup": {},
|
|
||||||
"register": {},
|
|
||||||
"settings": {},
|
|
||||||
"config": {},
|
|
||||||
"null": {},
|
|
||||||
"undefined": {},
|
|
||||||
"example": {},
|
|
||||||
"test": {},
|
|
||||||
"dev": {},
|
|
||||||
"system": {},
|
|
||||||
"administrator": {},
|
|
||||||
"dashboard": {},
|
|
||||||
"account": {},
|
|
||||||
"profile": {},
|
|
||||||
"user": {},
|
|
||||||
"users": {},
|
|
||||||
"auth": {},
|
|
||||||
"oauth": {},
|
|
||||||
"callback": {},
|
|
||||||
"webhook": {},
|
|
||||||
"webhooks": {},
|
|
||||||
"static": {},
|
|
||||||
"assets": {},
|
|
||||||
"cdn": {},
|
|
||||||
"mail": {},
|
|
||||||
"email": {},
|
|
||||||
"ftp": {},
|
|
||||||
"ssh": {},
|
|
||||||
"git": {},
|
|
||||||
"svn": {},
|
|
||||||
"blog": {},
|
|
||||||
"news": {},
|
|
||||||
"about": {},
|
|
||||||
"contact": {},
|
|
||||||
"terms": {},
|
|
||||||
"privacy": {},
|
|
||||||
"legal": {},
|
|
||||||
"billing": {},
|
|
||||||
"payment": {},
|
|
||||||
"checkout": {},
|
|
||||||
"cart": {},
|
|
||||||
"shop": {},
|
|
||||||
"store": {},
|
|
||||||
"download": {},
|
|
||||||
"uploads": {},
|
|
||||||
"images": {},
|
|
||||||
"img": {},
|
|
||||||
"css": {},
|
|
||||||
"js": {},
|
|
||||||
"fonts": {},
|
|
||||||
"public": {},
|
|
||||||
"private": {},
|
|
||||||
"internal": {},
|
|
||||||
"external": {},
|
|
||||||
"proxy": {},
|
|
||||||
"cache": {},
|
|
||||||
"debug": {},
|
|
||||||
"metrics": {},
|
|
||||||
"monitoring": {},
|
|
||||||
"graphql": {},
|
|
||||||
"rest": {},
|
|
||||||
"rpc": {},
|
|
||||||
"socket": {},
|
|
||||||
"ws": {},
|
|
||||||
"wss": {},
|
|
||||||
"app": {},
|
|
||||||
"apps": {},
|
|
||||||
"mobile": {},
|
|
||||||
"desktop": {},
|
|
||||||
"embed": {},
|
|
||||||
"widget": {},
|
|
||||||
"docs": {},
|
|
||||||
"documentation": {},
|
|
||||||
"wiki": {},
|
|
||||||
"forum": {},
|
|
||||||
"community": {},
|
|
||||||
"feedback": {},
|
|
||||||
"report": {},
|
|
||||||
"abuse": {},
|
|
||||||
"spam": {},
|
|
||||||
"security": {},
|
|
||||||
"verify": {},
|
|
||||||
"confirm": {},
|
|
||||||
"reset": {},
|
|
||||||
"password": {},
|
|
||||||
"recovery": {},
|
|
||||||
"unsubscribe": {},
|
|
||||||
"subscribe": {},
|
|
||||||
"notifications": {},
|
|
||||||
"alerts": {},
|
|
||||||
"messages": {},
|
|
||||||
"inbox": {},
|
|
||||||
"outbox": {},
|
|
||||||
"sent": {},
|
|
||||||
"draft": {},
|
|
||||||
"trash": {},
|
|
||||||
"archive": {},
|
|
||||||
"search": {},
|
|
||||||
"explore": {},
|
|
||||||
"discover": {},
|
|
||||||
"trending": {},
|
|
||||||
"popular": {},
|
|
||||||
"featured": {},
|
|
||||||
"new": {},
|
|
||||||
"latest": {},
|
|
||||||
"top": {},
|
|
||||||
"best": {},
|
|
||||||
"hot": {},
|
|
||||||
"random": {},
|
|
||||||
"all": {},
|
|
||||||
"any": {},
|
|
||||||
"none": {},
|
|
||||||
"true": {},
|
|
||||||
"false": {},
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ type Lifecycle interface {
|
|||||||
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) bool)
|
SetSlugModificator(func(oldSlug, newSlug string) error)
|
||||||
Start()
|
Start()
|
||||||
SetWH(w, h int)
|
SetWH(w, h int)
|
||||||
Redraw()
|
Redraw()
|
||||||
@@ -45,7 +45,7 @@ type Interaction struct {
|
|||||||
slugManager slug.Manager
|
slugManager slug.Manager
|
||||||
forwarder Forwarder
|
forwarder Forwarder
|
||||||
lifecycle Lifecycle
|
lifecycle Lifecycle
|
||||||
updateClientSlug func(oldSlug, newSlug string) bool
|
updateClientSlug func(oldSlug, newSlug string) error
|
||||||
program *tea.Program
|
program *tea.Program
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -121,7 +121,7 @@ func (i *Interaction) SetChannel(channel ssh.Channel) {
|
|||||||
i.channel = channel
|
i.channel = channel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interaction) SetSlugModificator(modificator func(oldSlug, newSlug string) (success bool)) {
|
func (i *Interaction) SetSlugModificator(modificator func(oldSlug, newSlug string) error) {
|
||||||
i.updateClientSlug = modificator
|
i.updateClientSlug = modificator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,20 +218,10 @@ 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 isForbiddenSlug(inputValue) {
|
m.slugError = err.Error()
|
||||||
m.slugError = "This subdomain is reserved. Please choose a different one."
|
|
||||||
return m, nil
|
|
||||||
} else if !isValidSlug(inputValue) {
|
|
||||||
m.slugError = "Invalid subdomain. Follow the rules."
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.interaction.updateClientSlug(m.interaction.slugManager.Get(), inputValue) {
|
|
||||||
m.slugError = "Someone already uses this subdomain."
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.editingSlug = false
|
m.editingSlug = false
|
||||||
m.slugError = ""
|
m.slugError = ""
|
||||||
return m, tea.Batch(tea.ClearScreen, textinput.Blink)
|
return m, tea.Batch(tea.ClearScreen, textinput.Blink)
|
||||||
@@ -823,30 +813,3 @@ func buildURL(protocol, subdomain, domain string) string {
|
|||||||
func generateRandomSubdomain() string {
|
func generateRandomSubdomain() string {
|
||||||
return random.GenerateRandomString(20)
|
return random.GenerateRandomString(20)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidSlug(slug string) bool {
|
|
||||||
if len(slug) < minSlugLength || len(slug) > maxSlugLength {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if slug[0] == '-' || slug[len(slug)-1] == '-' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range slug {
|
|
||||||
if !isValidSlugChar(byte(c)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidSlugChar(c byte) bool {
|
|
||||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
func isForbiddenSlug(slug string) bool {
|
|
||||||
_, ok := forbiddenSlugs[slug]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package session
|
package session
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
type Registry interface {
|
type Registry interface {
|
||||||
Get(slug string) (session *SSHSession, exist bool)
|
Get(slug string) (session *SSHSession, err error)
|
||||||
Update(oldSlug, newSlug string) (success bool)
|
Update(oldSlug, newSlug string) error
|
||||||
Register(slug string, session *SSHSession) (success bool)
|
Register(slug string, session *SSHSession) (success bool)
|
||||||
Remove(slug string)
|
Remove(slug string)
|
||||||
}
|
}
|
||||||
@@ -19,31 +22,40 @@ func NewRegistry() Registry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registry) Get(slug string) (session *SSHSession, exist bool) {
|
func (r *registry) Get(slug string) (session *SSHSession, err error) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
session, exist = r.clients[slug]
|
client, ok := r.clients[slug]
|
||||||
return
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("session not found")
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) Update(oldSlug, newSlug string) error {
|
||||||
|
if isForbiddenSlug(newSlug) {
|
||||||
|
return fmt.Errorf("this subdomain is reserved. Please choose a different one")
|
||||||
|
} else if !isValidSlug(newSlug) {
|
||||||
|
return fmt.Errorf("invalid subdomain. Follow the rules")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registry) Update(oldSlug, newSlug string) (success bool) {
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
if _, exists := r.clients[newSlug]; exists && newSlug != oldSlug {
|
if _, exists := r.clients[newSlug]; exists && newSlug != oldSlug {
|
||||||
return false
|
return fmt.Errorf("someone already uses this subdomain")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, ok := r.clients[oldSlug]
|
client, ok := r.clients[oldSlug]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(r.clients, oldSlug)
|
delete(r.clients, oldSlug)
|
||||||
client.slugManager.Set(newSlug)
|
client.slugManager.Set(newSlug)
|
||||||
r.clients[newSlug] = client
|
r.clients[newSlug] = client
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registry) Register(slug string, session *SSHSession) (success bool) {
|
func (r *registry) Register(slug string, session *SSHSession) (success bool) {
|
||||||
@@ -64,3 +76,164 @@ func (r *registry) Remove(slug string) {
|
|||||||
|
|
||||||
delete(r.clients, slug)
|
delete(r.clients, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidSlug(slug string) bool {
|
||||||
|
if len(slug) < minSlugLength || len(slug) > maxSlugLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if slug[0] == '-' || slug[len(slug)-1] == '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range slug {
|
||||||
|
if !isValidSlugChar(byte(c)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidSlugChar(c byte) bool {
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isForbiddenSlug(slug string) bool {
|
||||||
|
_, ok := forbiddenSlugs[slug]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
var forbiddenSlugs = map[string]struct{}{
|
||||||
|
"ping": {},
|
||||||
|
"staging": {},
|
||||||
|
"admin": {},
|
||||||
|
"root": {},
|
||||||
|
"api": {},
|
||||||
|
"www": {},
|
||||||
|
"support": {},
|
||||||
|
"help": {},
|
||||||
|
"status": {},
|
||||||
|
"health": {},
|
||||||
|
"login": {},
|
||||||
|
"logout": {},
|
||||||
|
"signup": {},
|
||||||
|
"register": {},
|
||||||
|
"settings": {},
|
||||||
|
"config": {},
|
||||||
|
"null": {},
|
||||||
|
"undefined": {},
|
||||||
|
"example": {},
|
||||||
|
"test": {},
|
||||||
|
"dev": {},
|
||||||
|
"system": {},
|
||||||
|
"administrator": {},
|
||||||
|
"dashboard": {},
|
||||||
|
"account": {},
|
||||||
|
"profile": {},
|
||||||
|
"user": {},
|
||||||
|
"users": {},
|
||||||
|
"auth": {},
|
||||||
|
"oauth": {},
|
||||||
|
"callback": {},
|
||||||
|
"webhook": {},
|
||||||
|
"webhooks": {},
|
||||||
|
"static": {},
|
||||||
|
"assets": {},
|
||||||
|
"cdn": {},
|
||||||
|
"mail": {},
|
||||||
|
"email": {},
|
||||||
|
"ftp": {},
|
||||||
|
"ssh": {},
|
||||||
|
"git": {},
|
||||||
|
"svn": {},
|
||||||
|
"blog": {},
|
||||||
|
"news": {},
|
||||||
|
"about": {},
|
||||||
|
"contact": {},
|
||||||
|
"terms": {},
|
||||||
|
"privacy": {},
|
||||||
|
"legal": {},
|
||||||
|
"billing": {},
|
||||||
|
"payment": {},
|
||||||
|
"checkout": {},
|
||||||
|
"cart": {},
|
||||||
|
"shop": {},
|
||||||
|
"store": {},
|
||||||
|
"download": {},
|
||||||
|
"uploads": {},
|
||||||
|
"images": {},
|
||||||
|
"img": {},
|
||||||
|
"css": {},
|
||||||
|
"js": {},
|
||||||
|
"fonts": {},
|
||||||
|
"public": {},
|
||||||
|
"private": {},
|
||||||
|
"internal": {},
|
||||||
|
"external": {},
|
||||||
|
"proxy": {},
|
||||||
|
"cache": {},
|
||||||
|
"debug": {},
|
||||||
|
"metrics": {},
|
||||||
|
"monitoring": {},
|
||||||
|
"graphql": {},
|
||||||
|
"rest": {},
|
||||||
|
"rpc": {},
|
||||||
|
"socket": {},
|
||||||
|
"ws": {},
|
||||||
|
"wss": {},
|
||||||
|
"app": {},
|
||||||
|
"apps": {},
|
||||||
|
"mobile": {},
|
||||||
|
"desktop": {},
|
||||||
|
"embed": {},
|
||||||
|
"widget": {},
|
||||||
|
"docs": {},
|
||||||
|
"documentation": {},
|
||||||
|
"wiki": {},
|
||||||
|
"forum": {},
|
||||||
|
"community": {},
|
||||||
|
"feedback": {},
|
||||||
|
"report": {},
|
||||||
|
"abuse": {},
|
||||||
|
"spam": {},
|
||||||
|
"security": {},
|
||||||
|
"verify": {},
|
||||||
|
"confirm": {},
|
||||||
|
"reset": {},
|
||||||
|
"password": {},
|
||||||
|
"recovery": {},
|
||||||
|
"unsubscribe": {},
|
||||||
|
"subscribe": {},
|
||||||
|
"notifications": {},
|
||||||
|
"alerts": {},
|
||||||
|
"messages": {},
|
||||||
|
"inbox": {},
|
||||||
|
"outbox": {},
|
||||||
|
"sent": {},
|
||||||
|
"draft": {},
|
||||||
|
"trash": {},
|
||||||
|
"archive": {},
|
||||||
|
"search": {},
|
||||||
|
"explore": {},
|
||||||
|
"discover": {},
|
||||||
|
"trending": {},
|
||||||
|
"popular": {},
|
||||||
|
"featured": {},
|
||||||
|
"new": {},
|
||||||
|
"latest": {},
|
||||||
|
"top": {},
|
||||||
|
"best": {},
|
||||||
|
"hot": {},
|
||||||
|
"random": {},
|
||||||
|
"all": {},
|
||||||
|
"any": {},
|
||||||
|
"none": {},
|
||||||
|
"true": {},
|
||||||
|
"false": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
minSlugLength = 3
|
||||||
|
maxSlugLength = 20
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user