Add SOCKS5 and HTTP/HTTPS proxy support

- Updated `GetAuthenticatedClient` to handle proxy configuration via `proxy-url`.
- Extended `Config` to include `proxy-url` property.
- Adjusted error handling and removed unused JSON error response logic for API handlers.
- Updated documentation and configuration examples to reflect new proxy settings.
This commit is contained in:
Luis Pater
2025-07-03 16:50:20 +08:00
parent 827bd6e356
commit d29245666e
7 changed files with 77 additions and 28 deletions

View File

@@ -146,12 +146,13 @@ The server uses a YAML configuration file (`config.yaml`) located in the project
### Configuration Options ### Configuration Options
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
|-----------|------|--------------------|-------------| |-------------|----------|--------------------|----------------------------------------------------------------------------------------------|
| `port` | integer | 8317 | The port number on which the server will listen | | `port` | integer | 8317 | The port number on which the server will listen |
| `auth_dir` | string | "~/.cli-proxy-api" | Directory where authentication tokens are stored. Supports using `~` for home directory | | `auth_dir` | string | "~/.cli-proxy-api" | Directory where authentication tokens are stored. Supports using `~` for home directory |
| `debug` | boolean | false | Enable debug mode for verbose logging | | `proxy-url` | string | "" | Proxy url, support socks5/http/https protocol, example: socks5://user:pass@192.168.1.1:1080/ |
| `api_keys` | string[] | [] | List of API keys that can be used to authenticate requests | | `debug` | boolean | false | Enable debug mode for verbose logging |
| `api_keys` | string[] | [] | List of API keys that can be used to authenticate requests |
### Example Configuration File ### Example Configuration File

View File

@@ -104,7 +104,7 @@ func main() {
clientCtx := context.Background() clientCtx := context.Background()
log.Info("Initializing authentication...") log.Info("Initializing authentication...")
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg.AuthDir) httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg)
if errGetClient != nil { if errGetClient != nil {
log.Fatalf("failed to get authenticated client: %v", errGetClient) log.Fatalf("failed to get authenticated client: %v", errGetClient)
return return
@@ -165,7 +165,7 @@ func main() {
clientCtx := context.Background() clientCtx := context.Background()
log.Info("Initializing authentication...") log.Info("Initializing authentication...")
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg.AuthDir) httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg)
if errGetClient != nil { if errGetClient != nil {
log.Fatalf("failed to get authenticated client: %v", errGetClient) log.Fatalf("failed to get authenticated client: %v", errGetClient)
return errGetClient return errGetClient

View File

@@ -1,6 +1,7 @@
port: 8317 port: 8317
auth_dir: "~/.cli-proxy-api" auth_dir: "~/.cli-proxy-api"
debug: false debug: false
proxy-url: ""
api_keys: api_keys:
- "12345" - "12345"
- "23456" - "23456"

View File

@@ -429,12 +429,15 @@ func (h *APIHandlers) handleNonStreamingResponse(c *gin.Context, rawJson []byte)
} }
case err, okError := <-errChan: case err, okError := <-errChan:
if okError { if okError {
c.JSON(http.StatusInternalServerError, ErrorResponse{ c.Status(http.StatusInternalServerError)
Error: ErrorDetail{ _, _ = fmt.Fprint(c.Writer, err.Error())
Message: err.Error(), flusher.Flush()
Type: "server_error", // c.JSON(http.StatusInternalServerError, ErrorResponse{
}, // Error: ErrorDetail{
}) // Message: err.Error(),
// Type: "server_error",
// },
// })
cliCancel() cliCancel()
return return
} }
@@ -523,12 +526,15 @@ func (h *APIHandlers) handleStreamingResponse(c *gin.Context, rawJson []byte) {
} }
case err, okError := <-errChan: case err, okError := <-errChan:
if okError { if okError {
c.JSON(http.StatusInternalServerError, ErrorResponse{ c.Status(http.StatusInternalServerError)
Error: ErrorDetail{ _, _ = fmt.Fprint(c.Writer, err.Error())
Message: err.Error(), flusher.Flush()
Type: "server_error", // c.JSON(http.StatusInternalServerError, ErrorResponse{
}, // Error: ErrorDetail{
}) // Message: err.Error(),
// Type: "server_error",
// },
// })
cliCancel() cliCancel()
return return
} }

