staging #41

Merged
bagas merged 10 commits from staging into main 2025-12-28 07:55:00 +00:00
10 changed files with 403 additions and 108 deletions
Showing only changes of commit c69cd68820 - Show all commits

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ id_rsa*
.idea .idea
.env .env
tmp tmp
certs

View File

@@ -26,6 +26,10 @@ The following environment variables can be configured in the `.env` file:
| `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No | | `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No |
| `CERT_LOC` | Path to TLS certificate | `certs/cert.pem` | No | | `CERT_LOC` | Path to TLS certificate | `certs/cert.pem` | No |
| `KEY_LOC` | Path to TLS private key | `certs/privkey.pem` | No | | `KEY_LOC` | Path to TLS private key | `certs/privkey.pem` | No |
| `CERT_STORAGE_PATH` | Path for CertMagic certificate storage | `certs/certmagic` | No |
| `ACME_EMAIL` | Email for Let's Encrypt registration | `admin@<DOMAIN>` | No |
| `CF_API_TOKEN` | Cloudflare API token for DNS-01 challenge | - | Yes (if auto-cert) |
| `ACME_STAGING` | Use Let's Encrypt staging server | `false` | No |
| `SSH_PRIVATE_KEY` | Path to SSH private key (auto-generated if missing) | `certs/id_rsa` | No | | `SSH_PRIVATE_KEY` | Path to SSH private key (auto-generated if missing) | `certs/id_rsa` | No |
| `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No | | `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No |
| `ALLOWED_PORTS` | Port range for TCP tunnels (e.g., 40000-41000) | `40000-41000` | No | | `ALLOWED_PORTS` | Port range for TCP tunnels (e.g., 40000-41000) | `40000-41000` | No |
@@ -35,6 +39,36 @@ The following environment variables can be configured in the `.env` file:
**Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality. **Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality.
### Automatic TLS Certificate Management
The server supports automatic TLS certificate generation and renewal using [CertMagic](https://github.com/caddyserver/certmagic) with Cloudflare DNS-01 challenge. This is required for wildcard certificate support (`*.yourdomain.com`).
**How it works:**
1. If user-provided certificates (`CERT_LOC`, `KEY_LOC`) exist and cover both `DOMAIN` and `*.DOMAIN`, they will be used
2. If certificates are missing, expired, expiring within 30 days, or don't cover the required domains, CertMagic will automatically obtain new certificates from Let's Encrypt
3. Certificates are automatically renewed before expiration
4. User-provided certificates support hot-reload (changes detected every 30 seconds)
**Cloudflare API Token Setup:**
To use automatic certificate generation, you need a Cloudflare API token with the following permissions:
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens)
2. Click "Create Token"
3. Use "Create Custom Token" with these permissions:
- **Zone → Zone → Read** (for all zones or specific zone)
- **Zone → DNS → Edit** (for all zones or specific zone)
4. Copy the token and set it as `CF_API_TOKEN` environment variable
**Example configuration for automatic certificates:**
```env
DOMAIN=example.com
TLS_ENABLED=true
CF_API_TOKEN=your_cloudflare_api_token_here
ACME_EMAIL=admin@example.com
# ACME_STAGING=true # Uncomment for testing to avoid rate limits
```
### SSH Key Auto-Generation ### SSH Key Auto-Generation
If the SSH private key specified in `SSH_PRIVATE_KEY` doesn't exist, the application will automatically generate a new 4096-bit RSA key pair at the specified location. This makes it easier to get started without manually creating SSH keys. If the SSH private key specified in `SSH_PRIVATE_KEY` doesn't exist, the application will automatically generate a new 4096-bit RSA key pair at the specified location. This makes it easier to get started without manually creating SSH keys.

View File

