10 Commits

Author SHA1 Message Date
Wim
c5a4da5572 Release v0.0.3 2022-11-02 19:00:51 +01:00
Wim
cf44709b69 Update README.md 2022-11-02 18:59:54 +01:00
Wim
f9a7e98702 Add support for gitea-pages-allowall repo topic tag
If a repo has been tagged with gitea-pages-allowall we don't need an
extra gitea-pages branch with a toml configuration.

This makes it easier for repos that can just expose every branch
2022-11-02 18:53:55 +01:00
Wim
e46079d2b1 Add README.md and Dockerfile 2022-11-02 01:08:01 +01:00
Wim
1ed8d43714 Add support for branch.repo.username.hostname
If we have a domain configured eg giteapages.io then besides the normal
user.giteapages.io and org.giteapages.io we also support

repo.user.giteapages.io / repo.org.giteapages.io
branch.repo.user.giteapages.io / branch.repo.org.giteapages.io
2022-11-02 00:00:04 +01:00
Wim
f99aedb6a1 Add support for a gitea-pages repo with gitea-pages branch on the root
If we don't find a repo, we search for the gitea-pages repo if this
exists, has the gitea-pages topic tag and has a gitea-pages branch we
will use this to do a file lookup.

We don't need a gitea-pages.toml config file for this repo.
2022-11-01 23:21:52 +01:00
Wim
1a9f1b8a15 Fix linting 2022-11-01 22:36:56 +01:00
Wim
3ee0dee61b Start using gitea-sdk where possible 2022-11-01 22:36:56 +01:00
Wim
9120ac1d28 Make gitea-pages variable 2022-11-01 22:36:52 +01:00
Wim
9ed3fcd793 Return 404 always on error 2022-11-01 21:21:46 +01:00
6 changed files with 286 additions and 95 deletions

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM caddy:2.6-builder-alpine AS builder
RUN xcaddy build \
--with github.com/42wim/caddy-gitea@v0.0.3
FROM caddy:2.6.2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

118
README.md Normal file
View File

