commit 6a7461454c28557deca70abe4795e7be2d0d1341 Author: Alexander "PapaTutuWawa Date: Sun Dec 31 13:26:56 2023 +0100 Initial commit diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..32eaba5 --- /dev/null +++ b/dns.go @@ -0,0 +1,25 @@ +package main + +import ( + "net" + "time" + + "github.com/patrickmn/go-cache" +) + +var ( + cnameCache = cache.New(1 * time.Hour, 1 * time.Hour) +) + +func lookupCNAME(domain string) (string, error) { + cname, found := cnameCache.Get(domain) + if found { + return cname.(string), nil + } + + cname, err := net.LookupCNAME(domain) + if err == nil { + return cname.(string), nil + } + return "", err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5845f58 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module paptutuwawa/rio + +go 1.20 + +require ( + code.gitea.io/sdk/gitea v0.17.0 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/sirupsen/logrus v1.9.3 + github.com/urfave/cli/v2 v2.27.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/hashicorp/go-version v1.5.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43c4b76 --- /dev/null +++ b/go.sum @@ -0,0 +1,48 @@ +code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34= +code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= +github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..65a2530 --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "os" + "fmt" + "net" + "net/http" + "strings" + "time" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" + log "github.com/sirupsen/logrus" +) + +const ( + //PagesBranch "pages" + // TODO: Change back for testing + PagesBranch = "main" +) + +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:] + } + + _, 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, 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){ + 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) + 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 { + giteaUrl := ctx.String("gitea-url") + addr := ctx.String("listen-host") + ":" + ctx.String("listen-port") + domain := ctx.String("pages-domain") + + log.SetLevel(log.DebugLevel) + 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 + } + 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: "pages-domain", + Usage: "The domain on which the server is reachable", + EnvVars: []string{"PAGES_DOMAIN"}, + Required: true, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatalf("Failed to run app: %s", err) + } +} diff --git a/pages.go b/pages.go new file mode 100644 index 0000000..1bbb920 --- /dev/null +++ b/pages.go @@ -0,0 +1,71 @@ +package main + +import ( + "mime" + "net/http" + "strings" + "time" + + "code.gitea.io/sdk/gitea" + "github.com/patrickmn/go-cache" + log "github.com/sirupsen/logrus" +) + +var ( + pageCache = cache.New(6 * time.Hour, 1 * time.Hour) +) + +type PageContentCache struct { + Content []byte + mimeType string +} + +func makePageContentCacheEntry(username, path string) string { + return username + ":" + path +} + +func serveFile(username string, path string, giteaClient *gitea.Client, w http.ResponseWriter) { + // Provide a default + if path == "" { + path = "/index.html" + } + + // Strip away a starting / as it messes with Gitea + if path[:1] == "/" { + path = path[1:] + } + + key := makePageContentCacheEntry(username, path) + entry, found := pageCache.Get(key) + var content []byte + var mimeType string + if found { + log.Debugf("Returning %s from cache", path) + content = entry.(PageContentCache).Content + mimeType = entry.(PageContentCache).mimeType + } else { + content, _, err := giteaClient.GetFile(username, "pages", PagesBranch, path, false) + if err != nil { + log.Errorf("Failed to get file %s (%s)", path, err) + w.WriteHeader(404) + return + } + + pathParts := strings.Split(path, ".") + ext := pathParts[len(pathParts) - 1] + mimeType := mime.TypeByExtension("." + ext) + + pageCache.Set( + key, + PageContentCache{ + content, + mimeType, + }, + cache.DefaultExpiration, + ) + } + + w.WriteHeader(200) + w.Header().Set("Content-Type", mimeType) + w.Write(content) +} diff --git a/repo.go b/repo.go new file mode 100644 index 0000000..f689b09 --- /dev/null +++ b/repo.go @@ -0,0 +1,110 @@ +package main + +import ( + "errors" + "strings" + "time" + + "code.gitea.io/sdk/gitea" + "github.com/patrickmn/go-cache" + log "github.com/sirupsen/logrus" +) + +var ( + pathCache = cache.New(1 * time.Hour, 1 * time.Hour) +) + + +type PageCacheEntry struct { + Repository *gitea.Repository + Path string +} + +func makePageCacheKey(domain, path string) string { + return domain + "/" + path +} + +func lookupRepositoryAndCache(username, reponame, host, domain, path, cname string, giteaClient *gitea.Client) (*gitea.Repository, error) { + log.Debugf("Looking up repository %s/%s", username, reponame) + repo, _, err := giteaClient.GetRepo(username, reponame) + if err != nil { + return nil, err + } + + // Check if the CNAME file matches + if cname != "" { + file, _, err := giteaClient.GetFile( + username, + repo.Name, + PagesBranch, + "CNAME", + false, + ) + if err != nil { + log.Errorf("Could not verify CNAME of %s/%s: %v\n", username, repo.Name, err) + return nil, err + } + + cnameContent := strings.Trim( + string(file[:]), + "\n", + ) + if cnameContent != cname { + return nil, errors.New("CNAME mismatch") + } + } + + // Cache data + cnameCache.Set(host, domain, cache.DefaultExpiration) + pathCache.Set( + makePageCacheKey(domain, path), + PageCacheEntry{ + repo, + path, + }, + cache.DefaultExpiration, + ) + return repo, nil +} + +func RepoFromPath(username, host, cname, path string, giteaClient *gitea.Client) (*gitea.Repository, string, error) { + domain := host + + // Guess the repository + key := makePageCacheKey(domain, path) + entry, found := pathCache.Get(key) + if found { + pageEntry := entry.(PageCacheEntry) + return pageEntry.Repository, pageEntry.Path, nil + } + + pathParts := strings.Split(path, "/") + if len(pathParts) > 1 { + log.Debugf("Trying repository %s", pathParts[0]) + modifiedPath := strings.Join(pathParts[1:], "/") + repo, err := lookupRepositoryAndCache( + username, + pathParts[0], + host, + domain, + modifiedPath, + cname, + giteaClient, + ) + if err == nil { + return repo, modifiedPath, nil + } + } + + log.Debugf("Trying repository pages") + repo, err := lookupRepositoryAndCache( + username, + "pages", + host, + domain, + path, + cname, + giteaClient, + ) + return repo, path, err +}