From 1a9c5a47a5e81468eec5c07966969e13fb3e0e55 Mon Sep 17 00:00:00 2001 From: Marc Szanto <11840265+Xemdo@users.noreply.github.com> Date: Mon, 3 Apr 2023 18:03:46 -0700 Subject: [PATCH 1/2] twitch token now listens and serves separately, which lets bind errors be focused. Also introduced --ip and --port flags; Fixes #221 --- cmd/token.go | 12 ++++++++++-- internal/login/login.go | 38 ++++++++++++++++++++++++------------ internal/login/login_test.go | 4 ++-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/cmd/token.go b/cmd/token.go index 6b072c94..8761cc70 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -3,6 +3,9 @@ package cmd import ( + "fmt" + "strconv" + "github.com/twitchdev/twitch-cli/internal/login" "github.com/spf13/cobra" @@ -13,6 +16,8 @@ var isUserToken bool var userScopes string var revokeToken string var overrideClientId string +var tokenServerPort int +var tokenServerIP string // loginCmd represents the login command var loginCmd = &cobra.Command{ @@ -28,13 +33,16 @@ func init() { loginCmd.Flags().StringVarP(&userScopes, "scopes", "s", "", "Space seperated list of scopes to request with your user token.") loginCmd.Flags().StringVarP(&revokeToken, "revoke", "r", "", "Instead of generating a new token, revoke the one passed to this parameter.") loginCmd.Flags().StringVar(&overrideClientId, "client-id", "", "Override/manually set client ID for token actions. By default client ID from CLI config will be used.") + loginCmd.Flags().StringVar(&tokenServerIP, "ip", "localhost", "Manually set the IP address to be binded to for the User Token web server.") + loginCmd.Flags().IntVarP(&tokenServerPort, "port", "p", 3000, "Manually set the port to be used for the User Token web server.") } func loginCmdRun(cmd *cobra.Command, args []string) { clientID = viper.GetString("clientId") clientSecret = viper.GetString("clientSecret") - redirectURL := "http://localhost:3000" + webserverPort := strconv.Itoa(tokenServerPort) + redirectURL := fmt.Sprintf("http://%v:%v", tokenServerIP, webserverPort) if clientID == "" || clientSecret == "" { println("No Client ID or Secret found in configuration. Triggering configuration now.") @@ -61,7 +69,7 @@ func loginCmdRun(cmd *cobra.Command, args []string) { login.CredentialsLogout(p) } else if isUserToken == true { p.URL = login.UserCredentialsURL - login.UserCredentialsLogin(p) + login.UserCredentialsLogin(p, tokenServerIP, webserverPort) } else { p.URL = login.ClientCredentialsURL login.ClientCredentialsLogin(p) diff --git a/internal/login/login.go b/internal/login/login.go index 3af8d1c3..3b4a58db 100644 --- a/internal/login/login.go +++ b/internal/login/login.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "log" + "net" "net/http" "net/url" "os/exec" @@ -95,7 +96,7 @@ func ClientCredentialsLogin(p LoginParameters) (LoginResponse, error) { return r, nil } -func UserCredentialsLogin(p LoginParameters) (LoginResponse, error) { +func UserCredentialsLogin(p LoginParameters, webserverIP string, webserverPort string) (LoginResponse, error) { u, err := url.Parse(p.AuthorizeURL) if err != nil { log.Fatal(err) @@ -115,17 +116,20 @@ func UserCredentialsLogin(p LoginParameters) (LoginResponse, error) { q.Set("state", state) u.RawQuery = q.Encode() - fmt.Println("Opening browser. Press Ctrl+C to cancel...") - err = openBrowser(u.String()) - if err != nil { - fmt.Printf("Unable to open default browser. You can manually navigate to this URL to complete the login: %s\n", u.String()) + execOpenBrowser := func() { + fmt.Println("Opening browser. Press Ctrl+C to cancel...") + err = openBrowser(u.String()) + if err != nil { + fmt.Printf("Unable to open default browser. You can manually navigate to this URL to complete the login: %s\n", u.String()) + } } - ur, err := userAuthServer() + urp, err := userAuthServer(webserverIP, webserverPort, execOpenBrowser) if err != nil { fmt.Printf("Error processing request; %v\n", err.Error()) return LoginResponse{}, err } + ur := *urp if ur.State != state { log.Fatal("state mismatch") @@ -175,7 +179,7 @@ func CredentialsLogout(p LoginParameters) (LoginResponse, error) { } if resp.StatusCode != http.StatusOK { - log.Printf("API responded with an error while revoking token: %v", string(resp.Body)) + log.Printf("API responded with an error while revoking token: [%v] %v", resp.StatusCode, string(resp.Body)) return LoginResponse{}, errors.New("API responded with an error while revoking token") } @@ -256,9 +260,9 @@ func openBrowser(url string) error { return err } -func userAuthServer() (UserAuthorizationQueryResponse, error) { +func userAuthServer(ip string, port string, onSuccessfulListenCallback func()) (*UserAuthorizationQueryResponse, error) { m := http.NewServeMux() - s := http.Server{Addr: ":3000", Handler: m} + s := http.Server{Addr: fmt.Sprintf("%v:%v", ip, port), Handler: m} userAuth := make(chan UserAuthorizationQueryResponse) m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/favicon.ico" { @@ -282,8 +286,19 @@ func userAuthServer() (UserAuthorizationQueryResponse, error) { userAuth <- u } }) + + ln, err := net.Listen("tcp", s.Addr) + defer s.Shutdown(context.Background()) + if err != nil { + return nil, err + } + + if onSuccessfulListenCallback != nil { + onSuccessfulListenCallback() + } + go func() { - if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := s.Serve(ln); err != nil && err != http.ErrServerClosed { log.Fatal(err) return } @@ -292,8 +307,7 @@ func userAuthServer() (UserAuthorizationQueryResponse, error) { log.Printf("Waiting for authorization response ...") userAuthResponse := <-userAuth log.Printf("Closing local server ...") - s.Shutdown(context.Background()) - return userAuthResponse, userAuthResponse.Error + return &userAuthResponse, userAuthResponse.Error } func storeInConfig(token string, refresh string, scopes []string, expiresAt time.Time) { diff --git a/internal/login/login_test.go b/internal/login/login_test.go index b315662d..5bd02c10 100644 --- a/internal/login/login_test.go +++ b/internal/login/login_test.go @@ -146,9 +146,9 @@ func TestUserAuthServer(t *testing.T) { userResponse := make(chan UserAuthorizationQueryResponse) go func() { - res, err := userAuthServer() + res, err := userAuthServer("", "3000", nil) a.Nil(err) - userResponse <- res + userResponse <- *res }() time.Sleep(25) From 762b7f4b2c43b34476a5d1e7809e2a8f1489330e Mon Sep 17 00:00:00 2001 From: Marc Szanto <11840265+Xemdo@users.noreply.github.com> Date: Mon, 3 Apr 2023 18:12:05 -0700 Subject: [PATCH 2/2] Added --ip flag for 'twitch event websocket start' --- cmd/events.go | 7 +++++-- internal/events/websocket/mock_server/manager.go | 14 ++++++++------ .../events/websocket/mock_server/rpc_handler.go | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd/events.go b/cmd/events.go index 39b2aa7a..24e078ed 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -45,6 +45,8 @@ var ( clientId string version string websocketClient string + websocketServerIP string + websocketServerPort int ) // websocketCmd-specific flags @@ -173,7 +175,8 @@ func init() { // websocket flags /// flags for start-server - websocketCmd.Flags().IntVarP(&port, "port", "p", 8080, "Defines the port that the mock EventSub websocket server will run on.") + websocketCmd.Flags().StringVar(&websocketServerIP, "ip", "127.0.0.1", "Defines the ip that the mock EventSub websocket server will bind to.") + websocketCmd.Flags().IntVarP(&websocketServerPort, "port", "p", 8080, "Defines the port that the mock EventSub websocket server will run on.") websocketCmd.Flags().BoolVar(&wsDebug, "debug", false, "Set on/off for debug messages for the EventSub WebSocket server.") websocketCmd.Flags().BoolVarP(&wsStrict, "require-subscription", "S", false, "Requires subscriptions for all events, and activates 10 second subscription requirement.") @@ -332,7 +335,7 @@ func websocketCmdRun(cmd *cobra.Command, args []string) { if args[0] == "start-server" || args[0] == "start" { log.Printf("`Ctrl + C` to exit mock WebSocket servers.") - mock_server.StartWebsocketServer(wsDebug, port, wsStrict) + mock_server.StartWebsocketServer(wsDebug, websocketServerIP, websocketServerPort, wsStrict) } else { // Forward all other commands via RPC websocket.ForwardWebsocketCommand(args[0], websocket.WebsocketCommandParameters{ diff --git a/internal/events/websocket/mock_server/manager.go b/internal/events/websocket/mock_server/manager.go index f4abee95..bde642b4 100644 --- a/internal/events/websocket/mock_server/manager.go +++ b/internal/events/websocket/mock_server/manager.go @@ -24,6 +24,7 @@ type ServerManager struct { serverList *util.List[WebSocketServer] reconnectTesting bool primaryServer string + ip string port int debugEnabled bool strictMode bool @@ -31,11 +32,12 @@ type ServerManager struct { var serverManager *ServerManager -func StartWebsocketServer(enableDebug bool, port int, strictMode bool) { +func StartWebsocketServer(enableDebug bool, ip string, port int, strictMode bool) { serverManager = &ServerManager{ serverList: &util.List[WebSocketServer]{ Elements: make(map[string]*WebSocketServer), }, + ip: ip, port: port, reconnectTesting: false, strictMode: strictMode, @@ -75,7 +77,7 @@ func StartWebsocketServer(enableDebug bool, port int, strictMode bool) { // Start HTTP server go func() { // Listen to port - listen, err := net.Listen("tcp", fmt.Sprintf(":%v", port)) + listen, err := net.Listen("tcp", fmt.Sprintf("%v:%v", ip, port)) if err != nil { log.Fatalf("Cannot start HTTP server: %v", err) return @@ -86,14 +88,14 @@ func StartWebsocketServer(enableDebug bool, port int, strictMode bool) { lightYellow := color.New(color.FgHiYellow).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc() - log.Printf(lightBlue("Started WebSocket server on 127.0.0.1:%v"), port) + log.Printf(lightBlue("Started WebSocket server on %v:%v"), ip, port) if serverManager.strictMode { log.Printf(lightBlue("--require-subscription enabled. Clients will have 10 seconds to subscribe before being disconnected.")) } fmt.Println() - log.Printf(yellow("Simulate subscribing to events at: http://127.0.0.1:%v/eventsub/subscriptions"), port) + log.Printf(yellow("Simulate subscribing to events at: http://%v:%v/eventsub/subscriptions"), ip, port) log.Printf(yellow("POST, GET, and DELETE are supported")) log.Printf(yellow("For more info: https://dev.twitch.tv/docs/cli/websocket-event-command/#simulate-subscribing-to-mock-eventsub")) @@ -107,7 +109,7 @@ func StartWebsocketServer(enableDebug bool, port int, strictMode bool) { log.Printf(lightGreen("For further usage information, please see our official documentation:\nhttps://dev.twitch.tv/docs/cli/websocket-event-command/")) fmt.Println() - log.Printf(lightBlue("Connect to the WebSocket server at: ")+"ws://127.0.0.1:%v/ws", port) + log.Printf(lightBlue("Connect to the WebSocket server at: ")+"ws://%v:%v/ws", ip, port) // Serve HTTP server if err := http.Serve(listen, m); err != nil { @@ -136,7 +138,7 @@ func StartWebsocketServer(enableDebug bool, port int, strictMode bool) { func wsPageHandler(w http.ResponseWriter, r *http.Request) { server, ok := serverManager.serverList.Get(serverManager.primaryServer) if !ok { - log.Printf("Failed to find primary server [%v] when new client was accessing ws://127.0.0.1:%v/ws -- Aborting...", serverManager.primaryServer, serverManager.port) + log.Printf("Failed to find primary server [%v] when new client was accessing ws://%v:%v/ws -- Aborting...", serverManager.primaryServer, serverManager.ip, serverManager.port) return } diff --git a/internal/events/websocket/mock_server/rpc_handler.go b/internal/events/websocket/mock_server/rpc_handler.go index dade9444..0ee743b2 100644 --- a/internal/events/websocket/mock_server/rpc_handler.go +++ b/internal/events/websocket/mock_server/rpc_handler.go @@ -215,7 +215,7 @@ func RPCSubscriptionHandler(args rpc.RPCArgs) rpc.RPCResponse { return rpc.RPCResponse{ ResponseCode: COMMAND_RESPONSE_MISSING_FLAG, DetailedInfo: "Command \"subscription\" requires flags --status, --subscription, and --session" + - fmt.Sprintf("\nThe flag --subscription must be the ID of the subscription made at http://localhost:%v/eventsub/subscriptions", serverManager.port) + + fmt.Sprintf("\nThe flag --subscription must be the ID of the subscription made at http://%v:%v/eventsub/subscriptions", serverManager.ip, serverManager.port) + "\nThe flag --status must be one of the non-webhook status options defined here:" + "\nhttps://dev.twitch.tv/docs/api/reference/#get-eventsub-subscriptions" + "\n\nExample: twitch event websocket subscription --status=user_removed --subscription=82a855-fae8-93bff0",