@@ -1,26 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEWzCCA0OgAwIBAgIUDYTdEDHwVznxV/qnn0/WVlHNMZAwDQYJKoZIhvcNAQEL
BQAwgZYxCzAJBgNVBAYTAklEMQ8wDQYDVQQIDAZCYW50ZW4xGjAYBgNVBAcMEVRh
bmdlcmFuZyBTZWxhdGFuMRIwEAYDVQQKDAlGb3NzeSBMVFMxDjAMBgNVBAsMBUZv
c3N5MRQwEgYDVQQDDAtmb3NzeS5teS5pZDEgMB4GCSqGSIb3DQEJARYRYmFnYXNA
Zm9zc3kubXkuaWQwHhcNMjUwMjA3MTYyMTU1WhcNMjYwMjA3MTYyMTU1WjCBljEL
MAkGA1UEBhMCSUQxDzANBgNVBAgMBkJhbnRlbjEaMBgGA1UEBwwRVGFuZ2VyYW5n
IFNlbGF0YW4xEjAQBgNVBAoMCUZvc3N5IExUUzEOMAwGA1UECwwFRm9zc3kxFDAS
BgNVBAMMC2Zvc3N5Lm15LmlkMSAwHgYJKoZIhvcNAQkBFhFiYWdhc0Bmb3NzeS5t
eS5pZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMU3pCA0eP+VR2CA
+p3p0BgKw33xCKQmQx52jnqNbvwW4NMMDc3mS6a+wb6QB6v0WLGMI1g22TtJPatx
7dcy4PCSOCV9xJ2Yfq5HlQgDHYoyE+juy4/pGlMjo45thJ0yI8zOSzaz2esosP22
XkfFwj7oVMXXPIY6UovcAlGU+DFtwVrNa76/esUwJs+7M3tBubjkpcal0wXR+SIX
3fmw5v0YzKV8qLGMGOvX6+OyLQCx4r9gB0d+WOrT6EfrPfAzo07NKjzWG9aWl1rk
Q+h0i38hke2MTFxId7za57L+NlXRjLo3ESbBF0hjYquTQOG3jx/UvWs+I1NEfsdu
vj8beiMCAwEAAaOBnjCBmzAfBgNVHSMEGDAWgBT6nj69+I+GSdgScjfOqnVFzX7G
aTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAs
BgNVHREEJTAjgglsb2NhbGhvc3SCCTEyNy4wLjAuMYILKi5sb2NhbGhvc3QwHQYD
VR0OBBYEFD9ci20pAUTRLVWxvVXjfE2GKwSAMA0GCSqGSIb3DQEBCwUAA4IBAQAT
rkjU+GzQy9B3/nd/79N7ozK80ygzmnRnj3ou/bbqUHOIYQQgKM1cuN6zh57ovRh/
u45s6pZUOUVN59POFUCvqUiOgsDkY/auXGbKtzzqzoZABuvm85ySd6zurOOx8tA5
e7ArX1ppy3LgzSb+cXANvzYC9bCwp70w2YylMFhwHBAp5TXRVqsqG6jujD8GMLoS
zDSDx8M6o8gqWmPQve7Saim9mgLJUvvCYBzvowuNjzZrJfAeGoIfLV127xCQiylm
fzUQ1Ac6udldWm9scA32nteSQMWg2d1nW3RG1nRondp13WUkgGQ490/c97D3MKLt
D1HB5dLIYkRVHVhCKHT1
-----END CERTIFICATE-----

View File

