A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
73
fork

Configure Feed

Select the types of activity you want to include in your feed.

at label-service 173 lines 4.9 kB view raw
1package main 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "time" 11) 12 13// Device authorization API types 14 15type DeviceCodeRequest struct { 16 DeviceName string `json:"device_name"` 17} 18 19type DeviceCodeResponse struct { 20 DeviceCode string `json:"device_code"` 21 UserCode string `json:"user_code"` 22 VerificationURI string `json:"verification_uri"` 23 ExpiresIn int `json:"expires_in"` 24 Interval int `json:"interval"` 25} 26 27type DeviceTokenRequest struct { 28 DeviceCode string `json:"device_code"` 29} 30 31type DeviceTokenResponse struct { 32 DeviceSecret string `json:"device_secret,omitempty"` 33 Handle string `json:"handle,omitempty"` 34 DID string `json:"did,omitempty"` 35 Error string `json:"error,omitempty"` 36} 37 38// AuthErrorResponse is the JSON error response from /auth/token 39type AuthErrorResponse struct { 40 Error string `json:"error"` 41 Message string `json:"message"` 42 LoginURL string `json:"login_url,omitempty"` 43} 44 45// ValidationResult represents the result of credential validation 46type ValidationResult struct { 47 Valid bool 48 OAuthSessionExpired bool 49 LoginURL string 50} 51 52// requestDeviceCode requests a device code from the AppView. 53// Returns the code response and resolved AppView URL. 54// Does not print anything — the caller controls UX. 55func requestDeviceCode(serverURL string) (*DeviceCodeResponse, string, error) { 56 appViewURL := buildAppViewURL(serverURL) 57 deviceName := hostname() 58 59 reqBody, _ := json.Marshal(DeviceCodeRequest{DeviceName: deviceName}) 60 resp, err := http.Post(appViewURL+"/auth/device/code", "application/json", bytes.NewReader(reqBody)) 61 if err != nil { 62 return nil, appViewURL, fmt.Errorf("failed to request device code: %w", err) 63 } 64 defer resp.Body.Close() 65 66 if resp.StatusCode != http.StatusOK { 67 body, _ := io.ReadAll(resp.Body) 68 return nil, appViewURL, fmt.Errorf("device code request failed: %s", string(body)) 69 } 70 71 var codeResp DeviceCodeResponse 72 if err := json.NewDecoder(resp.Body).Decode(&codeResp); err != nil { 73 return nil, appViewURL, fmt.Errorf("failed to decode device code response: %w", err) 74 } 75 76 return &codeResp, appViewURL, nil 77} 78 79// pollDeviceToken polls the token endpoint until authorization completes. 80// Does not print anything — the caller controls UX. 81// Returns the account on success, or an error on timeout/failure. 82func pollDeviceToken(appViewURL string, codeResp *DeviceCodeResponse) (*Account, error) { 83 pollInterval := time.Duration(codeResp.Interval) * time.Second 84 timeout := time.Duration(codeResp.ExpiresIn) * time.Second 85 deadline := time.Now().Add(timeout) 86 87 for time.Now().Before(deadline) { 88 time.Sleep(pollInterval) 89 90 tokenReqBody, _ := json.Marshal(DeviceTokenRequest{DeviceCode: codeResp.DeviceCode}) 91 tokenResp, err := http.Post(appViewURL+"/auth/device/token", "application/json", bytes.NewReader(tokenReqBody)) 92 if err != nil { 93 continue 94 } 95 96 var tokenResult DeviceTokenResponse 97 if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResult); err != nil { 98 tokenResp.Body.Close() 99 continue 100 } 101 tokenResp.Body.Close() 102 103 if tokenResult.Error == "authorization_pending" { 104 continue 105 } 106 107 if tokenResult.Error != "" { 108 return nil, fmt.Errorf("authorization failed: %s", tokenResult.Error) 109 } 110 111 return &Account{ 112 Handle: tokenResult.Handle, 113 DID: tokenResult.DID, 114 DeviceSecret: tokenResult.DeviceSecret, 115 }, nil 116 } 117 118 return nil, fmt.Errorf("authorization timed out") 119} 120 121// validateCredentials checks if the credentials are still valid by making a test request 122func validateCredentials(appViewURL, handle, deviceSecret string) ValidationResult { 123 client := &http.Client{ 124 Timeout: 5 * time.Second, 125 } 126 127 tokenURL := appViewURL + "/auth/token?service=" + appViewURL 128 129 req, err := http.NewRequest("GET", tokenURL, nil) 130 if err != nil { 131 return ValidationResult{Valid: false} 132 } 133 134 req.SetBasicAuth(handle, deviceSecret) 135 136 resp, err := client.Do(req) 137 if err != nil { 138 // Network error — assume credentials are valid but server unreachable 139 return ValidationResult{Valid: true} 140 } 141 defer resp.Body.Close() 142 143 if resp.StatusCode == http.StatusOK { 144 return ValidationResult{Valid: true} 145 } 146 147 if resp.StatusCode == http.StatusUnauthorized { 148 body, err := io.ReadAll(resp.Body) 149 if err == nil { 150 var authErr AuthErrorResponse 151 if json.Unmarshal(body, &authErr) == nil && authErr.Error == "oauth_session_expired" { 152 return ValidationResult{ 153 Valid: false, 154 OAuthSessionExpired: true, 155 LoginURL: authErr.LoginURL, 156 } 157 } 158 } 159 return ValidationResult{Valid: false} 160 } 161 162 // Any other error = assume valid (don't re-auth on server issues) 163 return ValidationResult{Valid: true} 164} 165 166// hostname returns the machine hostname, or a fallback. 167func hostname() string { 168 name, err := os.Hostname() 169 if err != nil { 170 return "Unknown Device" 171 } 172 return name 173}