Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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"
}
}
}
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion example/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"mcpServers": {
"test": {
"type": "http",
"url": "http://localhost:8081/mcp"
"url": "http://localhost/mcp"
}
}
}
4 changes: 2 additions & 2 deletions example/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand Down
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
26 changes: 24 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +65,10 @@ func main() {

if err := mcpproxy.Run(
listen,
listenTLS,
tlsHost,
tlsDirectoryURL,
tlsAcceptTOS,
dataPath,
externalURL,
proxyURL,
Expand All @@ -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")

Expand Down
85 changes: 83 additions & 2 deletions pkg/mcp-proxy/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package mcpproxy

import (
"context"
"crypto/sha256"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path"
"sync"
"time"

"github.com/blendle/zapdriver"
Expand All @@ -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,
Expand Down Expand Up @@ -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,
},
Copy link

Copilot AI Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The acme.Client configuration is incomplete. The Client field should not be set manually when using autocert.Manager, as the manager handles ACME client configuration internally. Remove the Client field assignment.

Suggested change
},
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(tlsHost),
Cache: autocert.DirCache(path.Join(dataPath, "certs")),
DirectoryURL: tlsDirectoryURL,

Copilot uses AI. Check for mistakes.
}

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
Copy link

Copilot AI Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The channel receive will only wait for the first server to exit, but both servers could potentially exit simultaneously. This could lead to a race condition where one server's shutdown is not properly handled. Consider using a sync.WaitGroup or waiting for both servers to complete before proceeding with shutdown.

Suggested change
<-exit
}()
logger.Info("Starting server", zap.Strings("listen", []string{listen, listenTLS}))
wg.Wait()

Copilot uses AI. Check for mistakes.
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)
}
}