From 084284370c70e2c8ecba704774690b10012e1286 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Wed, 15 Oct 2025 18:44:51 -0500 Subject: [PATCH] initial commit --- cmd/cli/main.go | 66 ++++++++++++++++++++++++++++++++++++++ config.go | 5 +++ config.json | 11 +++++++ go.mod | 5 +++ go.sum | 10 ++++++ protocol.go | 44 +++++++++++++++++++++++++ router.go | 43 +++++++++++++++++++++++++ tcp-client.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ tcp-server.go | 78 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 347 insertions(+) create mode 100644 cmd/cli/main.go create mode 100644 config.go create mode 100644 config.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 protocol.go create mode 100644 router.go create mode 100644 tcp-client.go create mode 100644 tcp-server.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..210c565 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/jwetzell/showbridge-go" + "github.com/urfave/cli/v3" +) + +func main() { + cmd := &cli.Command{ + Name: "showbridge", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Value: "./config.json", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + configPath := c.String("config") + if configPath == "" { + return fmt.Errorf("config value cannot be empty") + } + + config, err := readConfig(configPath) + if err != nil { + return err + } + fmt.Printf("%+v\n", config) + router, err := showbridge.NewRouter(ctx, config) + if err != nil { + return err + } + router.Run() + + return nil + }, + } + + err := cmd.Run(context.Background(), os.Args) + + if err != nil { + panic(err) + } + +} + +func readConfig(configPath string) (showbridge.Config, error) { + configBytes, err := os.ReadFile(configPath) + + if err != nil { + return showbridge.Config{}, err + } + + config := showbridge.Config{} + + err = json.Unmarshal(configBytes, &config) + if err != nil { + return showbridge.Config{}, err + } + + return config, nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..5438350 --- /dev/null +++ b/config.go @@ -0,0 +1,5 @@ +package showbridge + +type Config struct { + Protocols []ProtocolConfig `json:"protocols"` +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..c2382b3 --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "protocols": [ + { + "type": "tcp.client", + "params": { + "host": "127.0.0.1", + "port": 8000 + } + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..402afa7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/jwetzell/showbridge-go + +go 1.25.1 + +require github.com/urfave/cli/v3 v3.4.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e5cca28 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= +github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +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/protocol.go b/protocol.go new file mode 100644 index 0000000..ca39094 --- /dev/null +++ b/protocol.go @@ -0,0 +1,44 @@ +package showbridge + +import ( + "context" + "fmt" + "sync" +) + +type Protocol interface { + Run(context.Context) error +} + +type ProtocolConfig struct { + Type string `json:"type"` + Params map[string]any `json:"params"` +} + +type ProtocolRegistration struct { + Type string `json:"type"` + New func(map[string]any) (Protocol, error) +} + +func RegisterProtocol(proto ProtocolRegistration) { + + if proto.Type == "" { + panic("protocol type is missing") + } + if proto.New == nil { + panic("missing ProtocolInfo.New") + } + + protocolRegistryMu.Lock() + defer protocolRegistryMu.Unlock() + + if _, ok := protocolRegistry[string(proto.Type)]; ok { + panic(fmt.Sprintf("protocol already registered: %s", proto.Type)) + } + protocolRegistry[string(proto.Type)] = proto +} + +var ( + protocolRegistryMu sync.RWMutex + protocolRegistry = make(map[string]ProtocolRegistration) +) diff --git a/router.go b/router.go new file mode 100644 index 0000000..3776581 --- /dev/null +++ b/router.go @@ -0,0 +1,43 @@ +package showbridge + +import ( + "context" + "fmt" +) + +type Router struct { + Context context.Context + ProtocolInstances []Protocol +} + +func NewRouter(ctx context.Context, config Config) (*Router, error) { + + router := Router{ + Context: ctx, + ProtocolInstances: []Protocol{}, + } + + for _, protocolDecl := range config.Protocols { + + protocolInfo, ok := protocolRegistry[protocolDecl.Type] + if !ok { + return nil, fmt.Errorf("problem loading protocol registration for protocol type: %s", protocolDecl.Type) + } + + protocolInstance, err := protocolInfo.New(protocolDecl.Params) + if err != nil { + return nil, err + } + + router.ProtocolInstances = append(router.ProtocolInstances, protocolInstance) + + } + + return &router, nil +} + +func (r *Router) Run() { + for _, protocolInstance := range r.ProtocolInstances { + protocolInstance.Run(r.Context) + } +} diff --git a/tcp-client.go b/tcp-client.go new file mode 100644 index 0000000..e2b0566 --- /dev/null +++ b/tcp-client.go @@ -0,0 +1,85 @@ +package showbridge + +import ( + "context" + "fmt" + "net" + "time" +) + +type TCPClient struct { + Host string + Port uint16 +} + +func init() { + RegisterProtocol(ProtocolRegistration{ + Type: "tcp.client", + New: func(params map[string]any) (Protocol, error) { + + host, ok := params["host"] + + if !ok { + return nil, fmt.Errorf("tcp client requires a host parameter") + } + + hostString, ok := host.(string) + + if !ok { + return nil, fmt.Errorf("tcp client host must be uint16") + } + + port, ok := params["port"] + if !ok { + return nil, fmt.Errorf("tcp client requires a port parameter") + } + + portNum, ok := port.(float64) + + if !ok { + return nil, fmt.Errorf("tcp client port must be uint16") + } + + return TCPClient{Host: hostString, Port: uint16(portNum)}, nil + }, + }) +} + +func (ts TCPClient) Run(ctx context.Context) error { + for { + client, err := net.Dial("tcp", fmt.Sprintf(":%d", ts.Port)) + if err != nil { + fmt.Println(err) + time.Sleep(time.Second * 2) + continue + } + + buffer := make([]byte, 1024) + select { + case <-ctx.Done(): + return nil + default: + READ: + for { + select { + case <-ctx.Done(): + return nil + default: + byteCount, err := client.Read(buffer) + + if err != nil { + fmt.Println("connection closed") + break READ + } + + if byteCount > 0 { + fmt.Println(buffer[0:byteCount]) + } + } + + } + } + + } + +} diff --git a/tcp-server.go b/tcp-server.go new file mode 100644 index 0000000..518c20a --- /dev/null +++ b/tcp-server.go @@ -0,0 +1,78 @@ +package showbridge + +import ( + "context" + "fmt" + "net" +) + +type TCPServer struct { + Port uint16 +} + +func init() { + RegisterProtocol(ProtocolRegistration{ + Type: "tcp.server", + New: func(params map[string]any) (Protocol, error) { + port, ok := params["port"] + if !ok { + return nil, fmt.Errorf("tcp server requires a port parameter") + } + + portNum, ok := port.(float64) + + if !ok { + return nil, fmt.Errorf("tcp server port must be uint16") + } + + return TCPServer{Port: uint16(portNum)}, nil + }, + }) +} + +func (ts TCPServer) HandleClient(ctx context.Context, client net.Conn) { + fmt.Printf("handling connection %s\n", client.RemoteAddr()) + + buffer := make([]byte, 1024) + for { + select { + case <-ctx.Done(): + return + default: + byteCount, err := client.Read(buffer) + + if err != nil { + if err.Error() == "EOF" { + fmt.Println("connection closed") + } + return + } + + if byteCount > 0 { + fmt.Println(buffer[0:byteCount]) + } + } + + } +} + +func (ts TCPServer) Run(ctx context.Context) error { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", ts.Port)) + if err != nil { + return err + } + + for { + select { + case <-ctx.Done(): + return nil + default: + client, err := listener.Accept() + if err != nil { + return err + } + go ts.HandleClient(ctx, client) + } + } + +}