mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
169 lines
5.2 KiB
Go
169 lines
5.2 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"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
|
|
"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"
|
|
)
|
|
|
|
// DoClaudeLogin handles the Claude OAuth login process for Anthropic Claude 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 DoClaudeLogin(cfg *config.Config, options *LoginOptions) {
|
|
if options == nil {
|
|
options = &LoginOptions{}
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
log.Info("Initializing Claude authentication...")
|
|
|
|
// Generate PKCE codes
|
|
pkceCodes, err := claude.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 := claude.NewOAuthServer(54545)
|
|
|
|
// Start OAuth callback server
|
|
if err = oauthServer.Start(); err != nil {
|
|
if strings.Contains(err.Error(), "already in use") {
|
|
authErr := claude.NewAuthenticationError(claude.ErrPortInUse, err)
|
|
log.Error(claude.GetUserFriendlyMessage(authErr))
|
|
os.Exit(13) // Exit code 13 for port-in-use error
|
|
}
|
|
authErr := claude.NewAuthenticationError(claude.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 Claude auth service
|
|
anthropicAuth := claude.NewClaudeAuth(cfg)
|
|
|
|
// Generate authorization URL
|
|
authURL, state, err := anthropicAuth.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(54545)
|
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
|
} else {
|
|
if err = browser.OpenURL(authURL); err != nil {
|
|
authErr := claude.NewAuthenticationError(claude.ErrBrowserOpenFailed, err)
|
|
log.Warn(claude.GetUserFriendlyMessage(authErr))
|
|
util.PrintSSHTunnelInstructions(54545)
|
|
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(54545)
|
|
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 := claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
|
|
log.Error(claude.GetUserFriendlyMessage(authErr))
|
|
} else {
|
|
log.Errorf("Authentication failed: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if result.Error != "" {
|
|
oauthErr := claude.NewOAuthError(result.Error, "", http.StatusBadRequest)
|
|
log.Error(claude.GetUserFriendlyMessage(oauthErr))
|
|
return
|
|
}
|
|
|
|
// Validate state parameter
|
|
if result.State != state {
|
|
authErr := claude.NewAuthenticationError(claude.ErrInvalidState, fmt.Errorf("expected %s, got %s", state, result.State))
|
|
log.Error(claude.GetUserFriendlyMessage(authErr))
|
|
return
|
|
}
|
|
|
|
log.Debug("Authorization code received, exchanging for tokens...")
|
|
|
|
// Exchange authorization code for tokens
|
|
authBundle, err := anthropicAuth.ExchangeCodeForTokens(ctx, result.Code, state, pkceCodes)
|
|
if err != nil {
|
|
authErr := claude.NewAuthenticationError(claude.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 := anthropicAuth.CreateTokenStorage(authBundle)
|
|
|
|
// Initialize Claude client
|
|
anthropicClient := client.NewClaudeClient(cfg, tokenStorage)
|
|
|
|
// Save token storage
|
|
if err = anthropicClient.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 Claude services through this CLI")
|
|
|
|
}
|