From 92cdddec9c20fd8c638d789cd1d2aac5e2581118 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 10 Jul 2024 01:09:18 +0200 Subject: [PATCH] https server --- Dockerfile | 9 ++-- README.md | 2 +- certs/certs.go | 79 ++++++++++++++++++++--------- certs/config.go | 1 + fly.toml | 34 ++++++++++--- http/server.go | 54 ++++++++++++++++++-- http/static/favicon.ico | Bin 0 -> 5286 bytes http/static/index.html | 89 +++++++++++++++++++++++++++++++++ http/static/og.png | Bin 0 -> 32442 bytes http/static/styles.css | 107 ++++++++++++++++++++++++++++++++++++++++ main.go | 6 +-- xip/xip.go | 49 +++++++++--------- 12 files changed, 360 insertions(+), 70 deletions(-) create mode 100644 http/static/favicon.ico create mode 100644 http/static/index.html create mode 100644 http/static/og.png create mode 100644 http/static/styles.css diff --git a/Dockerfile b/Dockerfile index 3d6c462..391942a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,12 +10,13 @@ FROM gcr.io/distroless/base-debian12:latest ENV PORT 53 -WORKDIR / +WORKDIR /app -COPY --from=build /app/local-ip / -COPY ./.lego /.lego +COPY --from=build /app/local-ip /app/local-ip +COPY --from=build /app/http/static /app/http/static +COPY ./.lego /app/.lego EXPOSE $PORT USER root -CMD ["/local-ip"] +CMD ["/app/local-ip"] diff --git a/README.md b/README.md index 7e23a0a..fd65450 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ as a proxy to access the files securely. ## Self-hosting -I'm currently hosting [local-ip.sh](https://www.local-ip.sh) at [Fly.io](https://fly.io) but you can host the service yourself +I'm currently hosting [local-ip.sh](https://local-ip.sh) at [Fly.io](https://fly.io) but you can host the service yourself if you're into that kind of thing. Note that you will need to edit your domain's glue records so make sure your registrar allows it. You will essentially need to: diff --git a/certs/certs.go b/certs/certs.go index 4aeca48..73e87eb 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -2,6 +2,7 @@ package certs import ( "encoding/json" + "fmt" "log" "os" "strings" @@ -15,14 +16,32 @@ import ( ) type certsClient struct { - legoClient *lego.Client - lastCertificate *certificate.Resource + legoClient *lego.Client + lastWildcardCertificate *certificate.Resource + lastRootCertificate *certificate.Resource } -func (c *certsClient) RequestCertificate() { - log.Println("requesting a certificate") - if c.lastCertificate != nil { - certificates, err := certcrypto.ParsePEMBundle(c.lastCertificate.Certificate) +func (c *certsClient) RequestCertificates() { + c.requestCertificate("wildcard") + c.requestCertificate("root") +} + +func (c *certsClient) requestCertificate(certType string) { + var lastCertificate *certificate.Resource + var domains []string + if certType == "wildcard" { + lastCertificate = c.lastWildcardCertificate + domains = []string{"*.local-ip.sh"} + } else if certType == "root" { + lastCertificate = c.lastRootCertificate + domains = []string{"local-ip.sh"} + } else { + log.Fatalf("Unexpected certType %s. Only \"wildcard\" and \"root\" are supported", certType) + } + + log.Printf("requesting %s certificate\n", certType) + if lastCertificate != nil { + certificates, err := certcrypto.ParsePEMBundle(c.lastWildcardCertificate.Certificate) if err != nil { log.Fatal(err) } @@ -34,45 +53,55 @@ func (c *certsClient) RequestCertificate() { return } - c.renewCertificate() + c.renewCertificates() return } certificates, err := c.legoClient.Certificate.Obtain(certificate.ObtainRequest{ - Domains: []string{"*.local-ip.sh"}, + Domains: domains, Bundle: true, }) if err != nil { log.Fatal(err) } - c.lastCertificate = certificates + if certType == "wildcard" { + c.lastWildcardCertificate = certificates + } else if certType == "root" { + c.lastRootCertificate = certificates + } + + persistFiles(certificates, certType) - persistFiles(certificates) - log.Printf("%#v\n", certificates) } -func (c *certsClient) renewCertificate() { +func (c *certsClient) renewCertificates() { log.Println("renewing currently existing certificate") - certificates, err := c.legoClient.Certificate.Renew(*c.lastCertificate, true, false, "") + wildcardCertificate, err := c.legoClient.Certificate.Renew(*c.lastWildcardCertificate, true, false, "") if err != nil { log.Fatal(err) } + c.lastWildcardCertificate = wildcardCertificate + persistFiles(wildcardCertificate, "wildcard") - c.lastCertificate = certificates + rootCertificate, err := c.legoClient.Certificate.Renew(*c.lastRootCertificate, true, false, "") + if err != nil { + log.Fatal(err) + } + c.lastRootCertificate = rootCertificate + persistFiles(rootCertificate, "root") - persistFiles(certificates) - log.Printf("%#v\n", certificates) } -func persistFiles(certificates *certificate.Resource) { - os.WriteFile("/certs/server.pem", certificates.Certificate, 0o644) - os.WriteFile("/certs/server.key", certificates.PrivateKey, 0o644) +func persistFiles(certificates *certificate.Resource, certType string) { + os.MkdirAll(fmt.Sprintf("/certs/%s", certType), 0o755) + os.WriteFile(fmt.Sprintf("/certs/%s/server.pem", certType), certificates.Certificate, 0o644) + os.WriteFile(fmt.Sprintf("/certs/%s/server.key", certType), certificates.PrivateKey, 0o644) jsonBytes, err := json.MarshalIndent(certificates, "", "\t") if err != nil { log.Fatal(err) } - os.WriteFile("/certs/output.json", jsonBytes, 0o644) + os.WriteFile(fmt.Sprintf("/certs/%s/output.json", certType), jsonBytes, 0o644) } func NewCertsClient(xip *xip.Xip, user *Account) *certsClient { @@ -86,16 +115,18 @@ func NewCertsClient(xip *xip.Xip, user *Account) *certsClient { provider := newProviderLocalIp(xip) legoClient.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers([]string{"1.1.1.1:53", "8.8.8.8:53"}), dns01.DisableCompletePropagationRequirement()) - lastCertificate := getLastCertificate(legoClient) + lastWildcardCertificate := getLastCertificate(legoClient, "wildcard") + lastRootCertificate := getLastCertificate(legoClient, "root") return &certsClient{ legoClient, - lastCertificate, + lastWildcardCertificate, + lastRootCertificate, } } -func getLastCertificate(legoClient *lego.Client) *certificate.Resource { - jsonBytes, err := os.ReadFile("/certs/output.json") +func getLastCertificate(legoClient *lego.Client, certType string) *certificate.Resource { + jsonBytes, err := os.ReadFile(fmt.Sprintf("/certs/%s/output.json", certType)) if err != nil { if strings.Contains(err.Error(), "no such file or directory") { return nil diff --git a/certs/config.go b/certs/config.go index ab4033d..b9577b1 100644 --- a/certs/config.go +++ b/certs/config.go @@ -10,6 +10,7 @@ import ( const ( email = "admin@local-ip.sh" caDirUrl = lego.LEDirectoryProduction + // caDirUrl = lego.LEDirectoryStaging ) var ( diff --git a/fly.toml b/fly.toml index bf8724d..3aeb3da 100644 --- a/fly.toml +++ b/fly.toml @@ -20,14 +20,6 @@ PORT = "53" source = "certs" destination = "/certs" -[http_service] -internal_port = 53 -force_https = true -auto_stop_machines = false -auto_start_machines = true -min_machines_running = 1 -processes = ["app"] - [[services]] protocol = "udp" internal_port = 53 @@ -38,7 +30,33 @@ min_machines_running = 0 [[services.ports]] port = 53 +[[services]] +protocol = "tcp" +internal_port = 80 + +[[services.ports]] +port = 80 + +[[services]] +protocol = "tcp" +internal_port = 443 + +[[services.ports]] +port = 443 + +# [[services.http_checks]] +# interval = 10000 +# grace_period = "30s" +# method = "get" +# path = "/" +# protocol = "https" +# timeout = 15000 +# tls_skip_verify = false +# tls_server_name = "local-ip.sh" +# [services.http_checks.headers] + [[vm]] +size = "shared-cpu-1x" cpu_kind = "shared" cpus = 1 memory_mb = 256 diff --git a/http/server.go b/http/server.go index a2cdf9f..44bb673 100644 --- a/http/server.go +++ b/http/server.go @@ -3,17 +3,61 @@ package http import ( "log" "net/http" + "os" + "strings" + "time" ) -func ServeCertificate() { +func ServeHttp() { + waitForCertificate() + + go http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + url := r.URL + url.Host = r.Host + url.Scheme = "https" + http.Redirect(w, r, url.String(), http.StatusMovedPermanently) + })) + http.HandleFunc("/server.key", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") - http.ServeFile(w, r, "/certs/server.key") + http.ServeFile(w, r, "/certs/wildcard/server.key") }) http.HandleFunc("/server.pem", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-x509-ca-cert") - http.ServeFile(w, r, "/certs/server.pem") + http.ServeFile(w, r, "/certs/wildcard/server.pem") }) - log.Printf("Serving cert files on :9229\n") - http.ListenAndServe(":9229", nil) + http.HandleFunc("/og.png", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=3600") + http.ServeFile(w, r, "./http/static/og.png") + }) + http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/x-icon; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=3600") + http.ServeFile(w, r, "./http/static/favicon.ico") + }) + http.HandleFunc("/styles.css", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + http.ServeFile(w, r, "./http/static/styles.css") + }) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + http.ServeFile(w, r, "./http/static/index.html") + }) + log.Printf("Serving HTTPS server on :443\n") + http.ListenAndServeTLS(":443", "/certs/root/server.pem", "/certs/root/server.key", nil) +} + +func waitForCertificate() { + for { + _, err := os.Stat("/certs/root/output.json") + if err != nil { + if strings.Contains(err.Error(), "no such file or directory") { + time.Sleep(1 * time.Second) + continue + } + log.Fatal(err) + } + break + } } diff --git a/http/static/favicon.ico b/http/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e29f8eeabf2dde9865645688cd267c0d5b5157a5 GIT binary patch literal 5286 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 TR_;SW&jVd;BdvT*n@4{Gvk8qu literal 0 HcmV?d00001 diff --git a/http/static/index.html b/http/static/index.html new file mode 100644 index 0000000..d8af752 --- /dev/null +++ b/http/static/index.html @@ -0,0 +1,89 @@ + + + + + + + local-ip.sh + + + + + + + + + + + + + + + + + + + + +
+
+  _                 _       _            _
+ | |               | |     (_)          | |
+ | | ___   ___ __ _| |      _ _ __   ___| |__
+ | |/ _ \ / __/ _` | |_____| | '_ \ / __| '_ \
+ | | (_) | (_| (_| | |_____| | |_) |\__ \ | | |
+ |_|\___/ \___\__,_|_|     |_| .__(_)___/_| |_|
+                             | |
+                             |_|
+        
+
+
+
+
What is local-ip.sh?
+
+
local-ip.sh is a magic domain name that provides wildcard DNS for any IP address. It is heavily + inspired by local-ip.co, sslip.io, and xip.io.
+
Quick example, say your LAN IP address is 192.168.1.10. Using + local-ip.sh,

+
       192.168.1.10.local-ip.sh resolves to 192.168.1.10
+     dots.192.168.1.10.local-ip.sh resolves to 192.168.1.10
+   dashes.192-168-1-10.local-ip.sh resolves to 192.168.1.10
+
+
...and so on. You can use these domains to access virtual hosts on your development web server + from devices on your local network. No configuration required!
+
The best part is, you can serve your content over HTTPS with our TLS certificate for + *.local-ip.sh:Be aware that wildcard certificates are not recursive, meaning they don't match + "sub-subdomains".
In our case, this certificate will only match subdomains of + local-ip.sh such as 192-168-1-10.local-ip.sh where dashes separate + the numbers that make up the IP address.
+
+
+
+
How does it work?
+
+
+ local-ip.sh runs publicly a custom DNS server. + When your computer looks up a local-ip.sh domain, the local-ip.sh DNS server resolves to the IP address it extracts from the domain. +
+
+ The TLS certificate is obtained from Let's Encrypt and renewed up to a month before it expires. +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/http/static/og.png b/http/static/og.png new file mode 100644 index 0000000000000000000000000000000000000000..d0c178186f12e57e689022d54ed00de3d1f98bae GIT binary patch literal 32442 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 pre { + margin: 1rem auto; +} + +main a { + color: #728ea7; +} + +main a:hover { + background: #7aa6da; + color: #111; + text-decoration: none; +} + +section:nth-child(n + 2) { + margin-top: 3rem; +} + +section > main { + display: flex; + flex-direction: column; + justify-content: space-between; + row-gap: 2rem; +} + +code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 0.95); + border-radius: 3px; + white-space: nowrap; +} + +header span.dim { + color: #728ea7; +} + +section strong { + color: #7aa6da; +} + +footer { + margin: 5rem auto 0; + color: #556a7d; +} + +footer a { + color: inherit; +} + +footer a:hover { + color: #728ea7; +} + +div.cursor { + display: inline-block; + background: #111; + margin-left: 1px; + margin-right: -1px; + animation: blink 2s linear 0s infinite; +} + +@keyframes blink { + 0% { + background: #7aa6da; + } + 47% { + background: #728ea7; + } + 50% { + background: #111; + } + 97% { + background: #111; + } + 100% { + background: #728ea7; + } +} diff --git a/main.go b/main.go index 449f308..9f4d601 100644 --- a/main.go +++ b/main.go @@ -20,16 +20,16 @@ func main() { certsClient := certs.NewCertsClient(n, account) time.Sleep(5 * time.Second) - certsClient.RequestCertificate() + certsClient.RequestCertificates() for { // try to renew certificate every day time.Sleep(24 * time.Hour) - certsClient.RequestCertificate() + certsClient.RequestCertificates() } }() - go http.ServeCertificate() + go http.ServeHttp() n.StartServer() } diff --git a/xip/xip.go b/xip/xip.go index c97969e..68c1aa3 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -23,6 +23,7 @@ type HardcodedRecord struct { TXT *dns.TXT MX []*dns.MX CNAME []*dns.CNAME + SRV *dns.SRV } const ( @@ -54,47 +55,45 @@ var ( }, "local-ip.sh.": { A: []*dns.A{ - {A: net.IPv4(66, 241, 125, 48)}, - }, - AAAA: []*dns.AAAA{ - {AAAA: net.IP{0x2a, 0x09, 0x82, 0x80, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0x1C, 0xC1, 0xC1}}, + // {A: net.IPv4(66, 241, 125, 48)}, + {A: net.IPv4(137, 66, 40, 11)}, // fly.io edge-only ip address }, TXT: &dns.TXT{ Txt: []string{ "sl-verification=frudknyqpqlpgzbglkqnsmorfcvxrf", - "v=spf1 include:simplelogin.co ~all", + "v=spf1 include:capsulecorp.dev ~all", }, }, MX: []*dns.MX{ - {Preference: 10, Mx: "mx1.simplelogin.co."}, - {Preference: 20, Mx: "mx2.simplelogin.co."}, + {Preference: 10, Mx: "email.capsulecorp.dev."}, + }, + }, + "autodiscover.local-ip.sh.": { + CNAME: []*dns.CNAME{ + {Target: "email.capsulecorp.dev"}, + }, + }, + "_autodiscover._tcp.local-ip.sh.": { + SRV: &dns.SRV{ + Target: "email.capsulecorp.dev 443", + }, + }, + "autoconfig.local-ip.sh.": { + CNAME: []*dns.CNAME{ + {Target: "email.capsulecorp.dev"}, }, }, "_dmarc.local-ip.sh.": { TXT: &dns.TXT{ - Txt: []string{"v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"}, + Txt: []string{"v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh"}, }, }, "dkim._domainkey.local-ip.sh.": { - CNAME: []*dns.CNAME{ - {Target: "dkim._domainkey.simplelogin.co."}, - }, - }, - "dkim02._domainkey.local-ip.sh.": { - CNAME: []*dns.CNAME{ - {Target: "dkim02._domainkey.simplelogin.co."}, - }, - }, - "dkim03._domainkey.local-ip.sh.": { - CNAME: []*dns.CNAME{ - {Target: "dkim03._domainkey.simplelogin.co."}, + TXT: &dns.TXT{ + Txt: []string{"v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB"}, }, }, "_acme-challenge.local-ip.sh.": { - // required for fly.io to obtain a certificate for the website - CNAME: []*dns.CNAME{ - {Target: "local-ip.sh.zzkxm3.flydns.net."}, - }, // will be filled in later when requesting the wildcard certificate TXT: &dns.TXT{}, }, @@ -365,7 +364,6 @@ func (xip *Xip) handleDnsRequest(response dns.ResponseWriter, request *dns.Msg) } func (xip *Xip) StartServer() { - log.Printf("Listening on %s\n", xip.server.Addr) err := xip.server.ListenAndServe() defer xip.server.Shutdown() if err != nil { @@ -383,6 +381,7 @@ func (xip *Xip) StartServer() { log.Fatalf("Failed to start server: %s\n ", err.Error()) } + log.Printf("Listening on %s\n", xip.server.Addr) } func NewXip(port int) (xip *Xip) {