diff --git a/.github/workflows/renew-cert.yml b/.github/workflows/renew-cert.yml new file mode 100644 index 0000000..a6c0e70 --- /dev/null +++ b/.github/workflows/renew-cert.yml @@ -0,0 +1,43 @@ +name: Renew Certificate + +on: + schedule: + - cron: '0 6 1 * *' # First day of every month at 6am UTC + workflow_dispatch: # Manual trigger + +jobs: + renew: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install lego + run: | + curl -sL https://github.com/go-acme/lego/releases/latest/download/lego_linux_amd64.tar.gz | tar xz + sudo mv lego /usr/local/bin/ + + - name: Renew certificate + env: + CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }} + run: | + lego --email="herrera.monterroso.mario@gmail.com" \ + --dns cloudflare \ + --domains="local.labelctl.dev" \ + --accept-tos \ + --path="./.lego" \ + run + + cp .lego/certificates/local.labelctl.dev.crt certs/server.crt + cp .lego/certificates/local.labelctl.dev.key certs/server.key + + - name: Commit updated certificate + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add certs/server.crt certs/server.key + if git diff --cached --quiet; then + echo "No certificate changes" + else + git commit -m "chore: renew Let's Encrypt certificate for local.labelctl.dev" + git push + fi diff --git a/.gitignore b/.gitignore index 63a3383..5e0048b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ tsc-bridge.exe *.exe .DS_Store .ai-sessions/ +.lego/ diff --git a/certs/server.crt b/certs/server.crt new file mode 100644 index 0000000..1b1d895 --- /dev/null +++ b/certs/server.crt @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDhjCCAw2gAwIBAgISBfRSqev+aldQsYsS6WahZreFMAoGCCqGSM49BAMDMDIx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF +ODAeFw0yNjAzMDgyMDQxMjFaFw0yNjA2MDYyMDQxMjBaMB0xGzAZBgNVBAMTEmxv +Y2FsLmxhYmVsY3RsLmRldjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABK/KfWZb +zQ1KDKZS82OdTj/Ny9hLTYlM/YYvmFQfsbyUDynmPfk5CsxFDvMpy61Iu6Ywb1s1 +3VA2jGteuFMyKQKjggIWMIICEjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI +KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUKPIkRGWqb3PFOEtNAnNv +YsJZjX4wHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNykcowMgYIKwYBBQUH +AQEEJjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5jci5vcmcvMB0GA1Ud +EQQWMBSCEmxvY2FsLmxhYmVsY3RsLmRldjATBgNVHSAEDDAKMAgGBmeBDAECATAs +BgNVHR8EJTAjMCGgH6AdhhtodHRwOi8vZTguYy5sZW5jci5vcmcvNi5jcmwwggEF +BgorBgEEAdZ5AgQCBIH2BIHzAPEAdgDLOPcViXyEoURfW8Hd+8lu8ppZzUcKaQWF +sMsUwxRY5wAAAZzPZG32AAAEAwBHMEUCIQDObDO1HnbH4udWbsCvUMSUs2NsYIk4 +1QwsaxkqMPGfIAIgD7QrqqJMcG42XDD1QNV8dx+n2zHse4sBMihl/fYsnj0AdwCW +l2S/VViXrfdDh2g3CEJ36fA61fak8zZuRqQ/D8qpxgAAAZzPZG4PAAAEAwBIMEYC +IQCRu6reTOB+B2IugxQeRYXJY1BzepdoUq9kuhT8/ZmNiwIhAKzLVMmmSFUg6irJ +a6yvZarNUIgFO+eJqJSYl+A9gmgqMAoGCCqGSM49BAMDA2cAMGQCMEYmOjHwswRn +ee3mm57saH0FxdMpevPWh1mphqBMt417QfKow6YillWN/YP4R7lIPgIwBeZ6UT6l +lYd9Ub2tJn7eLOKDehD+4hKNzwgHGn3OUspApf57lfPkwLjAWWjP12vm +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy +Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa +Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF +bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c +S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb +R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB +9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB +MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j +cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB +BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE +DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j +ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0 +RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d +AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8 +otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA +aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm +Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2 +HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1 +Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR +xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d +tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/ +jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS +u1igv3OefnWjSQ== +-----END CERTIFICATE----- diff --git a/certs/server.key b/certs/server.key new file mode 100644 index 0000000..cd8b101 --- /dev/null +++ b/certs/server.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP94a9FyoPzqzqLt3Cgd4HF/FjsSHPckdpxM+/C3FZxMoAoGCCqGSM49 +AwEHoUQDQgAEr8p9ZlvNDUoMplLzY51OP83L2EtNiUz9hi+YVB+xvJQPKeY9+TkK +zEUO8ynLrUi7pjBvWzXdUDaMa164UzIpAg== +-----END EC PRIVATE KEY----- diff --git a/certs_embed.go b/certs_embed.go new file mode 100644 index 0000000..523efa9 --- /dev/null +++ b/certs_embed.go @@ -0,0 +1,28 @@ +package main + +import ( + "crypto/tls" + _ "embed" + "log" +) + +//go:embed certs/server.crt +var embeddedCert []byte + +//go:embed certs/server.key +var embeddedKey []byte + +// loadEmbeddedCert returns the embedded Let's Encrypt certificate for local.labelctl.dev. +// Returns nil if the embedded cert is missing or invalid. +func loadEmbeddedCert() *tls.Certificate { + if len(embeddedCert) == 0 || len(embeddedKey) == 0 { + return nil + } + cert, err := tls.X509KeyPair(embeddedCert, embeddedKey) + if err != nil { + log.Printf("[tls] Embedded cert invalid: %v — falling back to self-signed", err) + return nil + } + log.Printf("[tls] Using embedded Let's Encrypt certificate for local.labelctl.dev") + return &cert +} diff --git a/go.mod b/go.mod index 9b11943..3cb1c76 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,9 @@ require ( github.com/boombuler/barcode v1.1.0 github.com/signintech/gopdf v0.36.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 github.com/xuri/excelize/v2 v2.9.0 + golang.org/x/image v0.18.0 ) require ( @@ -17,12 +19,10 @@ require ( github.com/pkg/errors v0.8.1 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect - github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 // indirect github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/image v0.18.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index ca8024f..863dd55 100644 --- a/go.sum +++ b/go.sum @@ -33,15 +33,15 @@ github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQ github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index c7b35d8..d4e06ca 100644 --- a/main.go +++ b/main.go @@ -749,19 +749,25 @@ func startServers(port string) error { }() } - // HTTPS server with auto-generated certs (also on dedicated thread) + // HTTPS server — try embedded Let's Encrypt cert first, fallback to self-signed httpsPort := fmt.Sprintf("%d", portInt(port)+1) httpsAddr := "127.0.0.1:" + httpsPort - certFile, keyFile, caFile, err := ensureCerts(defaultHostname) - if err != nil { - log.Printf("[tls] Could not generate certs: %v — HTTPS disabled", err) - return nil // HTTP is running, HTTPS is optional - } - go installCACert(caFile) - tlsCert, err := loadCertWithCA(certFile, keyFile, caFile) - if err != nil { - log.Printf("[tls] Could not load certs: %v — HTTPS disabled", err) - return nil + var tlsCert tls.Certificate + if embedded := loadEmbeddedCert(); embedded != nil { + tlsCert = *embedded + } else { + certFile, keyFile, caFile, err := ensureCerts(defaultHostname) + if err != nil { + log.Printf("[tls] Could not generate certs: %v — HTTPS disabled", err) + return nil + } + go installCACert(caFile) + loaded, err := loadCertWithCA(certFile, keyFile, caFile) + if err != nil { + log.Printf("[tls] Could not load certs: %v — HTTPS disabled", err) + return nil + } + tlsCert = loaded } httpsLn, err := net.Listen("tcp", httpsAddr) if err != nil { diff --git a/tls.go b/tls.go index 7a68f84..4ddc9b0 100644 --- a/tls.go +++ b/tls.go @@ -18,7 +18,7 @@ import ( "time" ) -const defaultHostname = "myprinter.com" +const defaultHostname = "local.labelctl.dev" // certDir returns the directory for TLS certificates. func certDir() string { @@ -101,7 +101,7 @@ func ensureCerts(hostname string) (certFile, keyFile, caFile string, err error) Organization: []string{"TSC Bridge"}, CommonName: hostname, }, - DNSNames: []string{hostname, "localhost", "tsc-bridge", "myprinter.com"}, + DNSNames: []string{hostname, "localhost", "local.labelctl.dev"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, NotBefore: time.Now(), NotAfter: time.Now().Add(397 * 24 * time.Hour), // Max 398 days for Apple/Chrome compliance