diff --git a/go.mod b/go.mod index 56f2801..fc3e96b 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,15 @@ go 1.15 replace github.com/blues/note-cli/lib => ./lib -// replace github.com/blues/note-go => ./note-go +// uncomment this for easier testing locally +// replace github.com/blues/note-go => ../hub/note-go require ( github.com/blues/note-cli/lib v0.0.0-20240515194341-6ba45582741d - github.com/blues/note-go v1.7.2 + github.com/blues/note-go v1.7.3 github.com/fatih/color v1.17.0 github.com/peterh/liner v1.2.2 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - golang.org/x/term v0.20.0 ) require ( @@ -24,6 +24,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil/v3 v3.24.4 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect - go.bug.st/serial v1.6.2 // indirect + go.bug.st/serial v1.6.2 + golang.org/x/sys v0.20.0 // indirect periph.io/x/host/v3 v3.8.2 // indirect ) diff --git a/go.sum b/go.sum index a2f377c..8d970de 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/blues/note-go v1.5.0/go.mod h1:F66ZqObdOhxRRXIwn9+YhVGqB93jMAnqlO2ibwMa998= -github.com/blues/note-go v1.7.2 h1:hasFsMNTnvyp5MPpnhqL46LC1tqzxQ5sGg+NOcJUV8E= -github.com/blues/note-go v1.7.2/go.mod h1:GfslvbmFus7z05P1YykcbMedTKTuDNTf8ryBb1Qjq/4= +github.com/blues/note-go v1.7.3 h1:aj7kprrgadR6sgxJs0fNMQfZRk1J0Mi4L2ythRtcxus= +github.com/blues/note-go v1.7.3/go.mod h1:GfslvbmFus7z05P1YykcbMedTKTuDNTf8ryBb1Qjq/4= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -99,8 +99,6 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/lib/config.go b/lib/config.go index bbf988b..354079d 100644 --- a/lib/config.go +++ b/lib/config.go @@ -5,8 +5,10 @@ package lib import ( + "errors" "flag" "fmt" + "io" "math/rand" "net/http" "os" @@ -21,8 +23,69 @@ import ( // ConfigCreds are the credentials for a given Notehub type ConfigCreds struct { - User string `json:"user,omitempty"` - Token string `json:"token,omitempty"` + User string `json:"user,omitempty"` + Token string `json:"token,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + Hub string `json:"-"` +} + +func (creds ConfigCreds) IsOAuthAccessToken() bool { + personalAccessTokenPrefixes := []string{"ory_st_", "api_key_"} + for _, prefix := range personalAccessTokenPrefixes { + if strings.HasPrefix(creds.Token, prefix) { + return false + } + } + return true +} + +func (creds ConfigCreds) AddHttpAuthHeader(req *http.Request) { + req.Header.Set("Authorization", "Bearer "+creds.Token) +} + +func IntrospectToken(hub string, token string) (string, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/userinfo", hub), nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + userinfo := map[string]interface{}{} + if err := note.JSONUnmarshal(body, &userinfo); err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + err := userinfo["err"] + return "", fmt.Errorf("%s (http %d)", err, resp.StatusCode) + } + + if email, ok := userinfo["email"].(string); !ok || email == "" { + fmt.Printf("response: %s\n", userinfo) + return "", fmt.Errorf("error introspecting token: no email in response") + } else { + return email, nil + } +} + +func (creds *ConfigCreds) Validate() error { + if creds == nil { + return errors.New("no credentials specified") + } + _, err := IntrospectToken(creds.Hub, creds.Token) + return err } // Port/PortConfig on a per-interface basis @@ -41,169 +104,223 @@ type ConfigSettings struct { } // Config are the master config settings -var Config ConfigSettings +var config *ConfigSettings var configFlagHub string var configFlagInterface string var configFlagPort string var configFlagPortConfig int -// ConfigRead reads the current info from config file -func ConfigRead() error { +func (config *ConfigSettings) Write() error { + // Marshal it + configJSON, err := note.JSONMarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("can't marshal configuration: %s", err) + } - // As a convenience to all tools, generate a new random seed for each iteration - rand.Seed(time.Now().UnixNano()) - rand.Seed(rand.Int63() ^ time.Now().UnixNano()) + // Write the file + configPath := configSettingsPath() + fd, err := os.OpenFile(configPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) + if err != nil { + return err + } + if _, err := fd.Write(configJSON); err != nil { + return fmt.Errorf("can't write %s: %s", configPath, err) + } + return fd.Close() +} - // Read the config file - contents, err := os.ReadFile(configSettingsPath()) - if os.IsNotExist(err) { - // If no interface has been provided and no saved config, set defaults - if Config.Interface == "" { - ConfigReset() - newConfigPort := Config.IPort[Config.Interface] - Config.Interface, newConfigPort.Port, newConfigPort.PortConfig = notecard.Defaults() - Config.IPort[Config.Interface] = newConfigPort - ConfigWrite() - } else { - ConfigReset() - } - err = nil - } else if err == nil { - err = note.JSONUnmarshal(contents, &Config) - if err != nil || Config.When == "" { - ConfigReset() - if err != nil { - err = fmt.Errorf("can't read configuration: %s", err) +func (config *ConfigSettings) Print() { + fmt.Printf("\nCurrently saved values:\n") + + if config.Hub != "" { + fmt.Printf(" hub: %s\n", config.Hub) + } + if len(config.HubCreds) != 0 { + fmt.Printf(" creds:\n") + for hub, cred := range config.HubCreds { + tokenType := "PAT" + if cred.IsOAuthAccessToken() { + tokenType = "OAuth" } + + expires := "" + if cred.ExpiresAt != nil { + if cred.ExpiresAt.Before(time.Now()) { + expires = fmt.Sprintf(" (expired)") + } else { + expires = fmt.Sprintf(" (expires at %s)", cred.ExpiresAt.Format("2006-01-02 15:04:05 MST")) + } + } + fmt.Printf(" %s: %s (%s)%s\n", hub, cred.User, tokenType, expires) } } + if config.Interface != "" { + fmt.Printf(" -interface %s\n", config.Interface) - return err - + configPort := config.IPort[config.Interface] + if configPort.Port == "" { + fmt.Printf(" -port -\n") + fmt.Printf(" -portconfig -\n") + } else { + fmt.Printf(" -port %s\n", configPort.Port) + fmt.Printf(" -portconfig %d\n", configPort.PortConfig) + } + } } -// ConfigWrite updates the file with the current config info -func ConfigWrite() error { - - // Marshal it - configJSON, _ := note.JSONMarshalIndent(Config, "", " ") +func (config *ConfigSettings) DefaultCredentials() *ConfigCreds { + if creds, present := config.HubCreds[config.Hub]; present && creds.Token != "" && creds.User != "" { + creds.Hub = config.Hub + return &creds + } + return nil +} - // Write the file - fd, err := os.OpenFile(configSettingsPath(), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) - if err != nil { - return err +// clear credentials for the currently config.Hub value +// if the credentials are from OAuth, then revoke the access token +func (config *ConfigSettings) RemoveDefaultCredentials() error { + credentials, present := config.HubCreds[config.Hub] + if !present { + return fmt.Errorf("not signed in to %s", config.Hub) } - fd.Write(configJSON) - fd.Close() - // Done - return err + if !credentials.IsOAuthAccessToken() { + notehub.RevokeAccessToken(credentials.Hub, credentials.Token) + } + // remove the credentials, and write the credentials file + delete(config.HubCreds, config.Hub) + return config.Write() } -// Reset the comms to default -func configResetInterface() { - Config = ConfigSettings{} - Config.HubCreds = map[string]ConfigCreds{} - Config.IPort = map[string]ConfigPort{} +func (config *ConfigSettings) SetDefaultCredentials(token string, email string, expiresAt *time.Time) { + config.HubCreds[config.Hub] = ConfigCreds{ + Hub: config.Hub, + User: email, + Token: token, + ExpiresAt: expiresAt, + } } -// ConfigReset updates the file with the default info -func ConfigReset() { - configResetInterface() - ConfigSetHub("-") - Config.When = time.Now().UTC().Format("2006-01-02T15:04:05Z") +func defaultConfig() *ConfigSettings { + iface, port, portConfig := notecard.Defaults() + return &ConfigSettings{ + When: time.Now().UTC().Format("2006-01-02T15:04:05Z"), + Hub: notehub.DefaultAPIService, + HubCreds: map[string]ConfigCreds{}, + IPort: map[string]ConfigPort{ + iface: { + Port: port, + PortConfig: portConfig, + }, + }, + } } -// ConfigShow displays all current config parameters -func ConfigShow() error { - - fmt.Printf("\nCurrently saved values:\n") +// returns (nil, nil) if there's no config file +// returns (non-nil, nil) if a config file was read from the filesystem successfully +// returns (nil, non-nil) if there was some any other error +func readConfigFromFile() (*ConfigSettings, error) { + // Read the config file + configPath := configSettingsPath() + contents, err := os.ReadFile(configPath) - if Config.Hub != "" { - fmt.Printf(" hub: %s\n", Config.Hub) - } - if Config.IPort == nil { - Config.IPort = map[string]ConfigPort{} - } - if Config.HubCreds == nil { - Config.HubCreds = map[string]ConfigCreds{} + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("can't read %s: %s", configPath, err) } - if len(Config.HubCreds) != 0 { - fmt.Printf(" creds:\n") - for hub, cred := range Config.HubCreds { - fmt.Printf(" %s: %s\n", hub, cred.User) - } + + var configFromFile ConfigSettings + if err := note.JSONUnmarshal(contents, &configFromFile); err != nil { + return nil, fmt.Errorf("can't parse %s: %s", configPath, err) } - if Config.Interface != "" { - fmt.Printf(" -interface %s\n", Config.Interface) - if Config.IPort[Config.Interface].Port == "" { - fmt.Printf(" -port -\n") - fmt.Printf(" -portconfig -\n") + + return &configFromFile, nil +} + +func GetConfig() (*ConfigSettings, error) { + if config == nil { + // try reading it from the filesystem + // otherwise, use a new default config + if configFromFile, err := readConfigFromFile(); err != nil { + return nil, err + } else if configFromFile != nil { + config = configFromFile } else { - fmt.Printf(" -port %s\n", Config.IPort[Config.Interface].Port) - fmt.Printf(" -portconfig %d\n", Config.IPort[Config.Interface].PortConfig) + config = defaultConfig() } } + return config, nil +} - return nil +// ConfigRead reads the current info from config file +func ConfigRead() error { -} + // As a convenience to all tools, generate a new random seed for each iteration + rand.Seed(time.Now().UnixNano()) + rand.Seed(rand.Int63() ^ time.Now().UnixNano()) -// ConfigFlagsProcess processes the registered config flags -func ConfigFlagsProcess() (err error) { + // Read the config file + configPath := configSettingsPath() + contents, err := os.ReadFile(configPath) + if os.IsNotExist(err) { + // If no interface has been provided and no saved config, + // set it to a default value and write it + config = defaultConfig() + return config.Write() + } else if err != nil { + return fmt.Errorf("can't read %s: %s", configPath, err) + } - // Create maps if they don't exist - if Config.IPort == nil { - Config.IPort = map[string]ConfigPort{} + var newConfig ConfigSettings + if err := note.JSONUnmarshal(contents, &newConfig); err != nil { + return fmt.Errorf("can't parse %s: %s", configPath, err) } - if Config.HubCreds == nil { - Config.HubCreds = map[string]ConfigCreds{} + config = &newConfig + + return nil +} + +// load current config, apply CLI flag values, and save if appropriate +func ConfigFlagsProcess() (err error) { + config, err := GetConfig() + if err != nil { + return } - // Read if not yet read - if Config.When == "" { - err = ConfigRead() - if err != nil { - return - } + if configFlagInterface == "-" { + config = defaultConfig() + } else if configFlagInterface != "" { + config.Interface = configFlagInterface } // Set or reset the flags as desired if configFlagHub != "" { ConfigSetHub(configFlagHub) } - if configFlagInterface == "-" { - configResetInterface() - } else if configFlagInterface != "" { - Config.Interface = configFlagInterface + if config.Hub == "" { + config.Hub = notehub.DefaultAPIService } + + defaultPort := config.IPort[config.Interface] + if configFlagPort == "-" { - temp := Config.IPort[Config.Interface] - temp.Port = "" - Config.IPort[Config.Interface] = temp + defaultPort.Port = "" } else if configFlagPort != "" { - temp := Config.IPort[Config.Interface] - temp.Port = configFlagPort - Config.IPort[Config.Interface] = temp + defaultPort.Port = configFlagPort } + if configFlagPortConfig < 0 { - temp := Config.IPort[Config.Interface] - temp.PortConfig = 0 - Config.IPort[Config.Interface] = temp + defaultPort.PortConfig = 0 } else if configFlagPortConfig != 0 { - temp := Config.IPort[Config.Interface] - temp.PortConfig = configFlagPortConfig - Config.IPort[Config.Interface] = temp - } - if Config.Interface == "" { - configFlagPort = "" - configFlagPortConfig = 0 + defaultPort.PortConfig = configFlagPortConfig } + config.IPort[config.Interface] = defaultPort + // Done return nil - } // ConfigFlagsRegister registers the config-related flags @@ -256,32 +373,28 @@ func FlagParse(notecardFlags bool, notehubFlags bool) (err error) { } } } - if configOnly && Config.Interface != "lease" { - fmt.Printf("*** saving configuration ***") - ConfigWrite() - ConfigShow() + + if configOnly && config.Interface != "lease" { + if err := config.Write(); err != nil { + return fmt.Errorf("could not write config file: %w", err) + } + fmt.Printf("configuration file saved\n\n") + config.Print() } // Override, just for this session, with env vars - str := os.Getenv("NOTE_INTERFACE") - if str != "" { - Config.Interface = str + if iface := os.Getenv("NOTE_INTERFACE"); iface != "" { + config.Interface = iface } // Override via env vars if specified - str = os.Getenv("NOTE_PORT") - if str != "" { - temp := Config.IPort[Config.Interface] - temp.Port = str - Config.IPort[Config.Interface] = temp - str := os.Getenv("NOTE_PORT_CONFIG") - strint, err2 := strconv.Atoi(str) - if err2 != nil { - strint = Config.IPort[Config.Interface].PortConfig + if port := os.Getenv("NOTE_PORT"); port != "" { + temp := config.IPort[config.Interface] + temp.Port = port + if portConfig, err := strconv.Atoi(os.Getenv("NOTE_PORT_CONFIG")); err == nil { + temp.PortConfig = portConfig } - temp = Config.IPort[Config.Interface] - temp.PortConfig = strint - Config.IPort[Config.Interface] = temp + config.IPort[config.Interface] = temp } // Done @@ -290,49 +403,31 @@ func FlagParse(notecardFlags bool, notehubFlags bool) (err error) { } // ConfigSignedIn returns info about whether or not we're signed in -func ConfigSignedIn() (username string, token string, authenticated bool) { - if Config.IPort == nil { - Config.IPort = map[string]ConfigPort{} - } - if Config.HubCreds == nil { - Config.HubCreds = map[string]ConfigCreds{} - } - hub := Config.Hub - if hub == "" { - hub = notehub.DefaultAPIService +// +// TODO: check credentials by issuing an HTTP request +// maybe this should return an error if a PAT is expired? +// what happens if an access token is expired? +func ConfigSignedIn() *ConfigCreds { + if creds, present := config.HubCreds[config.Hub]; present && creds.Token != "" && creds.User != "" { + creds.Hub = config.Hub + return &creds } - creds, present := Config.HubCreds[hub] - if present { - if creds.Token != "" && creds.User != "" { - authenticated = true - username = creds.User - token = creds.Token - } - } - - return - + return nil } // ConfigAuthenticationHeader sets the authorization field in the header as appropriate -func ConfigAuthenticationHeader(httpReq *http.Request) (err error) { - +func ConfigAuthenticationHeader(httpReq *http.Request) error { // Exit if not signed in - _, token, authenticated := ConfigSignedIn() - if !authenticated { - hub := Config.Hub - if hub == "" { - hub = notehub.DefaultAPIService - } - err = fmt.Errorf("not authenticated to %s: please use 'notehub -signin' to sign into the Notehub service", hub) - return + credentials := ConfigSignedIn() + if credentials == nil { + return fmt.Errorf("not authenticated to %s: please use 'notehub -signin' to sign into the Notehub service", config.Hub) } // Set the header - httpReq.Header.Set("X-Session-Token", token) + httpReq.Header.Set("Authorization", "Bearer "+credentials.Token) // Done - return + return nil } @@ -340,7 +435,7 @@ func ConfigAuthenticationHeader(httpReq *http.Request) (err error) { // the default Blues API service. Regardless, it always makes sure that the host has "api." as a prefix. // This enables flexibility in what's configured. func ConfigAPIHub() (hub string) { - hub = Config.Hub + hub = config.Hub if hub == "" || hub == "-" { hub = notehub.DefaultAPIService } @@ -353,7 +448,7 @@ func ConfigAPIHub() (hub string) { // ConfigNotecardHub returns the configured notehub, for use as the Notecard host. If none is configured // it returns "". Regardless, it always makes sure that the host does NOT have "api." as a prefix. func ConfigNotecardHub() (hub string) { - hub = Config.Hub + hub = config.Hub if hub == "" || hub == "-" { hub = notehub.DefaultAPIService } @@ -363,8 +458,8 @@ func ConfigNotecardHub() (hub string) { // ConfigSetHub clears the hub func ConfigSetHub(hub string) { - if hub == "-" { - hub = "" + if hub == "-" || hub == "" { + hub = notehub.DefaultAPIService } - Config.Hub = hub + config.Hub = hub } diff --git a/notecard/main.go b/notecard/main.go index 6083b8a..d905539 100644 --- a/notecard/main.go +++ b/notecard/main.go @@ -243,10 +243,16 @@ func main() { exitFailAndCloseCard() } + config, err := lib.GetConfig() + if err != nil { + fmt.Printf("%s\n", err) + exitFailAndCloseCard() + } + // If no action specified (i.e. just -port x), exit so that we don't touch the wrong port if len(os.Args) == 1 { lib.PrintGroupedFlags(getFlagGroups(), "notecard") - lib.ConfigShow() + config.Print() fmt.Printf("\n") fmt.Printf("PCAP Usage:\n") fmt.Printf(" notecard -port -pcap -output \n") @@ -255,9 +261,9 @@ func main() { fmt.Printf(" Example: notecard -port /dev/ttyAMA0 -pcap aux -portconfig 115200 -output capture.pcap\n") fmt.Printf("\n") nInterface, nPort, _ := notecard.Defaults() - if lib.Config.Interface != "" { - nInterface = lib.Config.Interface - nPort = lib.Config.IPort[lib.Config.Interface].Port + if config.Interface != "" { + nInterface = config.Interface + nPort = config.IPort[config.Interface].Port } var ports []string if nInterface == notecard.NotecardInterfaceSerial { @@ -270,7 +276,7 @@ func main() { fmt.Printf("Ports on '%s':\n", nInterface) for _, port := range ports { if port == nPort { - nPortConfig := lib.Config.IPort[lib.Config.Interface].PortConfig + nPortConfig := config.IPort[config.Interface].PortConfig if nPortConfig == 0 { fmt.Printf(" %s ***\n", port) } else { @@ -314,14 +320,14 @@ func main() { } // Open the card, just to make sure errors are reported early - configVal := lib.Config.IPort[lib.Config.Interface].PortConfig + configVal := config.IPort[config.Interface].PortConfig if actionPlaytime != 0 { configVal = actionPlaytime actionPlayground = true } notecard.InitialDebugMode = actionVerbose notecard.InitialTraceMode = actionTrace - card, err = notecard.Open(lib.Config.Interface, lib.Config.IPort[lib.Config.Interface].Port, configVal) + card, err = notecard.Open(config.Interface, config.IPort[config.Interface].Port, configVal) // Process non-config commands var rsp notecard.Request diff --git a/notecard/pcap.go b/notecard/pcap.go index 5c65b08..740605b 100644 --- a/notecard/pcap.go +++ b/notecard/pcap.go @@ -70,7 +70,13 @@ func pcapRecord(outputFile string, pcapType string, card *notecard.Context) erro // Use the explicit PCAP mode provided by the user pcapMode := "pcap-" + pcapType - fmt.Printf("Using PCAP mode: %s (port: %s)\n", pcapMode, lib.Config.IPort[lib.Config.Interface].Port) + config, err := lib.GetConfig() + if err != nil { + return fmt.Errorf("failed to get configuration: %w", err) + } + iport := config.IPort[config.Interface] + + fmt.Printf("Using PCAP mode: %s (port: %s)\n", pcapMode, iport.Port) // Enable PCAP mode _, err = card.TransactionRequest(notecard.Request{ @@ -88,12 +94,12 @@ func pcapRecord(outputFile string, pcapType string, card *notecard.Context) erro time.Sleep(500 * time.Millisecond) // Open the raw serial port for PCAP data streaming - fmt.Printf("Opening serial port for PCAP streaming: %s\n", lib.Config.IPort[lib.Config.Interface].Port) + fmt.Printf("Opening serial port for PCAP streaming: %s\n", iport.Port) // Configure serial port settings baudRate := 115200 // Default baud rate for PCAP - if lib.Config.IPort[lib.Config.Interface].PortConfig > 0 { - baudRate = lib.Config.IPort[lib.Config.Interface].PortConfig + if iport.PortConfig > 0 { + baudRate = iport.PortConfig } mode := &serial.Mode{ @@ -103,7 +109,7 @@ func pcapRecord(outputFile string, pcapType string, card *notecard.Context) erro StopBits: serial.OneStopBit, } - port, err := serial.Open(lib.Config.IPort[lib.Config.Interface].Port, mode) + port, err := serial.Open(iport.Port, mode) if err != nil { return fmt.Errorf("failed to open serial port: %w", err) } diff --git a/notehub/auth.go b/notehub/auth.go index afb9bf1..443e9f0 100644 --- a/notehub/auth.go +++ b/notehub/auth.go @@ -5,234 +5,81 @@ package main import ( - "bufio" - "bytes" - "encoding/json" "fmt" - "io" - "net/http" - "os" - "strings" "github.com/blues/note-cli/lib" "github.com/blues/note-go/notehub" - terminal "golang.org/x/term" ) -// Sign into the notehub account with a token -func authSignInToken(token string) (err error) { - - // Print hub if not the default - if lib.Config.Hub != "" { - fmt.Printf("notehub %s\n", lib.Config.Hub) - } - - // Extract the token and save it - var creds lib.ConfigCreds - creds.Token = token - creds.User = "(token)" - if lib.Config.HubCreds == nil { - lib.Config.HubCreds = map[string]lib.ConfigCreds{} - } - hub := lib.Config.Hub - if hub == "" { - hub = notehub.DefaultAPIService - } - lib.Config.HubCreds[hub] = creds - err = lib.ConfigWrite() +// Sign into the notehub account with a personal access token +func authSignInToken(personalAccessToken string) error { + // TODO: maybe call configInit() to set defaults? + config, err := lib.GetConfig() if err != nil { - return - } - - // Done - fmt.Printf("signed in successfully with token\n") - return - -} - -// Sign into the notehub account -func authSignIn() (err error) { - - // Sign out - _, _, authenticated := lib.ConfigSignedIn() - if authenticated { - authSignOut() + return err } - // Print banner - fmt.Printf("%s", banner()) - // Print hub if not the default - if lib.Config.Hub != "" { - fmt.Printf("notehub %s\n", lib.Config.Hub) - } - - // Read the account - var username string - for username == "" { - fmt.Printf("account email@address.com > ") - scanner := bufio.NewScanner(os.Stdin) - ok := scanner.Scan() - if ok { - username = strings.TrimRight(scanner.Text(), "\r\n") - } - } + fmt.Printf("notehub: %s\n", config.Hub) - // Read the password - var password string - for password == "" { - fmt.Printf("account password > ") - var pw []byte - pw, err = terminal.ReadPassword(int(os.Stdin.Fd())) - fmt.Printf("\n") - if err != nil { - return - } - password = string(pw) - } - - // Do the sign-in HTTP request - req := map[string]interface{}{} - req["username"] = username - req["password"] = password - reqJSON, err2 := json.Marshal(req) - if err2 != nil { - err = err2 - return - } - httpURL := "https://" + lib.ConfigAPIHub() + "/auth/login" - httpReq, err2 := http.NewRequest("POST", httpURL, bytes.NewBuffer(reqJSON)) - if err != nil { - err = err2 - return - } - httpReq.Header.Set("User-Agent", "notehub-client") - httpReq.Header.Set("Content-Type", "application/json") - httpClient := &http.Client{} - httpRsp, err2 := httpClient.Do(httpReq) - if err2 != nil { - err = err2 - return - } - if httpRsp.StatusCode == http.StatusUnauthorized || httpRsp.StatusCode == http.StatusBadRequest { - err = fmt.Errorf("unrecognized username or password") - return - } - if httpRsp.StatusCode != http.StatusOK { - err = fmt.Errorf("status %d", httpRsp.StatusCode) - return - } - rspJSON, err2 := io.ReadAll(httpRsp.Body) - if err2 != nil { - err = err2 - return - } - rsp := map[string]interface{}{} - err = json.Unmarshal(rspJSON, &rsp) + email, err := lib.IntrospectToken(config.Hub, personalAccessToken) if err != nil { - err = fmt.Errorf("%s: '%s'", err, string(rspJSON)) - return - } - token := "" - if rsp["session_token"] != nil { - token = rsp["session_token"].(string) - } - if token == "" { - err = fmt.Errorf("%s authentication error", lib.ConfigAPIHub()) - return + return err } - // Extract the token and save it - var creds lib.ConfigCreds - creds.Token = token - creds.User = username - if lib.Config.HubCreds == nil { - lib.Config.HubCreds = map[string]lib.ConfigCreds{} - } - hub := lib.Config.Hub - if hub == "" { - hub = notehub.DefaultAPIService - } - lib.Config.HubCreds[hub] = creds - err = lib.ConfigWrite() - if err != nil { - return + config.SetDefaultCredentials(personalAccessToken, email, nil) + + if err := config.Write(); err != nil { + return err } // Done - fmt.Printf("signed in successfully as %s\n", username) - return - + fmt.Printf("signed in successfully with token\n") + return nil } -// Sign out of the API -func authSignOut() (err error) { +// Sign into the Notehub account with browser-based OAuth2 flow +func authSignIn() error { - // Exit if not signed in - user, token, authenticated := lib.ConfigSignedIn() - if !authenticated { - err = fmt.Errorf("not currently signed in") - return + // load config + config, err := lib.GetConfig() + if err != nil { + return err } - // Get the token, and clear it - if lib.Config.HubCreds != nil { - hub := lib.Config.Hub - if hub == "" { - delete(lib.Config.HubCreds, "") - hub = notehub.DefaultAPIService + credentials := config.DefaultCredentials() + + // if signed in with an access token via OAuth, then revoke the access token + // we don't want to revoke a PAT because the user explicitly set an + // expiration date on that token + if credentials != nil && credentials.IsOAuthAccessToken() { + if err := config.RemoveDefaultCredentials(); err != nil { + return err } - delete(lib.Config.HubCreds, hub) - } - err = lib.ConfigWrite() - if err != nil { - return } - // Hit the logout endpoint in the API to revoke the session - httpURL := "https://" + lib.ConfigAPIHub() + "/auth/logout" - httpReq, err2 := http.NewRequest("POST", httpURL, bytes.NewBuffer([]byte{})) + // initiate the browser-based OAuth2 login flow + accessToken, err := notehub.InitiateBrowserBasedLogin(config.Hub) if err != nil { - err = err2 - return - } - httpReq.Header.Set("User-Agent", "notehub-client") - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("X-Session-Token", token) - httpClient := &http.Client{} - httpRsp, err2 := httpClient.Do(httpReq) - if err2 != nil { - err = err2 - return - } - if httpRsp.StatusCode == http.StatusUnauthorized || httpRsp.StatusCode == http.StatusBadRequest { - err = fmt.Errorf("user is not signed in") - return - } - rspJSON, err2 := io.ReadAll(httpRsp.Body) - if err2 != nil { - err = err2 - return + return fmt.Errorf("authentication failed: %w", err) } - response := string(rspJSON) - if response == "" { - fmt.Printf("%s signed out successfully\n", user) - } else { - fmt.Printf("%s signed out successfully: %s\n", user, response) + config.SetDefaultCredentials(accessToken.AccessToken, accessToken.Email, &accessToken.ExpiresAt) + + // save the config with the new credentials + if err := config.Write(); err != nil { + return err } - return -} -// Get the token for use in the API -func authToken() (user string, token string, err error) { - var authenticated bool - user, token, authenticated = lib.ConfigSignedIn() - if !authenticated { - err = fmt.Errorf("not currently signed in") - return + // print out information about the session + if accessToken != nil { + fmt.Printf("%s\n", banner()) + fmt.Printf("signed in as %s\n", accessToken.Email) + fmt.Printf("token expires at %s\n", accessToken.ExpiresAt.Format("2006-01-02 15:04:05 MST")) } - return + + // Done + return nil } // Banner for authentication diff --git a/notehub/main.go b/notehub/main.go index b950d3d..a53813d 100644 --- a/notehub/main.go +++ b/notehub/main.go @@ -166,14 +166,21 @@ func main() { os.Exit(exitFail) } + // after flags are parsed, get the resulting configuration + config, err := lib.GetConfig() + if err != nil { + fmt.Printf("%s\n", err) + os.Exit(exitFail) + } + // If no commands found, just show the config if len(os.Args) == 1 { lib.PrintGroupedFlags(getFlagGroups(), "notehub") - lib.ConfigShow() + config.Print() os.Exit(exitOk) } - // Process the sign-in request + // Process the interactive sign-in if flagSignIn { err = authSignIn() if err != nil { @@ -181,6 +188,8 @@ func main() { os.Exit(exitFail) } } + + // Process the sign-in with explicit personal access token if flagSignInToken != "" { err = authSignInToken(flagSignInToken) if err != nil { @@ -188,36 +197,40 @@ func main() { os.Exit(exitFail) } } + + // Get the current API credentials + credentials := config.DefaultCredentials() + + // Process the sign-out if flagSignOut { - err = authSignOut() - if err != nil { + if err := config.RemoveDefaultCredentials(); err != nil { fmt.Printf("%s\n", err) os.Exit(exitFail) } + os.Exit(exitOk) } - // See if we did something - didSomething := false - // Display the token if flagToken { - _, _, authenticated := lib.ConfigSignedIn() - if !authenticated { - fmt.Printf("please sign in using -signin\n") + if credentials == nil { + fmt.Printf("please sign in using -signin or -signin-token\n") os.Exit(exitFail) } - var token, username string - username, token, err = authToken() - if err != nil { - fmt.Printf("%s\n", err) - os.Exit(exitFail) - } else { - fmt.Printf("To issue HTTP API requests on behalf of %s place the token into the X-Session-Token header field\n", username) - fmt.Printf("%s\n", token) - } - didSomething = true + + fmt.Printf("%s\n", credentials.Token) + os.Exit(exitOk) } + // Past this point, we need valid credentials, so validate them here + if err := credentials.Validate(); err != nil { + fmt.Printf("invalid credentials for %s: %s\n\n", config.Hub, err) + fmt.Printf("please use 'notehub -signin' or 'notehub -signin-token' to sign into Notehub\n") + os.Exit(exitFail) + } + + // See if we did something + didSomething := false + // Create an output function that will be used during -req processing outq := make(chan string) go func() { diff --git a/notehub/trace.go b/notehub/trace.go index ffdc785..67d85bf 100644 --- a/notehub/trace.go +++ b/notehub/trace.go @@ -128,7 +128,8 @@ traceloop: if args[1] == "-" { args[1] = "" } - lib.Config.Hub = args[1] + config, _ := lib.GetConfig() + config.Hub = args[1] } fmt.Printf("hub is %s\n", flagApp)