A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

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

fix credential helper to read insecure-registries. fix device registration flow

+165 -42
+137 -21
cmd/credential-helper/main.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 "io" 8 + "net" 8 9 "net/http" 9 10 "os" 10 11 "os/exec" 11 12 "path/filepath" 12 13 "runtime" 14 + "strings" 13 15 "time" 14 16 ) 15 17 16 - const ( 17 - // Default AppView URL - can be overridden via environment variable 18 - defaultAppViewURL = "http://127.0.0.1:5000" 19 - ) 20 - 21 18 // DeviceConfig represents the stored device configuration 22 19 type DeviceConfig struct { 23 20 Handle string `json:"handle"` ··· 25 22 AppViewURL string `json:"appview_url"` 26 23 } 27 24 25 + // DockerDaemonConfig represents Docker's daemon.json configuration 26 + type DockerDaemonConfig struct { 27 + InsecureRegistries []string `json:"insecure-registries"` 28 + } 29 + 28 30 // Docker credential helper protocol 29 31 // https://github.com/docker/docker-credential-helpers 30 32 ··· 97 99 // First time - trigger device authorization 98 100 fmt.Fprintf(os.Stderr, "No device configuration found. Starting device authorization...\n") 99 101 100 - deviceConfig, err = authorizeDevice() 102 + deviceConfig, err = authorizeDevice(serverURL) 101 103 if err != nil { 102 104 fmt.Fprintf(os.Stderr, "Device authorization failed: %v\n", err) 103 - fmt.Fprintf(os.Stderr, "\nFallback: Use 'docker login atcr.io' with your ATProto app-password\n") 105 + fmt.Fprintf(os.Stderr, "\nFallback: Use 'docker login %s' with your ATProto app-password\n", serverURL) 104 106 os.Exit(1) 105 107 } 106 108 ··· 157 159 } 158 160 159 161 // authorizeDevice performs the device authorization flow 160 - func authorizeDevice() (*DeviceConfig, error) { 161 - // Get AppView URL 162 - appViewURL := os.Getenv("ATCR_APPVIEW_URL") 163 - if appViewURL == "" { 164 - appViewURL = defaultAppViewURL 165 - } 162 + func authorizeDevice(serverURL string) (*DeviceConfig, error) { 163 + appViewURL := buildAppViewURL(serverURL) 166 164 167 165 // Get device name (hostname) 168 166 deviceName, err := os.Hostname() ··· 190 188 return nil, fmt.Errorf("failed to decode device code response: %w", err) 191 189 } 192 190 193 - // 2. Open browser for user to approve 191 + // 2. Display authorization URL and user code 194 192 verificationURL := codeResp.VerificationURI + "?user_code=" + codeResp.UserCode 195 193 196 - fmt.Fprintf(os.Stderr, "\nOpening browser for device authorization...\n") 197 - fmt.Fprintf(os.Stderr, "User code: %s\n", codeResp.UserCode) 198 - fmt.Fprintf(os.Stderr, "\nIf browser doesn't open, visit: %s\n\n", verificationURL) 194 + fmt.Fprintf(os.Stderr, "\n╔════════════════════════════════════════════════════════════════╗\n") 195 + fmt.Fprintf(os.Stderr, "║ Device Authorization Required ║\n") 196 + fmt.Fprintf(os.Stderr, "╚════════════════════════════════════════════════════════════════╝\n\n") 197 + fmt.Fprintf(os.Stderr, "Visit this URL in your browser:\n") 198 + fmt.Fprintf(os.Stderr, " %s\n\n", verificationURL) 199 + fmt.Fprintf(os.Stderr, "Your code: %s\n\n", codeResp.UserCode) 199 200 200 - if err := openBrowser(verificationURL); err != nil { 201 - fmt.Fprintf(os.Stderr, "Could not open browser: %v\n", err) 201 + // Try to open browser (may fail on headless systems) 202 + if err := openBrowser(verificationURL); err == nil { 203 + fmt.Fprintf(os.Stderr, "Opening browser...\n\n") 204 + } else { 205 + fmt.Fprintf(os.Stderr, "Could not open browser automatically (%v)\n", err) 206 + fmt.Fprintf(os.Stderr, "Please open the URL above manually.\n\n") 202 207 } 203 208 204 - fmt.Fprintf(os.Stderr, "Waiting for authorization...\n") 209 + fmt.Fprintf(os.Stderr, "Waiting for authorization") 205 210 206 211 // 3. Poll for authorization completion 207 212 pollInterval := time.Duration(codeResp.Interval) * time.Second 208 213 timeout := time.Duration(codeResp.ExpiresIn) * time.Second 209 214 deadline := time.Now().Add(timeout) 210 215 216 + dots := 0 211 217 for time.Now().Before(deadline) { 212 218 time.Sleep(pollInterval) 213 219 220 + // Show progress dots 221 + dots = (dots + 1) % 4 222 + fmt.Fprintf(os.Stderr, "\rWaiting for authorization%s ", strings.Repeat(".", dots)) 223 + 214 224 // Poll token endpoint 215 225 tokenReqBody, _ := json.Marshal(DeviceTokenRequest{DeviceCode: codeResp.DeviceCode}) 216 226 tokenResp, err := http.Post(appViewURL+"/auth/device/token", "application/json", bytes.NewReader(tokenReqBody)) 217 227 if err != nil { 218 - fmt.Fprintf(os.Stderr, "Poll failed: %v\n", err) 228 + fmt.Fprintf(os.Stderr, "\nPoll failed: %v\n", err) 219 229 continue 220 230 } 221 231 ··· 229 239 } 230 240 231 241 if tokenResult.Error != "" { 242 + fmt.Fprintf(os.Stderr, "\n") 232 243 return nil, fmt.Errorf("authorization failed: %s", tokenResult.Error) 233 244 } 234 245 235 246 // Success! 247 + fmt.Fprintf(os.Stderr, "\n") 236 248 return &DeviceConfig{ 237 249 Handle: tokenResult.Handle, 238 250 DeviceSecret: tokenResult.DeviceSecret, ··· 240 252 }, nil 241 253 } 242 254 255 + fmt.Fprintf(os.Stderr, "\n") 243 256 return nil, fmt.Errorf("authorization timeout") 244 257 } 245 258 ··· 302 315 303 316 return cmd.Start() 304 317 } 318 + 319 + // buildAppViewURL constructs the AppView URL with the appropriate protocol 320 + func buildAppViewURL(serverURL string) string { 321 + // If serverURL already has a scheme, use it as-is 322 + if strings.HasPrefix(serverURL, "http://") || strings.HasPrefix(serverURL, "https://") { 323 + return serverURL 324 + } 325 + 326 + // Determine protocol based on Docker configuration and heuristics 327 + if isInsecureRegistry(serverURL) { 328 + return "http://" + serverURL 329 + } 330 + 331 + // Default to HTTPS (mirrors Docker's default behavior) 332 + return "https://" + serverURL 333 + } 334 + 335 + // isInsecureRegistry checks if a registry should use HTTP instead of HTTPS 336 + func isInsecureRegistry(serverURL string) bool { 337 + // Check Docker's insecure-registries configuration 338 + insecureRegistries := getDockerInsecureRegistries() 339 + for _, reg := range insecureRegistries { 340 + // Match exact serverURL or just the host part 341 + if reg == serverURL || reg == stripPort(serverURL) { 342 + return true 343 + } 344 + } 345 + 346 + // Fallback heuristics: localhost and private IPs 347 + host := stripPort(serverURL) 348 + 349 + // Check for localhost variants 350 + if host == "localhost" || host == "127.0.0.1" || host == "::1" { 351 + return true 352 + } 353 + 354 + // Check if it's a private IP address 355 + if ip := net.ParseIP(host); ip != nil { 356 + if ip.IsLoopback() || ip.IsPrivate() { 357 + return true 358 + } 359 + } 360 + 361 + return false 362 + } 363 + 364 + // getDockerInsecureRegistries reads Docker's insecure-registries configuration 365 + func getDockerInsecureRegistries() []string { 366 + var paths []string 367 + 368 + // Common Docker daemon.json locations 369 + switch runtime.GOOS { 370 + case "windows": 371 + programData := os.Getenv("ProgramData") 372 + if programData != "" { 373 + paths = append(paths, filepath.Join(programData, "docker", "config", "daemon.json")) 374 + } 375 + default: 376 + // Linux and macOS 377 + paths = append(paths, "/etc/docker/daemon.json") 378 + if homeDir, err := os.UserHomeDir(); err == nil { 379 + // Rootless Docker location 380 + paths = append(paths, filepath.Join(homeDir, ".docker", "daemon.json")) 381 + } 382 + } 383 + 384 + // Try each path 385 + for _, path := range paths { 386 + if config := readDockerDaemonConfig(path); config != nil && len(config.InsecureRegistries) > 0 { 387 + return config.InsecureRegistries 388 + } 389 + } 390 + 391 + return nil 392 + } 393 + 394 + // readDockerDaemonConfig reads and parses a Docker daemon.json file 395 + func readDockerDaemonConfig(path string) *DockerDaemonConfig { 396 + data, err := os.ReadFile(path) 397 + if err != nil { 398 + return nil 399 + } 400 + 401 + var config DockerDaemonConfig 402 + if err := json.Unmarshal(data, &config); err != nil { 403 + return nil 404 + } 405 + 406 + return &config 407 + } 408 + 409 + // stripPort removes the port from a host:port string 410 + func stripPort(hostPort string) string { 411 + if colonIdx := strings.LastIndex(hostPort, ":"); colonIdx != -1 { 412 + // Check if this is IPv6 (has multiple colons) 413 + if strings.Count(hostPort, ":") > 1 { 414 + // IPv6 address, don't strip 415 + return hostPort 416 + } 417 + return hostPort[:colonIdx] 418 + } 419 + return hostPort 420 + }
+20 -13
pkg/appview/db/device_store.go
··· 28 28 29 29 // PendingAuthorization represents a device awaiting user approval 30 30 type PendingAuthorization struct { 31 - DeviceCode string `json:"device_code"` 32 - UserCode string `json:"user_code"` 33 - DeviceName string `json:"device_name"` 34 - IPAddress string `json:"ip_address"` 35 - UserAgent string `json:"user_agent"` 36 - ExpiresAt time.Time `json:"expires_at"` 37 - ApprovedDID string `json:"approved_did"` 38 - ApprovedAt time.Time `json:"approved_at"` 39 - DeviceSecret string `json:"device_secret"` 31 + DeviceCode string `json:"device_code"` 32 + UserCode string `json:"user_code"` 33 + DeviceName string `json:"device_name"` 34 + IPAddress string `json:"ip_address"` 35 + UserAgent string `json:"user_agent"` 36 + ExpiresAt time.Time `json:"expires_at"` 37 + ApprovedDID *string `json:"approved_did"` 38 + ApprovedAt *time.Time `json:"approved_at"` 39 + DeviceSecret *string `json:"device_secret"` 40 40 } 41 41 42 42 // DeviceStore manages devices and pending authorizations with SQLite persistence ··· 194 194 } 195 195 196 196 // Check if already approved 197 - if pending.ApprovedDID != "" { 197 + if pending.ApprovedDID != nil && *pending.ApprovedDID != "" { 198 198 return "", fmt.Errorf("already approved") 199 199 } 200 200 ··· 259 259 for rows.Next() { 260 260 var device Device 261 261 var lastUsed sql.NullTime 262 + var location sql.NullString 262 263 263 264 err := rows.Scan( 264 265 &device.ID, ··· 267 268 &device.Name, 268 269 &device.SecretHash, 269 270 &device.IPAddress, 270 - &device.Location, 271 + &location, 271 272 &device.UserAgent, 272 273 &device.CreatedAt, 273 274 &lastUsed, ··· 278 279 279 280 if lastUsed.Valid { 280 281 device.LastUsed = lastUsed.Time 282 + } 283 + if location.Valid { 284 + device.Location = location.String 281 285 } 282 286 283 287 // Check if this device's hash matches the secret ··· 302 306 `, did) 303 307 304 308 if err != nil { 305 - fmt.Printf("Warning: Failed to list devices: %v\n", err) 306 309 return []*Device{} 307 310 } 308 311 defer rows.Close() ··· 311 314 for rows.Next() { 312 315 var device Device 313 316 var lastUsed sql.NullTime 317 + var location sql.NullString 314 318 315 319 err := rows.Scan( 316 320 &device.ID, ··· 318 322 &device.Handle, 319 323 &device.Name, 320 324 &device.IPAddress, 321 - &device.Location, 325 + &location, 322 326 &device.UserAgent, 323 327 &device.CreatedAt, 324 328 &lastUsed, ··· 329 333 330 334 if lastUsed.Valid { 331 335 device.LastUsed = lastUsed.Time 336 + } 337 + if location.Valid { 338 + device.Location = location.String 332 339 } 333 340 334 341 devices = append(devices, &device)
+8 -7
pkg/appview/handlers/device.go
··· 117 117 } 118 118 119 119 // Check if approved 120 - if pending.ApprovedDID == "" { 120 + if pending.ApprovedDID == nil || *pending.ApprovedDID == "" { 121 121 // Still pending 122 122 resp := DeviceTokenResponse{ 123 123 Error: "authorization_pending", ··· 128 128 } 129 129 130 130 // Approved! Get device from store to find handle 131 - devices := h.Store.ListDevices(pending.ApprovedDID) 131 + devices := h.Store.ListDevices(*pending.ApprovedDID) 132 + 132 133 var handle string 133 134 for _, d := range devices { 134 - if d.DID == pending.ApprovedDID { 135 + if d.DID == *pending.ApprovedDID { 135 136 handle = d.Handle 136 137 break 137 138 } ··· 139 140 140 141 // Return device secret 141 142 resp := DeviceTokenResponse{ 142 - DeviceSecret: pending.DeviceSecret, 143 + DeviceSecret: *pending.DeviceSecret, 143 144 Handle: handle, 144 - DID: pending.ApprovedDID, 145 + DID: *pending.ApprovedDID, 145 146 } 146 147 147 148 w.Header().Set("Content-Type", "application/json") ··· 204 205 } 205 206 206 207 // Check if already approved 207 - if pending.ApprovedDID != "" { 208 + if pending.ApprovedDID != nil && *pending.ApprovedDID != "" { 208 209 h.renderSuccess(w, pending.DeviceName) 209 210 return 210 211 } ··· 260 261 // Approve the device 261 262 _, err := h.Store.ApprovePending(req.UserCode, sess.DID, sess.Handle) 262 263 if err != nil { 264 + fmt.Printf("ERROR [device/approve]: Failed to approve: %v\n", err) 263 265 http.Error(w, fmt.Sprintf("failed to approve: %v", err), http.StatusInternalServerError) 264 266 return 265 267 } 266 - 267 268 w.Header().Set("Content-Type", "application/json") 268 269 json.NewEncoder(w).Encode(map[string]string{"status": "approved"}) 269 270 }
-1
pkg/appview/jetstream/worker.go
··· 602 602 Time string `json:"time"` 603 603 Status string `json:"status,omitempty"` 604 604 } 605 -