commit 8192d90126d987dff736878d9fa0e0a369ac7bf5 Author: Billy Brawner Date: Mon Mar 7 16:14:24 2022 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3060ffd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/ntgr +*.exe diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..76d9238 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +ntgr: + go build ./cmd/ntgr + +run: + go run ./cmd/ntgr + +install: + go install ./cmd/ntgr diff --git a/cmd/ntgr/main.go b/cmd/ntgr/main.go new file mode 100644 index 0000000..f2c164f --- /dev/null +++ b/cmd/ntgr/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "flag" + "fmt" + "log" + "sort" + "strconv" + "strings" + + "git.wbrawner.com/wbrawner/ntgr/internal/netgear" + "git.wbrawner.com/wbrawner/ntgr/internal/netrc" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +func loadDevices(table *tview.Table, devices netgear.DeviceList, sortby rune) { + switch sortby { + case 'i': + sort.SliceStable(devices.ConnDevices, func(i, j int) bool { + leftIP := [4]int{} + for index, quartet := range strings.Split(devices.ConnDevices[i].IP, ".") { + leftIP[index], _ = strconv.Atoi(quartet) + } + for index, quartet := range strings.Split(devices.ConnDevices[j].IP, ".") { + rightQuartet, _ := strconv.Atoi(quartet) + if leftIP[index] < rightQuartet { + return true + } + } + + return false + }) + case 'n': + sort.SliceStable(devices.ConnDevices, func(i, j int) bool { + return strings.ToLower(devices.ConnDevices[i].Name) < strings.ToLower(devices.ConnDevices[j].Name) + }) + default: + log.Fatalf("unexpected sort argument: %v", sortby) + + } + table.SetCell(0, 0, tview.NewTableCell("Name")) + table.SetCell(0, 1, tview.NewTableCell("Model")) + table.SetCell(0, 2, tview.NewTableCell("IP Address")) + table.SetCell(0, 3, tview.NewTableCell("Mac Address")) + for index, device := range devices.ConnDevices { + row := index + 1 + table.SetCell(row, 0, tview.NewTableCell(device.Name)) + table.SetCell(row, 1, tview.NewTableCell(device.Model)) + table.SetCell(row, 2, tview.NewTableCell(device.IP)) + table.SetCell(row, 3, tview.NewTableCell(device.Mac)) + } +} + +func main() { + hostPtr := flag.String( + "host", + "192.168.1.1", + "the host or ip address for your router", + ) + flag.Parse() + credentials, err := netrc.ReadCredentials(*hostPtr) + if err != nil { + panic(err) + } + devices := netgear.ListDevices(credentials) + if len(devices.ConnDevices) == 0 { + panic(fmt.Errorf("no devices found\n")) + } + app := tview.NewApplication() + table := tview.NewTable() + table.SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEscape { + app.Stop() + } + }) + table.SetInputCapture(func(eventKey *tcell.EventKey) *tcell.EventKey { + switch eventKey.Rune() { + case 'i': + loadDevices(table, devices, 'i') + case 'n': + loadDevices(table, devices, 'n') + case 'q': + app.Stop() + default: + return eventKey + } + return nil + }) + loadDevices(table, devices, 'n') + if err := app.SetRoot(table, true).SetFocus(table).Run(); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..94b40ce --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.wbrawner.com/wbrawner/ntgr + +go 1.23.0 + +require github.com/gdamore/tcell/v2 v2.7.4 + +require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f2d4cc7 --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= +github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 h1:YIJ+B1hePP6AgynC5TcqpO0H9k3SSoZa2BGyL6vDUzM= +github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/netgear/netgear.go b/internal/netgear/netgear.go new file mode 100644 index 0000000..7be093b --- /dev/null +++ b/internal/netgear/netgear.go @@ -0,0 +1,94 @@ +package netgear + +import ( + "encoding/json" + "fmt" + "git.wbrawner.com/wbrawner/ntgr/internal/netrc" + "io" + "log" + "net/http" +) + +type DeviceList struct { + NumberOfDevices int `json:"numberOfDevices"` + Mode string `json:"mode"` + ConnDevices []struct { + CheckDev int `json:"checkDev"` + Mac string `json:"mac"` + Scene string `json:"scene"` + Mark string `json:"mark"` + Connection string `json:"connection"` + Priority int `json:"priority"` + PriorityStr string `json:"priorityStr"` + CurBarDownStr string `json:"curBarDownStr"` + CurBarUpStr string `json:"curBarUpStr"` + DownloadSpeedStr string `json:"downloadSpeedStr"` + UploadSpeedStr string `json:"uploadSpeedStr"` + Type string `json:"type"` + Model string `json:"model"` + Name string `json:"name"` + IP string `json:"ip"` + } `json:"connDevices"` +} + +// login will attempt to authenticate with the router to perform a subsequent +// request. That request can be executed in the onSuccess function +func login[T interface{}](credentials netrc.NetrcCredential, onSuccess func(netrc.NetrcCredential) T) T { + req, err := http.NewRequest("GET", credentials.HttpHost(), nil) + if err != nil { + log.Fatalln("Failed to create request") + } + req.Header.Set( + "Authorization", + fmt.Sprintf("Basic %s", credentials.Base64()), + ) + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalln("Failed to send first login request") + } + if res.StatusCode == 200 { + return onSuccess(credentials) + } + // if the first login attempt fails, it'll return an XSRF_TOKEN cookie + // that we'll need to set for the second request. it's only needed for + // the login request to succeed, and subsequent requests won't need it + // anymore + req.Header.Set( + "Cookie", + res.Header.Get("Set-Cookie"), + ) + res, err = http.DefaultClient.Do(req) + if err != nil || res.StatusCode != 200 { + log.Fatalln("Failed to send second login request") + } + return onSuccess(credentials) +} + +func ListDevices(credentials netrc.NetrcCredential) DeviceList { + log.Println("loading device list...") + req, err := http.NewRequest("GET", credentials.HttpHostWithPath("/ajax/devices_table_result"), nil) + if err != nil { + log.Fatalln("Failed to create request") + } + req.Header.Set( + "Authorization", + fmt.Sprintf("Basic %s", credentials.Base64()), + ) + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("failed to list devices: %v\n", err) + } + if res.StatusCode != 200 { + // if loading the device list fails, then we need to log in + // first + log.Println("listing devices failed, attempting login") + return login(credentials, ListDevices) + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + var devices DeviceList + if err = json.Unmarshal(body, &devices); err != nil { + log.Fatalln("Failed to parse JSON response from router") + } + return devices +} diff --git a/internal/netrc/netrc.go b/internal/netrc/netrc.go new file mode 100644 index 0000000..5aa7a0f --- /dev/null +++ b/internal/netrc/netrc.go @@ -0,0 +1,62 @@ +package netrc + +import ( + "bufio" + "encoding/base64" + "fmt" + "os" + "path" + "strings" +) + +type NetrcCredential struct { + Machine string + Login string + Password string +} + +func (credential *NetrcCredential) HttpHost() string { + return credential.HttpHostWithPath("") +} + +func (credential *NetrcCredential) HttpHostWithPath(path string) string { + return fmt.Sprintf("http://%s%s", credential.Machine, path) +} + +func (credential *NetrcCredential) Base64() string { + formattedCredentials := fmt.Sprintf("%s:%s", credential.Login, credential.Password) + credentialBytes := []byte(formattedCredentials) + return base64.StdEncoding.EncodeToString(credentialBytes) +} + +func ReadCredentials(host string) (NetrcCredential, error) { + credentials := NetrcCredential{Machine: host} + netrcPath := path.Join(os.Getenv("HOME"), ".netrc") + netrcFile, err := os.Open(netrcPath) + if err != nil { + return credentials, fmt.Errorf("failed to read ~/.netrc: %v", err) + } + defer netrcFile.Close() + scanner := bufio.NewScanner(netrcFile) + extractCredentials := false + for { + if !scanner.Scan() { + break + } + line := scanner.Text() + if strings.HasPrefix(line, "machine ") { + extractCredentials = strings.HasSuffix(line, credentials.Machine) + } else if strings.HasPrefix(line, "login ") && extractCredentials { + credentials.Login = strings.TrimPrefix(line, "login ") + } else if strings.HasPrefix(line, "password ") && extractCredentials { + credentials.Password = strings.TrimPrefix(line, "password ") + } + } + if credentials.Login == "" { + return credentials, fmt.Errorf("error: no login found for host \"%s\" in ~/.netrc", credentials.Machine) + } + if credentials.Password == "" { + return credentials, fmt.Errorf("error: no password found for host \"%s\" in ~/.netrc", credentials.Machine) + } + return credentials, scanner.Err() +}