mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
194 lines
5.9 KiB
Go
194 lines
5.9 KiB
Go
// Package cmd provides command-line interface functionality for the CLI Proxy API.
|
|
// It implements the main application commands including login/authentication
|
|
// and server startup, handling the complete user onboarding and service lifecycle.
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
|
|
"github.com/luispater/CLIProxyAPI/internal/browser"
|
|
"github.com/luispater/CLIProxyAPI/internal/client"
|
|
"github.com/luispater/CLIProxyAPI/internal/config"
|
|
"github.com/luispater/CLIProxyAPI/internal/util"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// LoginOptions contains options for the Codex login process.
|
|
type LoginOptions struct {
|
|
// NoBrowser indicates whether to skip opening the browser automatically.
|
|
NoBrowser bool
|
|
}
|
|
|
|
// DoCodexLogin handles the Codex OAuth login process for OpenAI Codex services.
|
|
// It initializes the OAuth flow, opens the user's browser for authentication,
|
|
// waits for the callback, exchanges the authorization code for tokens,
|
|
// and saves the authentication information to a file.
|
|
//
|
|
// Parameters:
|
|
// - cfg: The application configuration
|
|
// - options: The login options containing browser preferences
|
|
func DoCodexLogin(cfg *config.Config, options *LoginOptions) {
|
|
if options == nil {
|
|
options = &LoginOptions{}
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
log.Info("Initializing Codex authentication...")
|
|
|
|
// Generate PKCE codes
|
|
pkceCodes, err := codex.GeneratePKCECodes()
|
|
if err != nil {
|
|
log.Fatalf("Failed to generate PKCE codes: %v", err)
|
|
return
|
|
}
|
|
|
|
// Generate random state parameter
|
|
state, err := generateRandomState()
|
|
if err != nil {
|
|
log.Fatalf("Failed to generate state parameter: %v", err)
|
|
return
|
|
}
|
|
|
|
// Initialize OAuth server
|
|
oauthServer := codex.NewOAuthServer(1455)
|
|
|
|
// Start OAuth callback server
|
|
if err = oauthServer.Start(); err != nil {
|
|
if strings.Contains(err.Error(), "already in use") {
|
|
authErr := codex.NewAuthenticationError(codex.ErrPortInUse, err)
|
|
log.Error(codex.GetUserFriendlyMessage(authErr))
|
|
os.Exit(13) // Exit code 13 for port-in-use error
|
|
}
|
|
authErr := codex.NewAuthenticationError(codex.ErrServerStartFailed, err)
|
|
log.Fatalf("Failed to start OAuth callback server: %v", authErr)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err = oauthServer.Stop(ctx); err != nil {
|
|
log.Warnf("Failed to stop OAuth server: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Initialize Codex auth service
|
|
openaiAuth := codex.NewCodexAuth(cfg)
|
|
|
|
// Generate authorization URL
|
|
authURL, err := openaiAuth.GenerateAuthURL(state, pkceCodes)
|
|
if err != nil {
|
|
log.Fatalf("Failed to generate authorization URL: %v", err)
|
|
return
|
|
}
|
|
|
|
// Open browser or display URL
|
|
if !options.NoBrowser {
|
|
log.Info("Opening browser for authentication...")
|
|
|
|
// Check if browser is available
|
|
if !browser.IsAvailable() {
|
|
log.Warn("No browser available on this system")
|
|
util.PrintSSHTunnelInstructions(1455)
|
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
|
} else {
|
|
if err = browser.OpenURL(authURL); err != nil {
|
|
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
|
|
log.Warn(codex.GetUserFriendlyMessage(authErr))
|
|
util.PrintSSHTunnelInstructions(1455)
|
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
|
|
|
// Log platform info for debugging
|
|
platformInfo := browser.GetPlatformInfo()
|
|
log.Debugf("Browser platform info: %+v", platformInfo)
|
|
} else {
|
|
log.Debug("Browser opened successfully")
|
|
}
|
|
}
|
|
} else {
|
|
util.PrintSSHTunnelInstructions(1455)
|
|
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
|
}
|
|
|
|
log.Info("Waiting for authentication callback...")
|
|
|
|
// Wait for OAuth callback
|
|
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "timeout") {
|
|
authErr := codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
|
|
log.Error(codex.GetUserFriendlyMessage(authErr))
|
|
} else {
|
|
log.Errorf("Authentication failed: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if result.Error != "" {
|
|
oauthErr := codex.NewOAuthError(result.Error, "", http.StatusBadRequest)
|
|
log.Error(codex.GetUserFriendlyMessage(oauthErr))
|
|
return
|
|
}
|
|
|
|
// Validate state parameter
|
|
if result.State != state {
|
|
authErr := codex.NewAuthenticationError(codex.ErrInvalidState, fmt.Errorf("expected %s, got %s", state, result.State))
|
|
log.Error(codex.GetUserFriendlyMessage(authErr))
|
|
return
|
|
}
|
|
|
|
log.Debug("Authorization code received, exchanging for tokens...")
|
|
|
|
// Exchange authorization code for tokens
|
|
authBundle, err := openaiAuth.ExchangeCodeForTokens(ctx, result.Code, pkceCodes)
|
|
if err != nil {
|
|
authErr := codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
|
|
log.Errorf("Failed to exchange authorization code for tokens: %v", authErr)
|
|
log.Debug("This may be due to network issues or invalid authorization code")
|
|
return
|
|
}
|
|
|
|
// Create token storage
|
|
tokenStorage := openaiAuth.CreateTokenStorage(authBundle)
|
|
|
|
// Initialize Codex client
|
|
openaiClient, err := client.NewCodexClient(cfg, tokenStorage)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize Codex client: %v", err)
|
|
return
|
|
}
|
|
|
|
// Save token storage
|
|
if err = openaiClient.SaveTokenToFile(); err != nil {
|
|
log.Fatalf("Failed to save authentication tokens: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Info("Authentication successful!")
|
|
if authBundle.APIKey != "" {
|
|
log.Info("API key obtained and saved")
|
|
}
|
|
|
|
log.Info("You can now use Codex services through this CLI")
|
|
}
|
|
|
|
// generateRandomState generates a cryptographically secure random state parameter
|
|
// for OAuth2 flows to prevent CSRF attacks.
|
|
//
|
|
// Returns:
|
|
// - string: A hexadecimal encoded random state string
|
|
// - error: An error if the random generation fails, nil otherwise
|
|
func generateRandomState() (string, error) {
|
|
bytes := make([]byte, 16)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|