@@ -2,6 +2,7 @@ package main
22
33import (
44 "context"
5+ "crypto/subtle"
56 "errors"
67 "fmt"
78 "log"
@@ -10,27 +11,44 @@ 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.
26+ // NOTE: Anthropic clients are expected to include /v1 in their requests, so
27+ // it is not idiomatic to include it in the base URL.
2028 defaultAnthropicBaseURL = "https://api.anthropic.com"
21- openAIInboundPrefix = "/openai/"
22- anthropicInboundPrefix = "/anthropic/"
23- healthPath = "/health/liveliness"
24- headerAuthorization = "Authorization"
25- headerAnthropicAPIKey = "X-Api-Key"
29+ // openAIInboundPrefix is the path prefix used to route requests to OpenAI.
30+ openAIInboundPrefix = "/openai/"
31+ // anthropicInboundPrefix is the path prefix used to route requests to Anthropic.
32+ anthropicInboundPrefix = "/anthropic/"
33+ // healthPath is the HTTP endpoint used for container health checks.
34+ healthPath = "/health/liveliness"
35+ // headerAuthorization is the inbound HTTP header that carries bearer tokens.
36+ headerAuthorization = "Authorization"
37+ // headerAnthropicAPIKey is the Anthropic-specific header carrying API keys.
38+ headerAnthropicAPIKey = "X-Api-Key"
2639)
2740
2841// providerProxy defines how to forward requests to a specific upstream API.
2942type providerProxy struct {
30- Prefix string
31- Target * url.URL
32- HeaderName string
43+ // Prefix is the inbound path prefix handled by the provider.
44+ Prefix string
45+ // Target is the upstream endpoint used to service requests for the provider.
46+ Target * url.URL
47+ // HeaderName is the outbound header carrying the provider-specific credential.
48+ HeaderName string
49+ // HeaderValue is the credential value set on outbound requests.
3350 HeaderValue string
51+ // DisplayName is the human-readable name of the provider used in logs.
3452 DisplayName string
3553}
3654
@@ -41,6 +59,9 @@ func main() {
4159 log .Fatalf ("proxy configuration error: %v" , err )
4260 }
4361
62+ ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGINT , syscall .SIGTERM )
63+ defer stop ()
64+
4465 mux := http .NewServeMux ()
4566 mux .HandleFunc (healthPath , handleHealth )
4667
@@ -63,6 +84,15 @@ func main() {
6384 log .Printf ("proxy listening on %s (OpenAI -> %s, Anthropic -> %s)" ,
6485 cfg .listenAddr , cfg .openAIProxy .Target .String (), cfg .anthropicProxy .Target .String ())
6586
87+ go func () {
88+ <- ctx .Done ()
89+ shutdownCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
90+ defer cancel ()
91+ if err := server .Shutdown (shutdownCtx ); err != nil {
92+ log .Printf ("proxy shutdown error: %v" , err )
93+ }
94+ }()
95+
6696 if err = server .ListenAndServe (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
6797 log .Fatalf ("proxy server error: %v" , err )
6898 }
@@ -289,6 +319,8 @@ func firstNonEmpty(values ...string) string {
289319 return ""
290320}
291321
322+ // validateClientToken ensures inbound requests present the proxy bearer secret using
323+ // a constant-time comparison to avoid leaking timing information.
292324func validateClientToken (headerValue , expectedToken string ) bool {
293325 if expectedToken == "" {
294326 return false
@@ -300,5 +332,6 @@ func validateClientToken(headerValue, expectedToken string) bool {
300332 if ! strings .EqualFold (parts [0 ], "bearer" ) {
301333 return false
302334 }
303- return strings .TrimSpace (parts [1 ]) == expectedToken
335+ provided := strings .TrimSpace (parts [1 ])
336+ return subtle .ConstantTimeCompare ([]byte (provided ), []byte (expectedToken )) == 1
304337}
0 commit comments