@@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDjDCCAnSgAwIBAgIUPpuvw4ZdpnDBbwOZBt2ALfZykuYwDQYJKoZIhvcNAQEL
BQAwPDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u
ZG9uMQswCQYDVQQLDAJIUTAeFw0yNDExMTkxNjQxNTNaFw0zNDExMTcxNjQxNTNa
MDwxCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRv
bjELMAkGA1UECwwCSFEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD
L/aeaBzgkYxDNiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5E
sHptIpu5/uTBFaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgA
HKryOwMcB5DheBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9r
WSJllNHOFk+RH17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t
8Ik74GeNrM6f+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIk
tOot0YiZGQAoOedtGSEnAgMBAAGjgYUwgYIwCQYDVR0TBAIwADALBgNVHQ8EBAMC
BeAwSQYDVR0RBEIwQIIQbG9jYWxob3N0LmRpcmVjdIISKi5sb2NhbGhvc3QuZGly
ZWN0ghhTUy5jZXJ0LmxvY2FsaG9zdC5kaXJlY3QwHQYDVR0OBBYEFKBVeirQGZ4D
4AKVPd7LGfCF1wEZMA0GCSqGSIb3DQEBCwUAA4IBAQCRpvsc5DrQBo8yATmUS0OK
xfUXfZR28u3xYY+qMHi+ngIVT2TKJ1yoBJezV6WwQLkcGdWacULvPYt3jCFUtaP7
+hzfs5y1FssFsXDx+r3pQxYyE6BX3BhlrJPJhLRyG1siTTgN439Qu40/TsTzNgAT
A9GbAro+W6+qA4H92mBlyfQEOBossID/Kk95uvDnQguUOp0ZBLgFNRfE6Ra9+yC+
ufAOksYDrisPE6kZId0Ra4Ln/GmrIXKjjmLCitq2q2REC/70JSCnaJcBYeThSJLZ
AZ24AF+JteakSJ8FEgRGxvSu0wdZfnMobNoelsjai1p5l5mCTiD8GH9sQCkslnp3
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDL/aeaBzgkYxD
NiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5EsHptIpu5/uTB
FaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgAHKryOwMcB5Dh
eBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9rWSJllNHOFk+R
H17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t8Ik74GeNrM6f
+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIktOot0YiZGQAo
OedtGSEnAgMBAAECggEASXibb2MnQ4zl7ELL+HGYb5sNpLrHJU5M4ujmuMn6jNjh
+C2dbs2KYlMtpMcAweMD8Y0weDYnwiplNx0KSYJECpNnJehTqrn33J0EAyXz3CWX
eWXxBUXpkp2hfoSEQSTth3VDD60Q7ZMXgRdvi2EtBmLKPNLADHGu/aoM5ENdwE9/
E80X68E+MT2czOY/sEI/w2Tf/S2ZVOHRvFOsmvTLFlbiElWG8pmguajpJwdae7uX
c5VD87b0oYicSUvaHe6xOCCyyeBVq06sWk+vh6Tzrw6K/B+q0SYvzXdrdsJxbzUD
PvhVi9rf9AC1Ncb6lFOP2ZjGfqYQvfgGXKqaVxXWQQKBgQD8frATynMSeR15oERa
+Maa7r3GlwWV3tkUblX7/FxBo9UhAsivnWZprccZR43YPowbWNeU3AQppf/YDxmx
70/RVTCMjXPGyspSS2iFtcp+K2KnIZA4BuAG/s1EBrAUW2KrtaTaXgYu9usgb+Fq
dJUEBDWrL59XXtQUwSM1laH0BwKBgQDF5Z31VY96xOS7flmolq8Ag+frSMH0G4np
3nUnTlUkgpFXE5FkmUccbYZv9QialAVriBBUNANPDBQH4PRrNnX8Z6B2HJbpAy30
II9jPMTNKnXT3RKFcJCamNkOQhT0sBAwm4gTsx+7gbpdZxinxH0Cr/08sfU4lbee
EtMV/J1h4QKBgC4MLLBvS20jCW0U/WJZ3F6FC7cb87jRW2WOeb/q1ihiaIwMpezh
F7xOJPFHS2cUgRi7qxVKyreNvor4tgbtTfEvSBtZ8LNgaGV5uyYncTZxUxyH0nVl
S5X7AhRV4+bSg7ws9FOesiH+hgL0ZHe1qzeATQlbNgQJF0RxtKohD9ghAoGAcM0N
WIZInoYUivreSEZ7wiNt0qNKSsZXukLfLGRuC72Q8r1opprn+cBEXRSirtmorT6F
cDmlmS0dTdBgAaytXA4FXM23B2KUkw7sLHi7BOcq+nSM1hrvke+F6aapI0AoOkyt
J+12LP8pJ4xYdWh+iUWfZzVYvcQ5QZUhVOsFGoECgYA9llKmc/cFaXrCr97g7ls8
ZWW1kQKLawAv6+90dwECJl+zpyQN/TURyEfz6oFJDbNEL0xAAcm9thDah177Tq95
pcHbVn/pAI4h4CLzM2ExSe8Ybvy6iPZaBiCVXq3ms1PK0EDyYLH1p3FZqm+UStZh
/6fYspyivrQEivRK3jGuWA==
-----END PRIVATE KEY-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFN6QgNHj/lUdg
gPqd6dAYCsN98QikJkMedo56jW78FuDTDA3N5kumvsG+kAer9FixjCNYNtk7ST2r
ce3XMuDwkjglfcSdmH6uR5UIAx2KMhPo7suP6RpTI6OObYSdMiPMzks2s9nrKLD9
tl5HxcI+6FTF1zyGOlKL3AJRlPgxbcFazWu+v3rFMCbPuzN7Qbm45KXGpdMF0fki
F935sOb9GMylfKixjBjr1+vjsi0AseK/YAdHfljq0+hH6z3wM6NOzSo81hvWlpda
5EPodIt/IZHtjExcSHe82uey/jZV0Yy6NxEmwRdIY2Krk0Dht48f1L1rPiNTRH7H
br4/G3ojAgMBAAECggEAATiJ23ZhS1++squ87jsgAfR+TYP8Kpv408u/43Ig5KgC
ZnwPUWqvP2e0TLwyhMKwXxHMt2nITxQlSnyCXU/5nk07BYxkqhiwEni4Xo86YK+H
rQXEnKFaSHdF6dNNIo+VZiark4adS9XgJp0fsn0LnON7GhBt72MvPW6auxH1HJIC
rcjnzJu/KzTVrId9QNsEDl9cNRlHXuPSfohdq2o39PBKGDeDEDTvP9wHtasEF6oe
uj1OH5fAxiXptT0Ln0Y8QeFe8R8Odul5mUXCgMOkKmHZPDxTq9ldCuPjrz15JfnS
T3MecaWdvChpIP2WybjLstJy9qXTl0fhRkpJWpaVbQKBgQDu9puq64vZn0jak7ns
bFbKPemFw7tiiwwnqyzqCTRddw1vETJN/MgpRWvc7d1Cvzs2+Z2kn0Cgpdw/30Ej
VRHiE/d1rj+u33F6vGBUBccRue1t0UdFMFF/YnlNDRRnljQgx4N3mezzNE2RikR8
jVp+jvWTSTgf3y4Hip1ffCSNtQKBgQDTRx1huSzUOed/36YLdnxP+AlV3L+c/EKU
GItKZk2bqRlqgs6wcsNlVjveb9o9j6M5pg/lh5HaqJyhj8DYVnEdmXfcQJiUfIPh
5802asIXQVBmL9rTMR34wsr/lzzK3k0KORqFcUdf2fDI/UQNpVUJvWQQY8fl2sVo
zmp88JAPdwKBgQCLC6fsvn5ztMF5nffTX/7oUzosgYXpgyshcfMCgzSbJgkFFaaF
xo7ZpPFsbmQO0KMuC/T0s02xrJEKAWgvnPJ48FFPgoK/yHiJiE8s1OfOordK7Tlh
QwpI6w3WDcRPuhC++hi/YSuFIGv6QdA0ATQk7B5tA2/K69wmuztzMhM6+QKBgH5E
LwQbRfZj0L20bKjHHA4y32loLz/j5upZLM2/DDyuN9lW6a28OJiUi90pHdXSxSsL
2s5DUmDKiiloH0lrh9i3wlFobYe4Tp0xCoyuCucZCrK3gODcptvnlqhfu15GsuYc
MIR1qcFYH7YO3qAFIiha/rVo3Ku7LmWvjyayInaLAoGAZ/dELEAcyhImf1HvtmBT
qNg4uI6t/2fvHdoAeQkkGjEDWFTGEaMp0cJEbETPD68TwNh5dL/xUJibTwMbK+m5
rdIA2oTZMkFtWubIvMCrD/J6E+8Pzl+eK+0C0axO/I29S4veORNGWvCtqdFICDgZ
CluJ0NZFeMH8g8tgfI9HnHc=
-----END PRIVATE KEY-----

