This repository has been archived on 2024-12-14. You can view files and clone it, but cannot push or open issues or pull requests.
caddy-gitea/pkg/gitea/gitea.go

364 lines
7.4 KiB
Go

package gitea
import (
"bytes"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"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 dirEntry struct {
name string
isDir bool
content []byte
}
func (d *dirEntry) Name() string { return d.name }
func (d *dirEntry) IsDir() bool { return d.isDir }
func (d *dirEntry) Type() fs.FileMode { return fs.ModePerm }
func (d *dirEntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist }
type openDir struct {
entries []fs.DirEntry
name string
pos int
}
func (d *openDir) Read([]byte) (int, error) {
return 0, fmt.Errorf("cannot Read() a directory")
}
func (d *openDir) Close() error {
return nil
}
func (d *openDir) Stat() (fs.FileInfo, error) {
return nil, fs.ErrNotExist
}
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
n := len(d.entries) - d.pos
if count > 0 && n > count {
n = count
}
if n == 0 {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
entries := d.entries[d.pos : d.pos+n]
d.pos += n
return entries, nil
}
func (d *openDir) ReadDirFile(count int) ([]fs.DirEntry, error) {
return d.ReadDir(count)
}
func NewClient(serverURL, token, giteapages, giteapagesAllowAll string) (*Client, error) {
if giteapages == "" {
giteapages = "gitea-pages"
}
if giteapagesAllowAll == "" {
giteapagesAllowAll = "gitea-pages-allowall"
}
gc, err := gclient.NewClient(serverURL, gclient.SetToken(token), gclient.SetGiteaVersion(""))
if err != nil {
return nil, err
}
return &Client{
serverURL: serverURL,
token: token,
gc: gc,
giteapages: giteapages,
giteapagesAllowAll: giteapagesAllowAll,
}, nil
}
const defaultNotFoundPage = `<!DOCTYPE html>
<html>
<head>
<title>404 - Not Found</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; }
h1 { font-size: 48px; margin-bottom: 20px; }
p { color: #666; }
</style>
</head>
<body>
<h1>404</h1>
<p>The page you're looking for could not be found.</p>
</body>
</html>`
func (c *Client) serveNotFound(owner, repo string) fs.File {
if repo != "" {
custom404, err := c.getRawFileOrLFS(owner, repo, "404.html", "main")
if err == nil {
return &openFile{
content: custom404,
name: "404.html",
}
}
}
return &openFile{
content: []byte(defaultNotFoundPage),
name: "404.html",
}
}
func (c *Client) Open(name, ref string) (fs.File, error) {
owner, repo, filepath := splitName(name)
if repo == "" {
repo = owner + ".fluffy.pw"
filepath = "index.html"
}
isFluffyPagesRepo := strings.HasSuffix(repo, ".fluffy.pw")
_, resp, err := c.gc.GetRepo(owner, repo)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return c.serveNotFound(owner, repo), nil
}
return nil, fs.ErrNotExist
}
limited, allowall := c.allowsPages(owner, repo)
if !limited && !allowall && !isFluffyPagesRepo {
return c.serveNotFound(owner, repo), nil
}
hasConfig := true
if err := c.readConfig(owner, repo); err != nil {
if !isFluffyPagesRepo && !allowall {
return c.serveNotFound(owner, repo), nil
}
hasConfig = false
}
if !hasConfig && !validRefs(ref, allowall || isFluffyPagesRepo) {
return c.serveNotFound(owner, repo), nil
}
entries, err := c.getDirectoryContents(owner, repo, filepath, ref)
if err == nil {
return &openDir{
entries: entries,
name: filepath,
}, nil
}
res, err := c.getRawFileOrLFS(owner, repo, filepath, ref)
if err != nil {
if err == fs.ErrNotExist {
return c.serveNotFound(owner, repo), nil
}
return nil, err
}
if strings.HasSuffix(filepath, ".md") {
res, err = handleMD(res)
if err != nil {
return nil, err
}
}
return &openFile{
content: res,
name: filepath,
}, 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.serverURL+"/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)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusNotFound:
return nil, fs.ErrNotExist
case http.StatusOK:
default:
return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
}
res, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return res, nil
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func handleMD(res []byte) ([]byte, error) {
meta, resbody, err := extractFrontMatter(string(res))
if err != nil {
return nil, err
}
resmd, err := markdown([]byte(resbody))
if err != nil {
return nil, err
}
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>")...)
return res, nil
}
func (c *Client) repoTopics(owner, repo string) ([]string, error) {
repos, _, err := c.gc.ListRepoTopics(owner, repo, gclient.ListRepoTopicsOptions{})
return repos, err
}
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) {
if strings.HasSuffix(repo, ".fluffy.pw") {
return true, true
}
topics, err := c.repoTopics(owner, repo)
if err != nil {
return false, false
}
for _, topic := range topics {
if topic == c.giteapagesAllowAll {
return true, true
}
}
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, c.giteapages+".toml", c.giteapages)
if err != nil {
return err
}
viper.SetConfigType("toml")
return viper.ReadConfig(bytes.NewBuffer(cfg))
}
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:], "/")
}
}
func validRefs(ref string, allowall bool) bool {
if allowall {
return true
}
validrefs := viper.GetStringSlice("allowedrefs")
for _, r := range validrefs {
if r == ref {
return true
}
if r == "*" {
return true
}
}
return false
}
func (c *Client) getDirectoryContents(owner, repo, dirPath, ref string) ([]fs.DirEntry, error) {
entries, _, err := c.gc.ListContents(owner, repo, ref, dirPath)
if err != nil {
return nil, fs.ErrNotExist
}
var result []fs.DirEntry
for _, entry := range entries {
result = append(result, &dirEntry{
name: entry.Name,
isDir: entry.Type == "dir",
})
}
return result, nil
}