@@ -2,6 +2,7 @@ package main
22
33import (
44 "context"
5+ "crypto/subtle"
56 "errors"
67 "fmt"
78 "log"
@@ -10,27 +11,42 @@ import (
1011 "net/http/httputil"
1112 "net/url"
1213 "os"
14+ "os/signal"
1315 "strings"
16+ "syscall"
1417 "time"
1518)
1619
1720const (
18- defaultListenAddr = ":4000"
19- defaultOpenAIBaseURL = "https://api.openai.com/v1"
21+ // defaultListenAddr is the fallback bind address when none is provided via PROXY_LISTEN_ADDR.
22+ defaultListenAddr = ":4000"
23+ // defaultOpenAIBaseURL is the upstream OpenAI API base path used when none is provided.
24+ defaultOpenAIBaseURL = "https://api.openai.com/v1"
25+ // defaultAnthropicBaseURL is the upstream Anthropic API base path used when none is provided.
2026 defaultAnthropicBaseURL = "https://api.anthropic.com"
21- openAIInboundPrefix = "/openai/"
22- anthropicInboundPrefix = "/anthropic/"
23- healthPath = "/health/liveliness"
24- headerAuthorization = "Authorization"
25- headerAnthropicAPIKey = "X-Api-Key"
27+ // openAIInboundPrefix is the path prefix used to route requests to OpenAI.
28+ openAIInboundPrefix = "/openai/"
29+ // anthropicInboundPrefix is the path prefix used to route requests to Anthropic.
30+ anthropicInboundPrefix = "/anthropic/"
31+ // healthPath is the HTTP endpoint used for container health checks.
32+ healthPath = "/health/liveliness"
33+ // headerAuthorization is the inbound HTTP header that carries bearer tokens.
34+ headerAuthorization = "Authorization"
35+ // headerAnthropicAPIKey is the Anthropic-specific header carrying API keys.
36+ headerAnthropicAPIKey = "X-Api-Key"
2637)
2738
2839// providerProxy defines how to forward requests to a specific upstream API.
2940type providerProxy struct {
30- Prefix string
31- Target * url.URL
32- HeaderName string
41+ // Prefix is the inbound path prefix handled by the provider.
42+ Prefix string
43+ // Target is the upstream endpoint used to service requests for the provider.
44+ Target * url.URL
45+ // HeaderName is the outbound header carrying the provider-specific credential.
46+ HeaderName string
47+ // HeaderValue is the credential value set on outbound requests.
3348 HeaderValue string
49+ // DisplayName is the human-readable name of the provider used in logs.
3450 DisplayName string
3551}
3652
@@ -41,6 +57,9 @@ func main() {
4157 log .Fatalf ("proxy configuration error: %v" , err )
4258 }
4359
60+ ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGINT , syscall .SIGTERM )
61+ defer stop ()
62+
4463 mux := http .NewServeMux ()
4564 mux .HandleFunc (healthPath , handleHealth )
4665
@@ -63,6 +82,15 @@ func main() {
6382 log .Printf ("proxy listening on %s (OpenAI -> %s, Anthropic -> %s)" ,
6483 cfg .listenAddr , cfg .openAIProxy .Target .String (), cfg .anthropicProxy .Target .String ())
6584
85+ go func () {
86+ <- ctx .Done ()
87+ shutdownCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
88+ defer cancel ()
89+ if err := server .Shutdown (shutdownCtx ); err != nil {
90+ log .Printf ("proxy shutdown error: %v" , err )
91+ }
92+ }()
93+
6694 if err = server .ListenAndServe (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
6795 log .Fatalf ("proxy server error: %v" , err )
6896 }
@@ -289,6 +317,8 @@ func firstNonEmpty(values ...string) string {
289317 return ""
290318}
291319
320+ // validateClientToken ensures inbound requests present the proxy bearer secret using
321+ // a constant-time comparison to avoid leaking timing information.
292322func validateClientToken (headerValue , expectedToken string ) bool {
293323 if expectedToken == "" {
294324 return false
@@ -300,5 +330,6 @@ func validateClientToken(headerValue, expectedToken string) bool {
300330 if ! strings .EqualFold (parts [0 ], "bearer" ) {
301331 return false
302332 }
303- return strings .TrimSpace (parts [1 ]) == expectedToken
333+ provided := strings .TrimSpace (parts [1 ])
334+ return subtle .ConstantTimeCompare ([]byte (provided ), []byte (expectedToken )) == 1
304335}
0 commit comments