Skip to content

Commit

Permalink
CLI Client: Refactor HTTP request handling with retry logic and impro…
Browse files Browse the repository at this point in the history
…ve token refresh mechanism
  • Loading branch information
bnema committed Sep 12, 2024
1 parent e40c941 commit 11bec3a
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 27 deletions.
18 changes: 9 additions & 9 deletions internal/cli/auth/device_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
"net/url"
"time"

"github.com/bnema/gordon/internal/common"
"github.com/bnema/gordon/internal/cli"
)

func DeviceFlowAuth(config *common.Config) error {
func DeviceFlowAuth(a *cli.App) error {
fmt.Println("Starting device flow authentication...")

// Request device code
deviceCode, err := requestDeviceCode(config)
deviceCode, err := requestDeviceCode(a)
if err != nil {
return fmt.Errorf("error requesting device code: %w", err)
}
Expand All @@ -28,20 +28,20 @@ func DeviceFlowAuth(config *common.Config) error {
interval = int(i)
}

token, err := pollForAccessToken(config, deviceCode["device_code"].(string), interval)
token, err := pollForAccessToken(a, deviceCode["device_code"].(string), interval)
if err != nil {
return fmt.Errorf("error polling for access token: %w", err)
}

// Save the token
config.SetToken(token)
a.Config.SetToken(token)

fmt.Println("Authentication successful!")
return nil
}

func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
resp, err := http.PostForm(config.GetBackendURL()+"/api/device/code", url.Values{})
func requestDeviceCode(a *cli.App) (map[string]interface{}, error) {
resp, err := http.PostForm(a.Config.GetBackendURL()+"/api/device/code", url.Values{})
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
Expand Down Expand Up @@ -72,12 +72,12 @@ func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
return deviceCode, nil
}

func pollForAccessToken(config *common.Config, deviceCode string, interval int) (string, error) {
func pollForAccessToken(a *cli.App, deviceCode string, interval int) (string, error) {
maxAttempts := 60 // Maximum number of attempts (5 minutes with 5-second intervals)
for attempt := 0; attempt < maxAttempts; attempt++ {
time.Sleep(time.Duration(interval) * time.Second)

resp, err := http.PostForm(config.GetBackendURL()+"/api/device/token", url.Values{
resp, err := http.PostForm(a.Config.GetBackendURL()+"/api/device/token", url.Values{
"device_code": {deviceCode},
})
if err != nil {
Expand Down
6 changes: 2 additions & 4 deletions internal/cli/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ func NewUpdateCommand(a *cli.App) *cobra.Command {
panic("app instance is nil")
}

var withBackup bool

command := &cobra.Command{
Use: "update",
Short: "Update the Gordon executable",
Expand All @@ -94,13 +92,13 @@ func NewUpdateCommand(a *cli.App) *cobra.Command {
}
}

return pluginInstance.update(withBackup)
return pluginInstance.update()
},
}
return command
}

func (p *plugin) update(withBackup bool) error {
func (p *plugin) update() error {
color.Yellow("Fetching release information...")

latest, err := fetchLatestRelease(
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/handler/fieldcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func FieldCheck(a *cli.App) error {

if a.Config.General.Token == "" {
fmt.Println("Token is not set. Starting OAuth device flow...")
err := auth.DeviceFlowAuth(&a.Config)
err := auth.DeviceFlowAuth(a)
if err != nil {
fmt.Printf("Device flow authentication error: %v\n", err)
return fmt.Errorf("failed to complete device flow authentication: %w", err)
Expand Down
81 changes: 69 additions & 12 deletions internal/cli/handler/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"

"github.com/bnema/gordon/internal/cli"
"github.com/bnema/gordon/internal/cli/auth"
"github.com/bnema/gordon/internal/common"
)

Expand Down Expand Up @@ -43,8 +44,51 @@ type Response struct {
// SendHTTPRequest sends the HTTP request
func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpoint string) (*Response, error) {
apiUrl := a.Config.Http.BackendURL + "/api"
token := a.Config.General.Token
client := &http.Client{}

reauthenticated := false

for {
req, err := createRequest(apiUrl, endpoint, method, rp, a.Config.General.Token)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

if resp.StatusCode != http.StatusOK {
var errorResp map[string]interface{}
if err := json.Unmarshal(body, &errorResp); err == nil {
if resp.StatusCode == http.StatusUnauthorized && !reauthenticated {
fmt.Println("Token is invalid or expired. Initiating re-authentication...")
err := ReAuthenticate(a)
if err != nil {
return nil, fmt.Errorf("re-authentication failed: %w", err)
}

reauthenticated = true
continue
}
errorMsg := fmt.Sprintf("Request failed: status=%d, error=%v", resp.StatusCode, errorResp["error"])
return nil, fmt.Errorf(errorMsg)
}
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
}

return &Response{Http: resp, Body: body, StatusCode: resp.StatusCode}, nil
}
}

func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, token string) (*http.Request, error) {
var req *http.Request
var err error

Expand All @@ -60,7 +104,6 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
return nil, fmt.Errorf("failed to create new streaming request: %w", err)
}

setAuthRequestHeaders(req, token)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Ports", deployPayload.Port)
req.Header.Set("X-Target-Domain", deployPayload.TargetDomain)
Expand All @@ -77,11 +120,10 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
return nil, fmt.Errorf("failed to create new streaming request: %w", err)
}

setAuthRequestHeaders(req, token)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Image-Name", pushPayload.ImageName)

default: // This will handle "ping" and any other types
default:
jsonPayload, err := json.Marshal(rp)
if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %w", err)
Expand All @@ -92,21 +134,36 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
return nil, fmt.Errorf("failed to create new JSON request: %w", err)
}

setAuthRequestHeaders(req, token)
req.Header.Set("Content-Type", "application/json")
}

client := &http.Client{}
resp, err := client.Do(req)
// Set the authorization header for all request types
req.Header.Set("Authorization", "Bearer "+token)

return req, nil
}

func ReAuthenticate(a *cli.App) error {
fmt.Println("Re-authenticating...")
err := auth.DeviceFlowAuth(a)
if err != nil {
return fmt.Errorf("device flow authentication failed: %w", err)
}

// After successful re-authentication, get the new token
newToken, err := a.Config.GetToken()
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
return fmt.Errorf("failed to get new token: %w", err)
}
defer resp.Body.Close()
// Update the token in the app config
a.Config.General.Token = newToken

body, err := io.ReadAll(resp.Body)
// Save the new token to config file
err = a.Config.SaveConfig()
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
return fmt.Errorf("failed to save new token to config: %w", err)
}

return &Response{Http: resp, Body: body, StatusCode: resp.StatusCode}, nil
fmt.Println("Re-authentication successful. Your session has been renewed.")
return nil
}
1 change: 0 additions & 1 deletion internal/common/initconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ func (config *Config) SaveConfig() error {
return fmt.Errorf("error writing configuration file: %w", err)
}

fmt.Printf("Configuration saved to %s\n", configFilePath)
return nil
}

Expand Down

0 comments on commit 11bec3a

Please sign in to comment.