View File

@@ -5,10 +5,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/luispater/CLIProxyAPI/internal/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"golang.org/x/net/proxy"
"io" "io"
"net"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@@ -39,7 +43,42 @@ type TokenStorage struct {
// GetAuthenticatedClient configures and returns an HTTP client with OAuth2 tokens. // GetAuthenticatedClient configures and returns an HTTP client with OAuth2 tokens.
// It handles the entire flow: loading, refreshing, and fetching new tokens. // It handles the entire flow: loading, refreshing, and fetching new tokens.
func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, authDir string) (*http.Client, error) { func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, cfg *config.Config) (*http.Client, error) {
proxyURL, err := url.Parse(cfg.ProxyUrl)
if err == nil {
if proxyURL.Scheme == "socks5" {
username := proxyURL.User.Username()
password, _ := proxyURL.User.Password()
auth := &proxy.Auth{
User: username,
Password: password,
}
dialer, errSOCKS5 := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
if errSOCKS5 != nil {
log.Fatalf("create SOCKS5 dialer failed: %v", errSOCKS5)
}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) {
return dialer.Dial(network, addr)
},
}
proxyClient := &http.Client{
Transport: transport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, proxyClient)
} else if proxyURL.Scheme == "http" || proxyURL.Scheme == "https" {
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
proxyClient := &http.Client{
Transport: transport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, proxyClient)
}
}
conf := &oauth2.Config{ conf := &oauth2.Config{
ClientID: oauthClientID, ClientID: oauthClientID,
ClientSecret: oauthClientSecret, ClientSecret: oauthClientSecret,
@@ -49,7 +88,6 @@ func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, authDir strin
} }
var token *oauth2.Token var token *oauth2.Token
var err error
if ts.Token == nil { if ts.Token == nil {
log.Info("Could not load token from file, starting OAuth flow.") log.Info("Could not load token from file, starting OAuth flow.")
@@ -57,7 +95,7 @@ func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, authDir strin
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get token from web: %w", err) return nil, fmt.Errorf("failed to get token from web: %w", err)
} }
ts, err = saveTokenToFile(ctx, conf, token, ts.ProjectID, authDir) ts, err = saveTokenToFile(ctx, conf, token, ts.ProjectID, cfg.AuthDir)
if err != nil { if err != nil {
// Log the error but proceed, as we have a valid token for the session. // Log the error but proceed, as we have a valid token for the session.
log.Errorf("Warning: failed to save token to file: %v", err) log.Errorf("Warning: failed to save token to file: %v", err)

View File

@@ -284,7 +284,9 @@ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body int
_ = resp.Body.Close() _ = resp.Body.Close()
}() }()
bodyBytes, _ := io.ReadAll(resp.Body) bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("api streaming request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
return nil, fmt.Errorf(string(bodyBytes))
// return nil, fmt.Errorf("api streaming request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
} }
return resp.Body, nil return resp.Body, nil

View File

@@ -8,10 +8,11 @@ import (
// Config represents the application's configuration // Config represents the application's configuration
type Config struct { type Config struct {
Port int `yaml:"port"` Port int `yaml:"port"`
AuthDir string `yaml:"auth_dir"` AuthDir string `yaml:"auth_dir"`
Debug bool `yaml:"debug"` Debug bool `yaml:"debug"`
ApiKeys []string `yaml:"api_keys"` ProxyUrl string `yaml:"proxy-url"`
ApiKeys []string `yaml:"api_keys"`
} }
// / LoadConfig loads the configuration from the specified file // / LoadConfig loads the configuration from the specified file