rio/main.go

237 lines
5.5 KiB
Go

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)
}
}