detached acce586

CLI Client: Refactor HTTP request handling with retry logic and improve token refresh mechanism

commit
acce5868d6a8918d6c18518438ebff6999946f77
author
bamen · · 2024-09-12 09:44 UTC
parent
a0bdde0
signature
unsigned
5 files · +81 -27
M internal/cli/auth/device_flow.go +9 -9
@@ -8,14 +8,14 @@ import (
88 "net/url"
99 "time"
1010
11- "github.com/bnema/gordon/internal/common"
11+ "github.com/bnema/gordon/internal/cli"
1212 )
1313
14-func DeviceFlowAuth(config *common.Config) error {
14+func DeviceFlowAuth(a *cli.App) error {
1515 fmt.Println("Starting device flow authentication...")
1616
1717 // Request device code
18- deviceCode, err := requestDeviceCode(config)
18+ deviceCode, err := requestDeviceCode(a)
1919 if err != nil {
2020 return fmt.Errorf("error requesting device code: %w", err)
2121 }
@@ -28,20 +28,20 @@ func DeviceFlowAuth(config *common.Config) error {
2828 interval = int(i)
2929 }
3030
31- token, err := pollForAccessToken(config, deviceCode["device_code"].(string), interval)
31+ token, err := pollForAccessToken(a, deviceCode["device_code"].(string), interval)
3232 if err != nil {
3333 return fmt.Errorf("error polling for access token: %w", err)
3434 }
3535
3636 // Save the token
37- config.SetToken(token)
37+ a.Config.SetToken(token)
3838
3939 fmt.Println("Authentication successful!")
4040 return nil
4141 }
4242
43-func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
44- resp, err := http.PostForm(config.GetBackendURL()+"/api/device/code", url.Values{})
43+func requestDeviceCode(a *cli.App) (map[string]interface{}, error) {
44+ resp, err := http.PostForm(a.Config.GetBackendURL()+"/api/device/code", url.Values{})
4545 if err != nil {
4646 return nil, fmt.Errorf("error making request: %w", err)
4747 }
@@ -72,12 +72,12 @@ func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
7272 return deviceCode, nil
7373 }
7474
75-func pollForAccessToken(config *common.Config, deviceCode string, interval int) (string, error) {
75+func pollForAccessToken(a *cli.App, deviceCode string, interval int) (string, error) {
7676 maxAttempts := 60 // Maximum number of attempts (5 minutes with 5-second intervals)
7777 for attempt := 0; attempt < maxAttempts; attempt++ {
7878 time.Sleep(time.Duration(interval) * time.Second)
7979
80- resp, err := http.PostForm(config.GetBackendURL()+"/api/device/token", url.Values{
80+ resp, err := http.PostForm(a.Config.GetBackendURL()+"/api/device/token", url.Values{
8181 "device_code": {deviceCode},
8282 })
8383 if err != nil {
@@ -8,14 +8,14 @@ import (
8 "net/url"8 "net/url"
9 "time"9 "time"
10 10
11- "github.com/bnema/gordon/internal/common"11+ "github.com/bnema/gordon/internal/cli"
12 )12 )
13 13
14-func DeviceFlowAuth(config *common.Config) error {14+func DeviceFlowAuth(a *cli.App) error {
15 fmt.Println("Starting device flow authentication...")15 fmt.Println("Starting device flow authentication...")
16 16
17 // Request device code17 // Request device code
18- deviceCode, err := requestDeviceCode(config)18+ deviceCode, err := requestDeviceCode(a)
19 if err != nil {19 if err != nil {
20 return fmt.Errorf("error requesting device code: %w", err)20 return fmt.Errorf("error requesting device code: %w", err)
21 }21 }
@@ -28,20 +28,20 @@ func DeviceFlowAuth(config *common.Config) error {
28 interval = int(i)28 interval = int(i)
29 }29 }
30 30
31- token, err := pollForAccessToken(config, deviceCode["device_code"].(string), interval)31+ token, err := pollForAccessToken(a, deviceCode["device_code"].(string), interval)
32 if err != nil {32 if err != nil {
33 return fmt.Errorf("error polling for access token: %w", err)33 return fmt.Errorf("error polling for access token: %w", err)
34 }34 }
35 35
36 // Save the token36 // Save the token
37- config.SetToken(token)37+ a.Config.SetToken(token)
38 38
39 fmt.Println("Authentication successful!")39 fmt.Println("Authentication successful!")
40 return nil40 return nil
41 }41 }
42 42
43-func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {43+func requestDeviceCode(a *cli.App) (map[string]interface{}, error) {
44- resp, err := http.PostForm(config.GetBackendURL()+"/api/device/code", url.Values{})44+ resp, err := http.PostForm(a.Config.GetBackendURL()+"/api/device/code", url.Values{})
45 if err != nil {45 if err != nil {
46 return nil, fmt.Errorf("error making request: %w", err)46 return nil, fmt.Errorf("error making request: %w", err)
47 }47 }
@@ -72,12 +72,12 @@ func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
72 return deviceCode, nil72 return deviceCode, nil
73 }73 }
74 74
75-func pollForAccessToken(config *common.Config, deviceCode string, interval int) (string, error) {75+func pollForAccessToken(a *cli.App, deviceCode string, interval int) (string, error) {
76 maxAttempts := 60 // Maximum number of attempts (5 minutes with 5-second intervals)76 maxAttempts := 60 // Maximum number of attempts (5 minutes with 5-second intervals)
77 for attempt := 0; attempt < maxAttempts; attempt++ {77 for attempt := 0; attempt < maxAttempts; attempt++ {
78 time.Sleep(time.Duration(interval) * time.Second)78 time.Sleep(time.Duration(interval) * time.Second)
79 79
80- resp, err := http.PostForm(config.GetBackendURL()+"/api/device/token", url.Values{80+ resp, err := http.PostForm(a.Config.GetBackendURL()+"/api/device/token", url.Values{
81 "device_code": {deviceCode},81 "device_code": {deviceCode},
82 })82 })
83 if err != nil {83 if err != nil {
M internal/cli/cmd/update.go +2 -4
@@ -67,8 +67,6 @@ func NewUpdateCommand(a *cli.App) *cobra.Command {
6767 panic("app instance is nil")
6868 }
6969
70- var withBackup bool
71-
7270 command := &cobra.Command{
7371 Use: "update",
7472 Short: "Update the Gordon executable",
@@ -94,13 +92,13 @@ func NewUpdateCommand(a *cli.App) *cobra.Command {
9492 }
9593 }
9694
97- return pluginInstance.update(withBackup)
95+ return pluginInstance.update()
9896 },
9997 }
10098 return command
10199 }
102100
103-func (p *plugin) update(withBackup bool) error {
101+func (p *plugin) update() error {
104102 color.Yellow("Fetching release information...")
105103
106104 latest, err := fetchLatestRelease(
@@ -67,8 +67,6 @@ func NewUpdateCommand(a *cli.App) *cobra.Command {
67 panic("app instance is nil")67 panic("app instance is nil")
68 }68 }
69 69
70- var withBackup bool
71-
72 command := &cobra.Command{70 command := &cobra.Command{
73 Use: "update",71 Use: "update",
74 Short: "Update the Gordon executable",72 Short: "Update the Gordon executable",
@@ -94,13 +92,13 @@ func NewUpdateCommand(a *cli.App) *cobra.Command {
94 }92 }
95 }93 }
96 94
97- return pluginInstance.update(withBackup)95+ return pluginInstance.update()
98 },96 },
99 }97 }
100 return command98 return command
101 }99 }
102 100
103-func (p *plugin) update(withBackup bool) error {101+func (p *plugin) update() error {
104 color.Yellow("Fetching release information...")102 color.Yellow("Fetching release information...")
105 103
106 latest, err := fetchLatestRelease(104 latest, err := fetchLatestRelease(
M internal/cli/handler/fieldcheck.go +1 -1
@@ -34,7 +34,7 @@ func FieldCheck(a *cli.App) error {
3434
3535 if a.Config.General.Token == "" {
3636 fmt.Println("Token is not set. Starting OAuth device flow...")
37- err := auth.DeviceFlowAuth(&a.Config)
37+ err := auth.DeviceFlowAuth(a)
3838 if err != nil {
3939 fmt.Printf("Device flow authentication error: %v\n", err)
4040 return fmt.Errorf("failed to complete device flow authentication: %w", err)
@@ -34,7 +34,7 @@ func FieldCheck(a *cli.App) error {
34 34
35 if a.Config.General.Token == "" {35 if a.Config.General.Token == "" {
36 fmt.Println("Token is not set. Starting OAuth device flow...")36 fmt.Println("Token is not set. Starting OAuth device flow...")
37- err := auth.DeviceFlowAuth(&a.Config)37+ err := auth.DeviceFlowAuth(a)
38 if err != nil {38 if err != nil {
39 fmt.Printf("Device flow authentication error: %v\n", err)39 fmt.Printf("Device flow authentication error: %v\n", err)
40 return fmt.Errorf("failed to complete device flow authentication: %w", err)40 return fmt.Errorf("failed to complete device flow authentication: %w", err)
M internal/cli/handler/http.go +69 -12
@@ -8,6 +8,7 @@ import (
88 "net/http"
99
1010 "github.com/bnema/gordon/internal/cli"
11+ "github.com/bnema/gordon/internal/cli/auth"
1112 "github.com/bnema/gordon/internal/common"
1213 )
1314
@@ -43,8 +44,51 @@ type Response struct {
4344 // SendHTTPRequest sends the HTTP request
4445 func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpoint string) (*Response, error) {
4546 apiUrl := a.Config.Http.BackendURL + "/api"
46- token := a.Config.General.Token
47+ client := &http.Client{}
48+
49+ reauthenticated := false
50+
51+ for {
52+ req, err := createRequest(apiUrl, endpoint, method, rp, a.Config.General.Token)
53+ if err != nil {
54+ return nil, fmt.Errorf("failed to create request: %w", err)
55+ }
56+
57+ resp, err := client.Do(req)
58+ if err != nil {
59+ return nil, fmt.Errorf("failed to send request: %w", err)
60+ }
61+ defer resp.Body.Close()
62+
63+ body, err := io.ReadAll(resp.Body)
64+ if err != nil {
65+ return nil, fmt.Errorf("failed to read response body: %w", err)
66+ }
67+
68+ if resp.StatusCode != http.StatusOK {
69+ var errorResp map[string]interface{}
70+ if err := json.Unmarshal(body, &errorResp); err == nil {
71+ if resp.StatusCode == http.StatusUnauthorized && !reauthenticated {
72+ fmt.Println("Token is invalid or expired. Initiating re-authentication...")
73+ err := ReAuthenticate(a)
74+ if err != nil {
75+ return nil, fmt.Errorf("re-authentication failed: %w", err)
76+ }
77+
78+ reauthenticated = true
79+ continue
80+ }
81+ errorMsg := fmt.Sprintf("Request failed: status=%d, error=%v", resp.StatusCode, errorResp["error"])
82+ return nil, fmt.Errorf(errorMsg)
83+ }
84+ return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
85+ }
86+
87+ return &Response{Http: resp, Body: body, StatusCode: resp.StatusCode}, nil
88+ }
89+}
4790
91+func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, token string) (*http.Request, error) {
4892 var req *http.Request
4993 var err error
5094
@@ -60,7 +104,6 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
60104 return nil, fmt.Errorf("failed to create new streaming request: %w", err)
61105 }
62106
63- setAuthRequestHeaders(req, token)
64107 req.Header.Set("Content-Type", "application/octet-stream")
65108 req.Header.Set("X-Ports", deployPayload.Port)
66109 req.Header.Set("X-Target-Domain", deployPayload.TargetDomain)
@@ -77,11 +120,10 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
77120 return nil, fmt.Errorf("failed to create new streaming request: %w", err)
78121 }
79122
80- setAuthRequestHeaders(req, token)
81123 req.Header.Set("Content-Type", "application/octet-stream")
82124 req.Header.Set("X-Image-Name", pushPayload.ImageName)
83125
84- default: // This will handle "ping" and any other types
126+ default:
85127 jsonPayload, err := json.Marshal(rp)
86128 if err != nil {
87129 return nil, fmt.Errorf("failed to marshal payload: %w", err)
@@ -92,21 +134,36 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
92134 return nil, fmt.Errorf("failed to create new JSON request: %w", err)
93135 }
94136
95- setAuthRequestHeaders(req, token)
96137 req.Header.Set("Content-Type", "application/json")
97138 }
98139
99- client := &http.Client{}
100- resp, err := client.Do(req)
140+ // Set the authorization header for all request types
141+ req.Header.Set("Authorization", "Bearer "+token)
142+
143+ return req, nil
144+}
145+
146+func ReAuthenticate(a *cli.App) error {
147+ fmt.Println("Re-authenticating...")
148+ err := auth.DeviceFlowAuth(a)
149+ if err != nil {
150+ return fmt.Errorf("device flow authentication failed: %w", err)
151+ }
152+
153+ // After successful re-authentication, get the new token
154+ newToken, err := a.Config.GetToken()
101155 if err != nil {
102- return nil, fmt.Errorf("failed to send request: %w", err)
156+ return fmt.Errorf("failed to get new token: %w", err)
103157 }
104- defer resp.Body.Close()
158+ // Update the token in the app config
159+ a.Config.General.Token = newToken
105160
106- body, err := io.ReadAll(resp.Body)
161+ // Save the new token to config file
162+ err = a.Config.SaveConfig()
107163 if err != nil {
108- return nil, fmt.Errorf("failed to read response body: %w", err)
164+ return fmt.Errorf("failed to save new token to config: %w", err)
109165 }
110166
111- return &Response{Http: resp, Body: body, StatusCode: resp.StatusCode}, nil
167+ fmt.Println("Re-authentication successful. Your session has been renewed.")
168+ return nil
112169 }
@@ -8,6 +8,7 @@ import (
8 "net/http"8 "net/http"
9 9
10 "github.com/bnema/gordon/internal/cli"10 "github.com/bnema/gordon/internal/cli"
11+ "github.com/bnema/gordon/internal/cli/auth"
11 "github.com/bnema/gordon/internal/common"12 "github.com/bnema/gordon/internal/common"
12 )13 )
13 14
@@ -43,8 +44,51 @@ type Response struct {
43 // SendHTTPRequest sends the HTTP request44 // SendHTTPRequest sends the HTTP request
44 func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpoint string) (*Response, error) {45 func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpoint string) (*Response, error) {
45 apiUrl := a.Config.Http.BackendURL + "/api"46 apiUrl := a.Config.Http.BackendURL + "/api"
46- token := a.Config.General.Token47+ client := &http.Client{}
48+
49+ reauthenticated := false
50+
51+ for {
52+ req, err := createRequest(apiUrl, endpoint, method, rp, a.Config.General.Token)
53+ if err != nil {
54+ return nil, fmt.Errorf("failed to create request: %w", err)
55+ }
56+
57+ resp, err := client.Do(req)
58+ if err != nil {
59+ return nil, fmt.Errorf("failed to send request: %w", err)
60+ }
61+ defer resp.Body.Close()
62+
63+ body, err := io.ReadAll(resp.Body)
64+ if err != nil {
65+ return nil, fmt.Errorf("failed to read response body: %w", err)
66+ }
67+
68+ if resp.StatusCode != http.StatusOK {
69+ var errorResp map[string]interface{}
70+ if err := json.Unmarshal(body, &errorResp); err == nil {
71+ if resp.StatusCode == http.StatusUnauthorized && !reauthenticated {
72+ fmt.Println("Token is invalid or expired. Initiating re-authentication...")
73+ err := ReAuthenticate(a)
74+ if err != nil {
75+ return nil, fmt.Errorf("re-authentication failed: %w", err)
76+ }
77+
78+ reauthenticated = true
79+ continue
80+ }
81+ errorMsg := fmt.Sprintf("Request failed: status=%d, error=%v", resp.StatusCode, errorResp["error"])
82+ return nil, fmt.Errorf(errorMsg)
83+ }
84+ return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
85+ }
86+
87+ return &Response{Http: resp, Body: body, StatusCode: resp.StatusCode}, nil
88+ }
89+}
47 90
91+func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, token string) (*http.Request, error) {
48 var req *http.Request92 var req *http.Request
49 var err error93 var err error
50 94
@@ -60,7 +104,6 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
60 return nil, fmt.Errorf("failed to create new streaming request: %w", err)104 return nil, fmt.Errorf("failed to create new streaming request: %w", err)
61 }105 }
62 106
63- setAuthRequestHeaders(req, token)
64 req.Header.Set("Content-Type", "application/octet-stream")107 req.Header.Set("Content-Type", "application/octet-stream")
65 req.Header.Set("X-Ports", deployPayload.Port)108 req.Header.Set("X-Ports", deployPayload.Port)
66 req.Header.Set("X-Target-Domain", deployPayload.TargetDomain)109 req.Header.Set("X-Target-Domain", deployPayload.TargetDomain)
@@ -77,11 +120,10 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
77 return nil, fmt.Errorf("failed to create new streaming request: %w", err)120 return nil, fmt.Errorf("failed to create new streaming request: %w", err)
78 }121 }
79 122
80- setAuthRequestHeaders(req, token)
81 req.Header.Set("Content-Type", "application/octet-stream")123 req.Header.Set("Content-Type", "application/octet-stream")
82 req.Header.Set("X-Image-Name", pushPayload.ImageName)124 req.Header.Set("X-Image-Name", pushPayload.ImageName)
83 125
84- default: // This will handle "ping" and any other types126+ default:
85 jsonPayload, err := json.Marshal(rp)127 jsonPayload, err := json.Marshal(rp)
86 if err != nil {128 if err != nil {
87 return nil, fmt.Errorf("failed to marshal payload: %w", err)129 return nil, fmt.Errorf("failed to marshal payload: %w", err)
@@ -92,21 +134,36 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
92 return nil, fmt.Errorf("failed to create new JSON request: %w", err)134 return nil, fmt.Errorf("failed to create new JSON request: %w", err)
93 }135 }
94 136
95- setAuthRequestHeaders(req, token)
96 req.Header.Set("Content-Type", "application/json")137 req.Header.Set("Content-Type", "application/json")
97 }138 }
98 139
99- client := &http.Client{}140+ // Set the authorization header for all request types
100- resp, err := client.Do(req)141+ req.Header.Set("Authorization", "Bearer "+token)
142+
143+ return req, nil
144+}
145+
146+func ReAuthenticate(a *cli.App) error {
147+ fmt.Println("Re-authenticating...")
148+ err := auth.DeviceFlowAuth(a)
149+ if err != nil {
150+ return fmt.Errorf("device flow authentication failed: %w", err)
151+ }
152+
153+ // After successful re-authentication, get the new token
154+ newToken, err := a.Config.GetToken()
101 if err != nil {155 if err != nil {
102- return nil, fmt.Errorf("failed to send request: %w", err)156+ return fmt.Errorf("failed to get new token: %w", err)
103 }157 }
104- defer resp.Body.Close()158+ // Update the token in the app config
159+ a.Config.General.Token = newToken
105 160
106- body, err := io.ReadAll(resp.Body)161+ // Save the new token to config file
162+ err = a.Config.SaveConfig()
107 if err != nil {163 if err != nil {
108- return nil, fmt.Errorf("failed to read response body: %w", err)164+ return fmt.Errorf("failed to save new token to config: %w", err)
109 }165 }
110 166
111- return &Response{Http: resp, Body: body, StatusCode: resp.StatusCode}, nil167+ fmt.Println("Re-authentication successful. Your session has been renewed.")
168+ return nil
112 }169 }
M internal/common/initconfig.go +0 -1
@@ -166,7 +166,6 @@ func (config *Config) SaveConfig() error {
166166 return fmt.Errorf("error writing configuration file: %w", err)
167167 }
168168
169- fmt.Printf("Configuration saved to %s\n", configFilePath)
170169 return nil
171170 }
172171
@@ -166,7 +166,6 @@ func (config *Config) SaveConfig() error {
166 return fmt.Errorf("error writing configuration file: %w", err)166 return fmt.Errorf("error writing configuration file: %w", err)
167 }167 }
168 168
169- fmt.Printf("Configuration saved to %s\n", configFilePath)
170 return nil169 return nil
171 }170 }
172 171

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close