diff --git a/README.md b/README.md index 04d46be..839932e 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ If this project saves you time, please give it a star — it really helps visibi ## Quickstart ``` -docker run --rm -p 8081:8081 --net=host \ - -e EXTERNAL_URL=http://localhost:8081 \ +docker run --rm -p 80:80 --net=host \ + -e EXTERNAL_URL=http://localhost \ -e PROXY_URL=http://localhost:8080 \ -e GLOBAL_SECRET=$(openssl rand -hex 32) \ -e PASSWORD=changeme \ @@ -22,7 +22,7 @@ docker run --rm -p 8081:8081 --net=host \ "mcpServers": { "mcp": { "type": "http", - "url": "http://localhost:8081/mcp" + "url": "http://localhost/mcp" } } } @@ -43,22 +43,25 @@ For a simpler approach to publish local MCP servers over OAuth, consider [MCP Wa ### Environment Variables -| Variable | Required | Description | Default | -| ---------------------- | -------- | ------------------------------------------------ | ----------------------- | -| `LISTEN` | No | Server listen address | `:8081` | -| `DATA_PATH` | No | Data directory path | `./data` | -| `EXTERNAL_URL` | No | External URL for OAuth callbacks | `http://localhost:8081` | -| `PROXY_URL` | No | Target MCP server URL | `http://localhost:8080` | -| `GLOBAL_SECRET` | No | Global secret for session encryption | `supersecret` | -| `GOOGLE_CLIENT_ID` | No | Google OAuth client ID | - | -| `GOOGLE_CLIENT_SECRET` | No | Google OAuth client secret | - | -| `GOOGLE_ALLOWED_USERS` | No | Comma-separated list of allowed Google emails | - | -| `GITHUB_CLIENT_ID` | No | GitHub OAuth client ID | - | -| `GITHUB_CLIENT_SECRET` | No | GitHub OAuth client secret | - | -| `GITHUB_ALLOWED_USERS` | No | Comma-separated list of allowed GitHub usernames | - | -| `PASSWORD` | No | Password for authentication | - | -| `PASSWORD_HASH` | No | Hash of the password for authentication | - | -| `MODE` | No | Set to `debug` for development mode | `production` | +| Variable | Required | Description | Default | +| ---------------------- | -------- | ------------------------------------------------ | ------------------------------------------------ | +| `LISTEN` | No | Server listen address | `:80` | +| `TLS_LISTEN` | No | Address to listen on for TLS | `:443` | +| `TLS_HOST` | No | Host name for automatic TLS certificate | - | +| `TLS_DIRECTORY_URL` | No | ACME directory URL for TLS certificates | `https://acme-v02.api.letsencrypt.org/directory` | +| `DATA_PATH` | No | Data directory path | `./data` | +| `EXTERNAL_URL` | No | External URL for OAuth callbacks | `http://localhost` | +| `PROXY_URL` | No | Target MCP server URL | `http://localhost:8080` | +| `GLOBAL_SECRET` | No | Global secret for session encryption | `supersecret` | +| `GOOGLE_CLIENT_ID` | No | Google OAuth client ID | - | +| `GOOGLE_CLIENT_SECRET` | No | Google OAuth client secret | - | +| `GOOGLE_ALLOWED_USERS` | No | Comma-separated list of allowed Google emails | - | +| `GITHUB_CLIENT_ID` | No | GitHub OAuth client ID | - | +| `GITHUB_CLIENT_SECRET` | No | GitHub OAuth client secret | - | +| `GITHUB_ALLOWED_USERS` | No | Comma-separated list of allowed GitHub usernames | - | +| `PASSWORD` | No | Plain text password (will be hashed with bcrypt) | - | +| `PASSWORD_HASH` | No | Bcrypt hash of password for authentication | - | +| `MODE` | No | Set to `debug` for development mode | `production` | ### OAuth Provider Setup diff --git a/example/.mcp.json b/example/.mcp.json index b17d8cd..bf3074f 100644 --- a/example/.mcp.json +++ b/example/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "test": { "type": "http", - "url": "http://localhost:8081/mcp" + "url": "http://localhost/mcp" } } } diff --git a/example/docker-compose.yaml b/example/docker-compose.yaml index 8b15991..9f60434 100644 --- a/example/docker-compose.yaml +++ b/example/docker-compose.yaml @@ -7,11 +7,11 @@ services: env_file: - .env ports: - - 8081:8081 + - 80:80 environment: # If you are accessing from outside (such as Claude Web), # be sure to specify a domain name that can be accessed from outside for EXTERNAL_URL. - - EXTERNAL_URL=http://localhost:8081 + - EXTERNAL_URL=http://localhost - PROXY_URL=http://playwright:8931 volumes: - ./data:/data diff --git a/go.mod b/go.mod index d92ea1b..de8d2a5 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( go.etcd.io/bbolt v1.4.2 go.uber.org/mock v0.5.2 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.40.0 golang.org/x/oauth2 v0.14.0 ) @@ -99,11 +99,12 @@ require ( go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.16.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect diff --git a/go.sum b/go.sum index 80b4b43..8ded8ba 100644 --- a/go.sum +++ b/go.sum @@ -542,8 +542,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -581,8 +581,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -623,8 +623,8 @@ golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -650,8 +650,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -707,8 +707,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -729,8 +729,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -792,8 +792,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index a8a2002..4f00317 100644 --- a/main.go +++ b/main.go @@ -15,8 +15,22 @@ func getEnvWithDefault(key, defaultValue string) string { return defaultValue } +func getEnvBoolWithDefault(key string, defaultValue bool) bool { + if value := os.Getenv(key); value != "" { + if strings.ToLower(value) == "true" || value == "1" { + return true + } + return false + } + return defaultValue +} + func main() { var listen string + var listenTLS string + var tlsHost string + var tlsDirectoryURL string + var tlsAcceptTOS bool var dataPath string var externalURL string var proxyURL string @@ -51,6 +65,10 @@ func main() { if err := mcpproxy.Run( listen, + listenTLS, + tlsHost, + tlsDirectoryURL, + tlsAcceptTOS, dataPath, externalURL, proxyURL, @@ -69,9 +87,13 @@ func main() { }, } - rootCmd.Flags().StringVarP(&listen, "listen", "l", getEnvWithDefault("LISTEN", ":8081"), "Address to listen on") + rootCmd.Flags().StringVar(&listen, "listen", getEnvWithDefault("LISTEN", ":80"), "Address to listen on") + rootCmd.Flags().StringVar(&listenTLS, "listen-tls", getEnvWithDefault("TLS_LISTEN", ":443"), "Address to listen on for TLS") + rootCmd.Flags().StringVarP(&tlsHost, "tls-host", "H", getEnvWithDefault("TLS_HOST", ""), "Host name for TLS") + rootCmd.Flags().StringVar(&tlsDirectoryURL, "tls-directory-url", getEnvWithDefault("TLS_DIRECTORY_URL", "https://acme-v02.api.letsencrypt.org/directory"), "ACME directory URL for TLS certificates") + rootCmd.Flags().BoolVar(&tlsAcceptTOS, "tls-accept-tos", getEnvBoolWithDefault("TLS_ACCEPT_TOS", false), "Accept TLS terms of service") rootCmd.Flags().StringVarP(&dataPath, "data", "d", getEnvWithDefault("DATA_PATH", "./data"), "Path to the data directory") - rootCmd.Flags().StringVarP(&externalURL, "external-url", "e", getEnvWithDefault("EXTERNAL_URL", "http://localhost:8081"), "External URL for the proxy") + rootCmd.Flags().StringVarP(&externalURL, "external-url", "e", getEnvWithDefault("EXTERNAL_URL", "http://localhost"), "External URL for the proxy") rootCmd.Flags().StringVarP(&proxyURL, "proxy-url", "p", getEnvWithDefault("PROXY_URL", "http://localhost:8080"), "Proxy URL for the proxy") rootCmd.Flags().StringVarP(&globalSecret, "global-secret", "s", getEnvWithDefault("GLOBAL_SECRET", "supersecret"), "Global secret for the proxy") diff --git a/pkg/mcp-proxy/main.go b/pkg/mcp-proxy/main.go index dd525bb..792ff1d 100644 --- a/pkg/mcp-proxy/main.go +++ b/pkg/mcp-proxy/main.go @@ -1,11 +1,15 @@ package mcpproxy import ( + "context" "crypto/sha256" + "errors" "fmt" + "net/http" "net/url" "os" "path" + "sync" "time" "github.com/blendle/zapdriver" @@ -19,11 +23,17 @@ import ( "github.com/sigbit/mcp-auth-proxy/pkg/repository" "github.com/sigbit/mcp-auth-proxy/pkg/utils" "go.uber.org/zap" + "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/bcrypt" ) func Run( listen string, + listenTLS string, + tlsHost string, + tlsDirectoryURL string, + tlsAcceptTOS bool, dataPath string, externalURL string, proxyURL string, @@ -131,6 +141,77 @@ func Run( idpRouter.SetupRoutes(router) proxyRouter.SetupRoutes(router) - logger.Info("Starting server", zap.String("listen", listen)) - return router.Run(listen) + if tlsHost != "" { + m := autocert.Manager{ + Prompt: func(tosURL string) bool { + return tlsAcceptTOS + }, + HostPolicy: autocert.HostWhitelist(tlsHost), + Cache: autocert.DirCache(path.Join(dataPath, "certs")), + Client: &acme.Client{ + DirectoryURL: tlsDirectoryURL, + }, + } + + exit := make(chan struct{}, 2) + var wg sync.WaitGroup + + httpServer := &http.Server{ + Addr: listen, + Handler: m.HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host := r.Host + if host == "" { + host = r.URL.Host + } + target := "https://" + host + r.RequestURI + http.Redirect(w, r, target, http.StatusMovedPermanently) + })), + } + httpsServer := &http.Server{ + Addr: listenTLS, + Handler: router, + TLSConfig: m.TLSConfig(), + } + + wg.Add(2) + errs := []error{} + lock := sync.Mutex{} + go func() { + defer wg.Done() + err := httpServer.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + lock.Lock() + errs = append(errs, err) + lock.Unlock() + } + exit <- struct{}{} + }() + + go func() { + defer wg.Done() + err := httpsServer.ListenAndServeTLS("", "") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + lock.Lock() + errs = append(errs, err) + lock.Unlock() + } + exit <- struct{}{} + }() + + logger.Info("Starting server", zap.Strings("listen", []string{listen, listenTLS})) + <-exit + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + if shutdownErr := httpServer.Shutdown(shutdownCtx); shutdownErr != nil { + logger.Warn("HTTP server shutdown error", zap.Error(shutdownErr)) + } + if shutdownErr := httpsServer.Shutdown(shutdownCtx); shutdownErr != nil { + logger.Warn("HTTPS server shutdown error", zap.Error(shutdownErr)) + } + wg.Wait() + return errors.Join(errs...) + } else { + logger.Info("Starting server", zap.String("listen", listen)) + return router.Run(listen) + } }