@ -0,0 +1,118 @@
# caddy-gitea
[Gitea](https://gitea.io) plugin for [Caddy v2](https://github.com/caddyserver/caddy).
This allows you to have github pages (with more features) in Gitea.
This also requires you to setup a wildcard CNAME to your gitea host.
<!-- TOC -->
- [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)
<!-- /TOC -->
## Getting started
### Caddy config
The Caddyfile below creates a webserver listening on :3000 which will interact with gitea on <https://yourgitea.yourdomain.com> 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:
- <http://org.pages.yourdomain.com:3000/repo/file.html> (org is the organization or username)
- <http://org.pages.yourdomain.com:3000/repo/file.html?ref=abranch> (org is the organization or username)
- <http://repo.org.pages.yourdomain.com:3000/file.html>
- <http://branch.repo.org.pages.yourdomain.com:3000/file.html>
- <http://org.pages.yourdomain.com:3000/> (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 <http://yourorg.pages.yourdomain.com:3000/file.html>
#### 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 <http://yourorg.pages.yourdomain.com:3000/yourrepo/file.html>
- Your `file.html` in the `master` branch will now be available on <http://yourrepo.yourorg.pages.yourdomain.com:3000/file.html>
- Your `otherfile.html` in the `dev` branch will now be available on <http://yourorg.pages.yourdomain.com:3000/yourrepo/file.html?ref=dev>
- Your `otherfile.html` in the `dev` branch will now be available on <http://dev.yourrepo.yourorg.pages.yourdomain.com:3000/file.html>
#### 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 <http://yourorg.pages.yourdomain.com:3000/yourrepo/file.html>
- Your `file.html` in the `master` branch will now be available on <http://yourrepo.yourorg.pages.yourdomain.com:3000/file.html>
- Your `otherfile.html` in the `dev` branch will now be available on <http://yourorg.pages.yourdomain.com:3000/yourrepo/file.html?ref=dev>
- Your `otherfile.html` in the `dev` branch will now be available on <http://dev.yourrepo.yourorg.pages.yourdomain.com:3000/file.html>
## 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.3
```

View File

@ -1,9 +1,7 @@
package gitea
import (
"errors"
"io"
"io/fs"
"net/http"
"strings"
@ -22,6 +20,7 @@ func init() {
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m Middleware
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
@ -30,6 +29,9 @@ type Middleware struct {
Client *gitea.Client `json:"-"`
Server string `json:"server,omitempty"`
Token string `json:"token,omitempty"`
GiteaPages string `json:"gitea_pages,omitempty"`
GiteaPagesAllowAll string `json:"gitea_pages_allowall,omitempty"`
Domain string `json:"domain,omitempty"`
}
// CaddyModule returns the Caddy module information.
@ -42,9 +44,10 @@ func (Middleware) CaddyModule() caddy.ModuleInfo {
// Provision provisions gitea client.
func (m *Middleware) Provision(ctx caddy.Context) error {
m.Client = gitea.NewClient(m.Server, m.Token)
var err error
m.Client, err = gitea.NewClient(m.Server, m.Token, m.GiteaPages, m.GiteaPagesAllowAll)
return nil
return err
}
// Validate implements caddy.Validator.
@ -53,7 +56,7 @@ func (m *Middleware) Validate() error {
}
// UnmarshalCaddyfile unmarshals a Caddyfile.
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for n := d.Nesting(); d.NextBlock(n); {
switch d.Val() {
@ -61,6 +64,12 @@ func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
d.Args(&m.Server)
case "token":
d.Args(&m.Token)
case "gitea_pages":
d.Args(&m.GiteaPages)
case "gitea_pages_allowall":
d.Args(&m.GiteaPagesAllowAll)
case "domain":
d.Args(&m.Domain)
}
}
}
@ -70,17 +79,31 @@ func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
// ServeHTTP performs gitea content fetcher.
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
h := strings.Split(r.Host, ".")
fp := h[0] + r.URL.Path
// remove the domain if it's set (works fine if it's empty)
host := strings.TrimRight(strings.TrimSuffix(r.Host, m.Domain), ".")
h := strings.Split(host, ".")
f, err := m.Client.Open(fp, r.URL.Query().Get("ref"))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return caddyhttp.Error(http.StatusNotFound, err)
fp := h[0] + r.URL.Path
ref := r.URL.Query().Get("ref")
// if we haven't specified a domain, do not support repo.username and branch.repo.username
if m.Domain != "" {
switch {
case len(h) == 2:
fp = h[1] + "/" + h[0] + r.URL.Path
case len(h) == 3:
fp = h[2] + "/" + h[1] + r.URL.Path
ref = h[0]
}
}
f, err := m.Client.Open(fp, ref)
if err != nil {
return caddyhttp.Error(http.StatusNotFound, err)
}
_, err = io.Copy(w, f)
return err
}

2
go.mod
View File

@ -3,6 +3,7 @@ module github.com/42wim/caddy-gitea
go 1.19
require (
code.gitea.io/sdk/gitea v0.15.1
github.com/BurntSushi/toml v1.2.1
github.com/alecthomas/chroma v0.10.0
github.com/caddyserver/caddy/v2 v2.6.2
@ -45,6 +46,7 @@ require (
github.com/google/cel-go v0.12.5 // indirect
github.com/google/pprof v0.0.0-20221010195024-131d412537ea // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/go-version v1.2.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect

6
go.sum
View File

@ -39,6 +39,9 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
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=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
@ -254,6 +257,8 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c=
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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -837,6 +842,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

View File

@ -2,8 +2,6 @@ package gitea
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
@ -12,47 +10,95 @@ import (
"strings"
"sync"
gclient "code.gitea.io/sdk/gitea"
"github.com/spf13/viper"
)
type Client struct {
serverURL string
token string
giteapages string
giteapagesAllowAll string
gc *gclient.Client
}
type pagesConfig struct {
func NewClient(serverURL, token, giteapages, giteapagesAllowAll string) (*Client, error) {
if giteapages == "" {
giteapages = "gitea-pages"
}
type topicsResponse struct {
Topics []string `json:"topics"`
if giteapagesAllowAll == "" {
giteapagesAllowAll = "gitea-pages-allowall"
}
func NewClient(serverURL, token string) *Client {
return &Client{
serverURL: serverURL,
token: token,
}
}
func (c *Client) Open(name, ref string) (fs.File, error) {
if ref == "" {
ref = "gitea-pages"
}
owner, repo, filepath, err := splitName(name)
gc, err := gclient.NewClient(serverURL, gclient.SetToken(token), gclient.SetGiteaVersion(""))
if err != nil {
return nil, err
}
if !c.allowsPages(owner, repo) {
return &Client{
serverURL: serverURL,
token: token,
gc: gc,
giteapages: giteapages,
giteapagesAllowAll: giteapagesAllowAll,
}, nil
}
func (c *Client) Open(name, ref string) (fs.File, error) {
owner, repo, filepath := splitName(name)
// if repo is empty they want to have the gitea-pages repo
if repo == "" {
repo = c.giteapages
filepath = "index.html"
}
// if filepath is empty they want to have the index.html
if filepath == "" {
filepath = "index.html"
}
// we need to check if the repo exists (and allows access)
limited, allowall := c.allowsPages(owner, repo)
if !limited && !allowall {
// if we're checking the gitea-pages and it doesn't exist, return 404
if repo == c.giteapages && !c.hasRepoBranch(owner, repo, c.giteapages) {
return nil, fs.ErrNotExist
}
// the repo didn't exist but maybe it's a filepath in the gitea-pages repo
// so we need to check if the gitea-pages repo exists
filepath = repo
repo = c.giteapages
if ref == "" {
ref = c.giteapages
}
limited, allowall = c.allowsPages(owner, repo)
if !limited && !allowall || !c.hasRepoBranch(owner, repo, c.giteapages) {
return nil, fs.ErrNotExist
}
}
hasConfig := true
if err := c.readConfig(owner, repo); err != nil {
// we don't need a config for gitea-pages
// no config is only exposing the gitea-pages branch
if repo != c.giteapages && !allowall {
return nil, err
}
if !validRefs(ref) {
hasConfig = false
}
// if we don't have a config and the repo is the gitea-pages
// always overwrite the ref to the gitea-pages branch
if !hasConfig && (repo == c.giteapages || ref == c.giteapages) {
ref = c.giteapages
} else if !validRefs(ref, allowall) {
return nil, fs.ErrNotExist
}
@ -80,6 +126,8 @@ func (c *Client) getRawFileOrLFS(owner, repo, filepath, ref string) ([]byte, err
err error
)
// TODO: make pr for go-sdk
// gitea sdk doesn't support "media" type for lfs/non-lfs
giteaURL, err = url.JoinPath(c.serverURL+"/api/v1/repos/", owner, repo, "media", filepath)
if err != nil {
return nil, err
@ -115,7 +163,6 @@ func (c *Client) getRawFileOrLFS(owner, repo, filepath, ref string) ([]byte, err
defer resp.Body.Close()
return res, nil
}
var bufPool = sync.Pool{
@ -135,7 +182,9 @@ func handleMD(res []byte) ([]byte, error) {
return nil, err
}
res = append([]byte("<!DOCTYPE html>\n<html>\n<body>\n<h1>"), []byte(meta["title"].(string))...)
title, _ := meta["title"].(string)
res = append([]byte("<!DOCTYPE html>\n<html>\n<body>\n<h1>"), []byte(title)...)
res = append(res, []byte("</h1>")...)
res = append(res, resmd...)
res = append(res, []byte("</body></html>")...)
@ -144,85 +193,70 @@ func handleMD(res []byte) ([]byte, error) {
}
func (c *Client) repoTopics(owner, repo string) ([]string, error) {
var (
giteaURL string
err error
)
giteaURL, err = url.JoinPath(c.serverURL+"/api/v1/repos/", owner, repo, "topics")
if err != nil {
return nil, err
repos, _, err := c.gc.ListRepoTopics(owner, repo, gclient.ListRepoTopicsOptions{})
return repos, err
}
req, err := http.NewRequest(http.MethodGet, giteaURL, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "token "+c.token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
res, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
t := topicsResponse{}
json.Unmarshal(res, &t)
return t.Topics, nil
}
func (c *Client) allowsPages(owner, repo string) bool {
topics, err := c.repoTopics(owner, repo)
func (c *Client) hasRepoBranch(owner, repo, branch string) bool {
b, _, err := c.gc.GetRepoBranch(owner, repo, branch)
if err != nil {
return false
}
return b.Name == branch
}
func (c *Client) allowsPages(owner, repo string) (bool, bool) {
topics, err := c.repoTopics(owner, repo)
if err != nil {
return false, false
}
for _, topic := range topics {
if topic == "gitea-pages" {
return true
if topic == c.giteapagesAllowAll {
return true, true
}
}
return false
for _, topic := range topics {
if topic == c.giteapages {
return true, false
}
}
return false, false
}
func (c *Client) readConfig(owner, repo string) error {
cfg, err := c.getRawFileOrLFS(owner, repo, "gitea-pages.toml", "gitea-pages")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
cfg, err := c.getRawFileOrLFS(owner, repo, c.giteapages+".toml", c.giteapages)
if err != nil {
return err
}
viper.SetConfigType("toml")
viper.ReadConfig(bytes.NewBuffer(cfg))
return nil
return viper.ReadConfig(bytes.NewBuffer(cfg))
}
func splitName(name string) (string, string, string, error) {
func splitName(name string) (string, string, string) {
parts := strings.Split(name, "/")
// parts contains: ["owner", "repo", "filepath"]
// return invalid path if not enough parts
if len(parts) < 3 {
return "", "", "", fs.ErrInvalid
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:], "/")
}
}
owner := parts[0]
repo := parts[1]
filepath := strings.Join(parts[2:], "/")
return owner, repo, filepath, nil
func validRefs(ref string, allowall bool) bool {
if allowall {
return true
}
func validRefs(ref string) bool {
validrefs := viper.GetStringSlice("allowedrefs")
for _, r := range validrefs {
if r == ref {