20
go.mod
View File

@@ -7,4 +7,22 @@ require (
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.45.0
) )
require golang.org/x/sys v0.38.0 // indirect require (
github.com/caddyserver/certmagic v0.25.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/cloudflare v0.2.2 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mholt/acmez/v3 v3.1.3 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
)

32
go.sum
View File

@@ -1,9 +1,37 @@
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
@@ -11,3 +39,7 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=

View File

@@ -13,13 +13,14 @@ import (
) )
func NewHTTPSServer() error { func NewHTTPSServer() error {
cert, err := tls.LoadX509KeyPair(utils.Getenv("CERT_LOC", "certs/cert.pem"), utils.Getenv("KEY_LOC", "certs/privkey.pem")) domain := utils.Getenv("DOMAIN", "localhost")
tlsConfig, err := NewTLSConfig(domain)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to initialize TLS config: %w", err)
} }
config := &tls.Config{Certificates: []tls.Certificate{cert}} ln, err := tls.Listen("tcp", ":443", tlsConfig)
ln, err := tls.Listen("tcp", ":443", config)
if err != nil { if err != nil {
return err return err
} }

312
server/tls.go Normal file
View File

@@ -0,0 +1,312 @@
package server
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"os"
"sync"
"time"
"tunnel_pls/utils"
"github.com/caddyserver/certmagic"
"github.com/libdns/cloudflare"
)
type TLSManager struct {
domain string
certPath string
keyPath string
storagePath string
userCert *tls.Certificate
userCertMu sync.RWMutex
magic *certmagic.Config
useCertMagic bool
}
var tlsManager *TLSManager
var tlsManagerOnce sync.Once
func NewTLSConfig(domain string) (*tls.Config, error) {
var initErr error
tlsManagerOnce.Do(func() {
certPath := utils.Getenv("CERT_LOC", "certs/cert.pem")
keyPath := utils.Getenv("KEY_LOC", "certs/privkey.pem")
storagePath := utils.Getenv("CERT_STORAGE_PATH", "certs/certmagic")
tm := &TLSManager{
domain: domain,
certPath: certPath,
keyPath: keyPath,
storagePath: storagePath,
}
if tm.userCertsExistAndValid() {
log.Printf("Using user-provided certificates from %s and %s", certPath, keyPath)
if err := tm.loadUserCerts(); err != nil {
initErr = fmt.Errorf("failed to load user certificates: %w", err)
return
}
tm.useCertMagic = false
tm.startCertWatcher()
} else {
if !isACMEConfigComplete() {
log.Printf("User certificates missing or invalid, and ACME configuration is incomplete")
log.Printf("To enable automatic certificate generation, set CF_API_TOKEN environment variable")
initErr = fmt.Errorf("no valid certificates found and ACME configuration is incomplete (CF_API_TOKEN is required)")
return
}
log.Printf("User certificates missing or don't cover %s and *.%s, using CertMagic", domain, domain)
if err := tm.initCertMagic(); err != nil {
initErr = fmt.Errorf("failed to initialize CertMagic: %w", err)
return
}
tm.useCertMagic = true
}
tlsManager = tm
})
if initErr != nil {
return nil, initErr
}
return tlsManager.getTLSConfig(), nil
}
func isACMEConfigComplete() bool {
cfAPIToken := utils.Getenv("CF_API_TOKEN", "")
return cfAPIToken != ""
}
func (tm *TLSManager) userCertsExistAndValid() bool {
if _, err := os.Stat(tm.certPath); os.IsNotExist(err) {
log.Printf("Certificate file not found: %s", tm.certPath)
return false
}
if _, err := os.Stat(tm.keyPath); os.IsNotExist(err) {
log.Printf("Key file not found: %s", tm.keyPath)
return false
}
return ValidateCertDomains(tm.certPath, tm.domain)
}
func ValidateCertDomains(certPath, domain string) bool {
certPEM, err := os.ReadFile(certPath)
if err != nil {
log.Printf("Failed to read certificate: %v", err)
return false
}
block, _ := pem.Decode(certPEM)
if block == nil {
log.Printf("Failed to decode PEM block from certificate")
return false
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Printf("Failed to parse certificate: %v", err)
return false
}
if time.Now().After(cert.NotAfter) {
log.Printf("Certificate has expired (NotAfter: %v)", cert.NotAfter)
return false
}
if time.Now().Add(30 * 24 * time.Hour).After(cert.NotAfter) {
log.Printf("Certificate expiring soon (NotAfter: %v), will use CertMagic for renewal", cert.NotAfter)
return false
}
var certDomains []string
if cert.Subject.CommonName != "" {
certDomains = append(certDomains, cert.Subject.CommonName)
}
certDomains = append(certDomains, cert.DNSNames...)
hasBase := false
hasWildcard := false
wildcardDomain := "*." + domain
for _, d := range certDomains {
if d == domain {
hasBase = true
}
if d == wildcardDomain {
hasWildcard = true
}
}
if !hasBase {
log.Printf("Certificate does not cover base domain: %s", domain)
}
if !hasWildcard {
log.Printf("Certificate does not cover wildcard domain: %s", wildcardDomain)
}
return hasBase && hasWildcard
}
func (tm *TLSManager) loadUserCerts() error {
cert, err := tls.LoadX509KeyPair(tm.certPath, tm.keyPath)
if err != nil {
return err
}
tm.userCertMu.Lock()
tm.userCert = &cert
tm.userCertMu.Unlock()
log.Printf("Loaded user certificates successfully")
return nil
}
func (tm *TLSManager) startCertWatcher() {
go func() {
var lastCertMod, lastKeyMod time.Time
if info, err := os.Stat(tm.certPath); err == nil {
lastCertMod = info.ModTime()
}
if info, err := os.Stat(tm.keyPath); err == nil {
lastKeyMod = info.ModTime()
}
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
certInfo, certErr := os.Stat(tm.certPath)
keyInfo, keyErr := os.Stat(tm.keyPath)
if certErr != nil || keyErr != nil {
continue
}
if certInfo.ModTime().After(lastCertMod) || keyInfo.ModTime().After(lastKeyMod) {
log.Printf("Certificate files changed, reloading...")
if !ValidateCertDomains(tm.certPath, tm.domain) {
log.Printf("New certificates don't cover required domains")
if !isACMEConfigComplete() {
log.Printf("Cannot switch to CertMagic: ACME configuration is incomplete (CF_API_TOKEN is required)")
continue
}
log.Printf("Switching to CertMagic for automatic certificate management")
if err := tm.initCertMagic(); err != nil {
log.Printf("Failed to initialize CertMagic: %v", err)
continue
}
tm.useCertMagic = true
return
}
if err := tm.loadUserCerts(); err != nil {
log.Printf("Failed to reload certificates: %v", err)
continue
}
lastCertMod = certInfo.ModTime()
lastKeyMod = keyInfo.ModTime()
log.Printf("Certificates reloaded successfully")
}
}
}()
}
func (tm *TLSManager) initCertMagic() error {
if err := os.MkdirAll(tm.storagePath, 0700); err != nil {
return fmt.Errorf("failed to create cert storage directory: %w", err)
}
acmeEmail := utils.Getenv("ACME_EMAIL", "admin@"+tm.domain)
cfAPIToken := utils.Getenv("CF_API_TOKEN", "")
acmeStaging := utils.Getenv("ACME_STAGING", "false") == "true"
if cfAPIToken == "" {
return fmt.Errorf("CF_API_TOKEN environment variable is required for automatic certificate generation")
}
cfProvider := &cloudflare.Provider{
APIToken: cfAPIToken,
}
storage := &certmagic.FileStorage{Path: tm.storagePath}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return tm.magic, nil
},
})
magic := certmagic.New(cache, certmagic.Config{
Storage: storage,
})
acmeIssuer := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
Email: acmeEmail,
Agreed: true,
DNS01Solver: &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{
DNSProvider: cfProvider,
},
},
})
if acmeStaging {
acmeIssuer.CA = certmagic.LetsEncryptStagingCA
log.Printf("Using Let's Encrypt staging server")
} else {
acmeIssuer.CA = certmagic.LetsEncryptProductionCA
log.Printf("Using Let's Encrypt production server")
}
magic.Issuers = []certmagic.Issuer{acmeIssuer}
tm.magic = magic
domains := []string{tm.domain, "*." + tm.domain}
log.Printf("Requesting certificates for: %v", domains)
ctx := context.Background()
if err := magic.ManageSync(ctx, domains); err != nil {
return fmt.Errorf("failed to obtain certificates: %w", err)
}
log.Printf("Certificates obtained successfully for %v", domains)
return nil
}
func (tm *TLSManager) getTLSConfig() *tls.Config {
return &tls.Config{
GetCertificate: tm.getCertificate,
MinVersion: tls.VersionTLS12,
}
}
func (tm *TLSManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if tm.useCertMagic {
return tm.magic.GetCertificate(hello)
}
tm.userCertMu.RLock()
defer tm.userCertMu.RUnlock()
if tm.userCert == nil {
return nil, fmt.Errorf("no certificate available")
}
return tm.userCert, nil
}