package main import ( "crypto/tls" "os" "fmt" "net" "net/http" "strings" "time" "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v2" "github.com/go-acme/lego/v4/challenge/http01" log "github.com/sirupsen/logrus" ) const ( PagesBranch = "pages" ) func handleSubdomain(domain string, cname string, path string, giteaClient *gitea.Client, w http.ResponseWriter) { hostParts := strings.Split(domain, ".") username := hostParts[0] // Strip the leading / if path[:1] == "/" { path = path[1:] } repo, path, err := RepoFromPath( username, domain, cname, path, giteaClient, ) if err != nil { log.Errorf("Failed to get repo: %s", err) w.WriteHeader(404) return } serveFile(username, repo.Name, path, giteaClient, w) } func Handler(pagesDomain string, giteaClient *gitea.Client) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Server", "rio") if strings.HasSuffix(req.Host, pagesDomain){ if handleLetsEncryptChallenge(w, req) { return } log.Debug("Domain can be directly handled") handleSubdomain(req.Host, "", req.URL.Path, giteaClient, w) return } cname, err := lookupCNAME(req.Host) if err != nil { log.Warningf("CNAME request failed, we don't handle %s", req.Host) w.WriteHeader(400) return } log.Debugf("Got CNAME %s", cname) if strings.HasSuffix(cname, pagesDomain) { log.Debugf("%s is alias of %s", req.Host, cname) if handleLetsEncryptChallenge(w, req) { return } log.Debugf("Domain can be handled after a CNAME query") handleSubdomain(cname, cname, req.URL.Path, giteaClient, w) return } log.Errorf("Not handling %s", req.Host) w.WriteHeader(404) } } func runServer(ctx *cli.Context) error { log.SetLevel(log.DebugLevel) giteaUrl := ctx.String("gitea-url") addr := ctx.String("listen-host") + ":" + ctx.String("listen-port") domain := ctx.String("pages-domain") certsFile := ctx.String("certs-file") acmeEmail := ctx.String("acme-email") acmeServer := ctx.String("acme-server") acmeFile := ctx.String("acme-file") acmeHost := ctx.String("acme-host") acmePort := ctx.String("acme-port") err := LoadCertificateStoreFromFile(certsFile) if err != nil { log.Debugf("Generating cert") err := InitialiseFallbackCert(domain) if err != nil { log.Fatalf("Failed to generate fallback certificate: %v", err) return err } FlushCertificateStoreToFile(certsFile) log.Debug("Certificate wrote to disk") } else { log.Debug("Certificate store read from disk") } // Create an ACME client, if we failed to load one acmeClient, err := ClientFromFile(acmeFile, acmeServer) if err != nil { log.Warn("Failed to load ACME client data from disk. Generating new account") acmeClient, err = GenerateNewAccount(acmeEmail, acmeFile, acmeServer) if err != nil { log.Fatalf("Failed to generate new ACME client: %v", err) return err } log.Info("ACME account registered") } else { log.Info("ACME client data read from disk") } // Set up the HTTP01 listener err = acmeClient.Challenge.SetHTTP01Provider( http01.NewProviderServer(acmeHost, acmePort), ) if err != nil { log.Fatalf("Failed to setup HTTP01 challenge listener: %v", err) return err } // Setup the HTTPS stuff httpClient := http.Client{Timeout: 10 * time.Second} client, err := gitea.NewClient( giteaUrl, gitea.SetHTTPClient(&httpClient), gitea.SetToken(""), gitea.SetUserAgent("rio"), ) listener, err := net.Listen("tcp", addr) if err != nil { fmt.Errorf("Failed to create listener: %v", err) return err } tlsConfig := makeTlsConfig( domain, certsFile, acmeClient, ) listener = tls.NewListener(listener, tlsConfig) if err := http.Serve(listener, Handler(domain, client)); err != nil { fmt.Printf("Listening failed") return err } return nil } func main() { app := &cli.App{ Action: runServer, Flags: []cli.Flag{ &cli.StringFlag{ Name: "gitea-url", Usage: "The (HTTPS) URL to the serving Gitea instance", EnvVars: []string{"GITEA_URL"}, Required: true, }, &cli.StringFlag{ Name: "listen-host", Usage: "The host to listen on", EnvVars: []string{"HOST"}, Value: "127.0.0.1", }, &cli.StringFlag{ Name: "listen-port", Usage: "The port to listen on", EnvVars: []string{"PORT"}, Value: "8888", }, &cli.StringFlag{ Name: "acme-host", Usage: "The host to bind to for ACME challenges", EnvVars: []string{"ACME_HOST"}, Value: "", }, &cli.StringFlag{ Name: "acme-port", Usage: "The port to listen on for ACME challenges", EnvVars: []string{"ACME_PORT"}, Value: "8889", }, &cli.StringFlag{ Name: "pages-domain", Usage: "The domain on which the server is reachable", EnvVars: []string{"PAGES_DOMAIN"}, Required: true, }, &cli.StringFlag{ Name: "certs-file", Usage: "File to store certificates in", EnvVars: []string{"CERTS_FILE"}, Required: true, }, &cli.StringFlag{ Name: "acme-file", Usage: "File to store ACME configuration in", EnvVars: []string{"ACME_FILE"}, Required: true, }, &cli.StringFlag{ Name: "acme-email", Usage: "Email to use for an ACME account", EnvVars: []string{"ACME_EMAIL"}, Required: true, }, &cli.StringFlag{ Name: "acme-server", Usage: "CA Directory to use", EnvVars: []string{"ACME_SERVER"}, Value: "https://acme-staging-v02.api.letsencrypt.org/directory", }, }, } if err := app.Run(os.Args); err != nil { log.Fatalf("Failed to run app: %s", err) } }