commit 7f777dda60a07d1d8d172864a665ef3b1792f763 Author: James Mills <1290234+prologic@users.noreply.github.com> Date: Sun Aug 20 00:44:34 2023 +1000 Initial Commit diff --git a/.dockerfiles/entrypoint.sh b/.dockerfiles/entrypoint.sh new file mode 100755 index 0000000..93f4e47 --- /dev/null +++ b/.dockerfiles/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -e + +if [ "$(id -u)" -eq 0 ]; then + [ -n "${PUID}" ] && usermod -u "${PUID}" nobody + [ -n "${PGID}" ] && groupmod -g "${PGID}" nobody +fi + +printf "Configuring application...\n" +cat > /Caddyfile < + +- [caddy-gitea](#caddy-gitea) + - [Getting started](#getting-started) + - [Caddy config](#caddy-config) + - [DNS config](#dns-config) + - [Gitea config](#gitea-config) + - [gitea-pages repo](#gitea-pages-repo) + - [any repo with configurable allowed branch/tag/commits](#any-repo-with-configurable-allowed-branchtagcommits) + - [any repo with all branches/tags/commits exposed](#any-repo-with-all-branchestagscommits-exposed) + - [Building caddy](#building-caddy) + + + +## Getting started + +### Caddy config + +The Caddyfile below creates a webserver listening on :3000 which will interact with gitea on using `agiteatoken` as the token. + +```Caddyfile +{ + order gitea before file_server +} +:3000 +gitea { + server https://yourgitea.yourdomain.com + token agiteatoken + domain pages.yourdomain.com #this is optional +} +``` + +### DNS config + +This works with a wildcard domain. So you'll need to make a *.pages.yourdomain.com CNAME to the server you'll be running caddy on. +(this doesn't need to be the same server as gitea). + +Depending on the gitea config below you'll be able to access your pages using: + +- (org is the organization or username) +- (org is the organization or username) +- +- +- (if you have created a gitea-pages repo it'll be served on the root) + +### Gitea config + +There are multiple options to expose your repo's as a page, that you can use both at the same time. + +- creating a gitea-pages repo with a gitea-pages branch and a gitea-pages topic +- adding a gitea-pages branch to any repo of choice and a gitea-pages topic +- adding a gitea-pages-allowall topic to your repo (easiest, but less secure) + +#### gitea-pages repo + +e.g. we'll use the `yourorg` org. + +1. create a `gitea-pages` repo in `yourorg` org +2. Add a `gitea-pages` topic to this `gitea-pages` repo (this is used to opt-in your repo), +3. Create a `gitea-pages` branch in this `gitea-pages` repo. +4. Put your content in this branch. (eg file.html) + +Your content will now be available on + +#### any repo with configurable allowed branch/tag/commits + +e.g. we'll use the `yourrepo` repo in the `yourorg` org and there is a `file.html` in the `master` branch and a `otherfile.html` in the `dev` branch. The `master` branch is your default branch. + +1. Add a `gitea-pages` topic to the `yourrepo` repo (this is used to opt-in your repo). +2. Create a `gitea-pages` branch in this `yourrepo` repo. +3. Put a `gitea-pages.toml` file in this `gitea-pages` branch of `yourrepo` repo. (more info about the content below) + +The `gitea-pages.toml` file will contain the git reference (branch/tag/commit) you allow to be exposed. +To allow everything use the example below: + +```toml +allowedrefs=["*"] +``` + +To only allow main and dev: + +```toml +allowedrefs=["main","dev"] +``` + +- Your `file.html` in the `master` branch will now be available on +- Your `file.html` in the `master` branch will now be available on +- Your `otherfile.html` in the `dev` branch will now be available on +- Your `otherfile.html` in the `dev` branch will now be available on + +#### any repo with all branches/tags/commits exposed + +e.g. we'll use the `yourrepo` repo in the `yourorg` org and there is a `file.html` in the `master` branch and a `otherfile.html` in the `dev` branch. The `master` branch is your default branch. + +1. Add a `gitea-pages-allowall` topic to the `yourrepo` repo (this is used to opt-in your repo). + +- Your `file.html` in the `master` branch will now be available on +- Your `file.html` in the `master` branch will now be available on +- Your `otherfile.html` in the `dev` branch will now be available on +- Your `otherfile.html` in the `dev` branch will now be available on + +## Building caddy + +As this is a 3rd party plugin you'll need to build caddy (or use the binaries). +To build with this plugin you'll need to have go1.19 installed. + +```go +go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest #this will install xcaddy in ~/go/bin +~/go/bin/xcaddy build --with github.com/42wim/caddy-gitea@v0.0.4 +``` diff --git a/gitea/frontmatter.go b/gitea/frontmatter.go new file mode 100644 index 0000000..d85cfc2 --- /dev/null +++ b/gitea/frontmatter.go @@ -0,0 +1,180 @@ +// Package gitea ... +// Taken from caddy source code (https://github.com/mholt/caddy/) +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package gitea + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "unicode" + + "github.com/BurntSushi/toml" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/yuin/goldmark" + highlighting "github.com/yuin/goldmark-highlighting/v2" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + gmhtml "github.com/yuin/goldmark/renderer/html" + "gopkg.in/yaml.v3" +) + +func extractFrontMatter(input string) (map[string]any, string, error) { + // get the bounds of the first non-empty line + var firstLineStart, firstLineEnd int + lineEmpty := true + for i, b := range input { + if b == '\n' { + firstLineStart = firstLineEnd + if firstLineStart > 0 { + firstLineStart++ // skip newline character + } + firstLineEnd = i + if !lineEmpty { + break + } + continue + } + lineEmpty = lineEmpty && unicode.IsSpace(b) + } + firstLine := input[firstLineStart:firstLineEnd] + + // ensure residue windows carriage return byte is removed + firstLine = strings.TrimSpace(firstLine) + + // see what kind of front matter there is, if any + var closingFence []string + var fmParser func([]byte) (map[string]any, error) + for _, fmType := range supportedFrontMatterTypes { + if firstLine == fmType.FenceOpen { + closingFence = fmType.FenceClose + fmParser = fmType.ParseFunc + } + } + + if fmParser == nil { + // no recognized front matter; whole document is body + return nil, input, nil + } + + // find end of front matter + var fmEndFence string + fmEndFenceStart := -1 + for _, fence := range closingFence { + index := strings.Index(input[firstLineEnd:], "\n"+fence) + if index >= 0 { + fmEndFenceStart = index + fmEndFence = fence + break + } + } + if fmEndFenceStart < 0 { + return nil, "", fmt.Errorf("unterminated front matter") + } + fmEndFenceStart += firstLineEnd + 1 // add 1 to account for newline + + // extract and parse front matter + frontMatter := input[firstLineEnd:fmEndFenceStart] + fm, err := fmParser([]byte(frontMatter)) + if err != nil { + return nil, "", err + } + + // the rest is the body + body := input[fmEndFenceStart+len(fmEndFence):] + + return fm, body, nil +} + +func yamlFrontMatter(input []byte) (map[string]any, error) { + m := make(map[string]any) + err := yaml.Unmarshal(input, &m) + return m, err +} + +func tomlFrontMatter(input []byte) (map[string]any, error) { + m := make(map[string]any) + err := toml.Unmarshal(input, &m) + return m, err +} + +func jsonFrontMatter(input []byte) (map[string]any, error) { + input = append([]byte{'{'}, input...) + input = append(input, '}') + m := make(map[string]any) + err := json.Unmarshal(input, &m) + return m, err +} + +type parsedMarkdownDoc struct { + Meta map[string]any `json:"meta,omitempty"` + Body string `json:"body,omitempty"` +} + +type frontMatterType struct { + FenceOpen string + FenceClose []string + ParseFunc func(input []byte) (map[string]any, error) +} + +var supportedFrontMatterTypes = []frontMatterType{ + { + FenceOpen: "---", + FenceClose: []string{"---", "..."}, + ParseFunc: yamlFrontMatter, + }, + { + FenceOpen: "+++", + FenceClose: []string{"+++"}, + ParseFunc: tomlFrontMatter, + }, + { + FenceOpen: "{", + FenceClose: []string{"}"}, + ParseFunc: jsonFrontMatter, + }, +} + +func markdown(input []byte) ([]byte, error) { + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, + extension.Footnote, + highlighting.NewHighlighting( + highlighting.WithFormatOptions( + chromahtml.WithClasses(true), + ), + ), + ), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + goldmark.WithRendererOptions( + gmhtml.WithUnsafe(), // TODO: this is not awesome, maybe should be configurable? + ), + ) + + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + + defer bufPool.Put(buf) + + if err := md.Convert(input, buf); err != nil { + return input, err + } + + return buf.Bytes(), nil +} diff --git a/gitea/fs.go b/gitea/fs.go new file mode 100644 index 0000000..01d5908 --- /dev/null +++ b/gitea/fs.go @@ -0,0 +1,93 @@ +package gitea + +import ( + "io" + "io/fs" + "time" +) + +type fileInfo struct { + size int64 + isdir bool + name string +} + +type openFile struct { + content []byte + offset int64 + name string + isdir bool +} + +func (g fileInfo) Name() string { + return g.name +} + +func (g fileInfo) Size() int64 { + return g.size +} + +func (g fileInfo) Mode() fs.FileMode { + return 0o444 +} + +func (g fileInfo) ModTime() time.Time { + return time.Time{} +} + +func (g fileInfo) Sys() any { + return nil +} + +func (g fileInfo) IsDir() bool { + return g.isdir +} + +var _ io.Seeker = (*openFile)(nil) + +func (o *openFile) Close() error { + return nil +} + +func (o *openFile) Stat() (fs.FileInfo, error) { + return fileInfo{ + size: int64(len(o.content)), + isdir: o.isdir, + name: o.name, + }, nil +} + +func (o *openFile) Read(b []byte) (int, error) { + if o.offset >= int64(len(o.content)) { + return 0, io.EOF + } + + if o.offset < 0 { + return 0, &fs.PathError{Op: "read", Path: o.name, Err: fs.ErrInvalid} + } + + n := copy(b, o.content[o.offset:]) + + o.offset += int64(n) + + return n, nil +} + +func (o *openFile) Seek(offset int64, whence int) (int64, error) { + switch whence { + case 0: + offset += 0 + case 1: + offset += o.offset + case 2: + offset += int64(len(o.content)) + } + + if offset < 0 || offset > int64(len(o.content)) { + return 0, &fs.PathError{Op: "seek", Path: o.name, Err: fs.ErrInvalid} + } + + o.offset = offset + + return offset, nil +} diff --git a/gitea/gitea.go b/gitea/gitea.go new file mode 100644 index 0000000..ed07cc0 --- /dev/null +++ b/gitea/gitea.go @@ -0,0 +1,207 @@ +package gitea + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "net/url" + "strings" + "sync" + + "code.gitea.io/sdk/gitea" +) + +const ( + defaultPagesRef = "gitea-pages" + defaultPagesTopic = "gitea-pages" +) + +// Client ... +type Client struct { + cli *gitea.Client + url string + domain string + token string +} + +// NewClient ... +func NewClient(url, token, domain string) (*Client, error) { + cli, err := gitea.NewClient(url, gitea.SetToken(token), gitea.SetGiteaVersion("")) + if err != nil { + return nil, err + } + + return &Client{ + cli: cli, + url: url, + domain: domain, + token: token, + }, nil +} + +// Open ... +func (c *Client) Open(user, path string) (fs.File, error) { + log.Printf("user: %s", user) + log.Printf("path: %s", path) + + var repo string + + parts := strings.Split(strings.TrimLeft(path, "/"), "/") + log.Printf("parts: %d #%v", len(parts), parts) + if len(parts) > 1 { + repo = parts[0] + path = strings.Join(parts[1:], "/") + } else { + repo = fmt.Sprintf("%s.%s", user, c.domain) + } + log.Printf("repo: %s", repo) + + // if path is empty they want to have the index.html + if path == "" { + path = "index.html" + } + + allowed := c.allowsPages(user, repo) + log.Printf("allowed? %t", allowed) + if !allowed && repo != fmt.Sprintf("%s.%s", user, c.domain) { + repo = fmt.Sprintf("%s.%s", user, c.domain) + if !c.allowsPages(user, repo) { + return nil, fs.ErrNotExist + } + } + + res, err := c.getRawFileOrLFS(user, repo, path, defaultPagesRef) + if err != nil { + return nil, err + } + + if strings.HasSuffix(path, ".md") { + res, err = handleMD(res) + if err != nil { + return nil, err + } + } + + return &openFile{ + content: res, + name: path, + }, nil +} + +func (c *Client) getRawFileOrLFS(owner, repo, filepath, ref string) ([]byte, error) { + var ( + giteaURL string + err error + ) + + // TODO: make pr for go-sdk + // gitea sdk doesn't support "media" type for lfs/non-lfs + giteaURL, err = url.JoinPath(c.url+"/api/v1/repos/", owner, repo, "media", filepath) + if err != nil { + return nil, err + } + + giteaURL += "?ref=" + url.QueryEscape(ref) + + req, err := http.NewRequest(http.MethodGet, giteaURL, nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "token "+c.token) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + switch res.StatusCode { + case http.StatusNotFound: + return nil, fs.ErrNotExist + case http.StatusOK: + default: + return nil, fmt.Errorf("unexpected status code '%d'", res.StatusCode) + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + defer res.Body.Close() + + return data, nil +} + +var bufPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + +func handleMD(res []byte) ([]byte, error) { + meta, body, err := extractFrontMatter(string(res)) + if err != nil { + return nil, err + } + + html, err := markdown([]byte(body)) + if err != nil { + return nil, err + } + + title, _ := meta["title"].(string) + + res = append([]byte("\n\n\n

"), []byte(title)...) + res = append(res, []byte("

")...) + res = append(res, html...) + res = append(res, []byte("")...) + + return res, nil +} + +func (c *Client) repoTopics(owner, repo string) ([]string, error) { + repos, _, err := c.cli.ListRepoTopics(owner, repo, gitea.ListRepoTopicsOptions{}) + return repos, err +} + +func (c *Client) hasRepoBranch(owner, repo, branch string) bool { + b, _, err := c.cli.GetRepoBranch(owner, repo, branch) + if err != nil { + return false + } + + return b.Name == branch +} + +func (c *Client) allowsPages(owner, repo string) bool { + topics, err := c.repoTopics(owner, repo) + if err != nil { + log.Printf("error finding topics for %s/%s: %s", owner, repo, err) + return false + } + + for _, topic := range topics { + if topic == defaultPagesTopic { + return true + } + } + + return false +} + +func splitName(name string) (string, string, string) { + parts := strings.Split(name, "/") + + // parts contains: ["owner", "repo", "filepath"] + switch len(parts) { + case 1: + return parts[0], "", "" + case 2: + return parts[0], parts[1], "" + default: + return parts[0], parts[1], strings.Join(parts[2:], "/") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca4de04 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.mills.io/prologic/pages-server + +go 1.20 + +require ( + code.gitea.io/sdk/gitea v0.15.1 + github.com/BurntSushi/toml v1.3.2 + github.com/alecthomas/chroma/v2 v2.2.0 + github.com/yuin/goldmark v1.5.6 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/hashicorp/go-version v1.2.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/stretchr/testify v1.8.3 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..714b027 --- /dev/null +++ b/go.sum @@ -0,0 +1,61 @@ +code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= +code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= +code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA= +github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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 new file mode 100644 index 0000000..5a835ba --- /dev/null +++ b/main.go @@ -0,0 +1,62 @@ +// Package main ... +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + + "git.mills.io/prologic/pages-server/gitea" +) + +// GiteaHandler ... +type GiteaHandler struct { + Client *gitea.Client + + Server string + Domain string + Token string +} + +// Init ... +func (h *GiteaHandler) Init() error { + var err error + h.Client, err = gitea.NewClient(h.Server, h.Token, h.Domain) + + return err +} + +// ServeHTTP ... +func (h *GiteaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // remove the domain if it's set (works fine if it's empty) + user := strings.TrimRight(strings.TrimSuffix(r.Host, h.Domain), ".") + log.Printf("user: %s", user) + + f, err := h.Client.Open(user, r.URL.Path) + if err != nil { + http.Error(w, fmt.Sprintf("error resource not found: %s", err.Error()), http.StatusNotFound) + return + } + + if _, err := io.Copy(w, f); err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +func main() { + handler := &GiteaHandler{ + Domain: os.Getenv("DOMAIN"), + Server: os.Getenv("GITEA_URL"), + Token: os.Getenv("GITEA_TOKEN"), + } + if err := handler.Init(); err != nil { + log.Fatalf("error initializing handler: %s", err) + } + + if err := http.ListenAndServe("0.0.0.0:8000", handler); err != nil { + log.Fatalf("error running server: %s", err) + } +}