From 9e3aefc52560c981fd02730b01e0309be9d84de1 Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Wed, 28 Jan 2026 15:30:27 -0700 Subject: [PATCH] init --- README.md | 10 +++ go.mod | 3 + go.sum | 0 main.go | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..940bb23 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# docs.brut.systems + +This program acts as a proxy to serve files directly from repositories. + +It expects the following environment variables to be set: + +- `API_URL` - Base Forgejo API url (ex. `https://git.brut.systems`) +- `API_TOKEN` - Forgejo API authorization token +- `REDIRECT_URL` - Redirect url when access fails or files do not exist (ex. `https://git.brut.systems`) +- `PORT` - Port to listen on diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ed6904b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.brut.systems/judah/docs.brut.systems + +go 1.25.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go new file mode 100644 index 0000000..0c6bf3e --- /dev/null +++ b/main.go @@ -0,0 +1,180 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" +) + +var ( + API_URL string + API_TOKEN string + REDIRECT_URL string + PORT string +) + +type Server struct { + http.ServeMux +} + +func (sv *Server) getfile(user, repo, path string) ([]byte, error) { + var ( + endpoint = endpoint("/repos/%s/%s/contents/%s", user, repo, url.PathEscape(path)) + ) + + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return nil, err + } + + req.Header.Add("accept", "application/json") + req.Header.Add("Authorization", API_TOKEN) + + log.Printf("GET %q", endpoint) + + var client http.Client + res, err := client.Do(req) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("response had unexpected status %d - %s", res.StatusCode, string(body)) + } + + var response struct { + Encoding string `json:"encoding"` + Content string `json:"content"` + } + + err = json.Unmarshal(body, &response) + if err != nil { + return nil, err + } + + if response.Encoding != "base64" { + return nil, fmt.Errorf("expected base64 encoding, found %q", response.Encoding) + } + + file, err := base64.StdEncoding.DecodeString(response.Content) + if err != nil { + return nil, err + } + + return file, nil +} + +func main() { + var err error + if API_URL, err = getvar("API_URL"); err != nil { + log.Fatal(err) + } else { + if _, err := url.Parse(API_URL); err != nil { + log.Fatalf("invalid API_URL %q", err) + } + + if !strings.HasPrefix(API_URL, "http") { + log.Fatalf("invalid API_URL %q - must being with http(s)://", API_URL) + } + } + + if REDIRECT_URL, err = getvar("REDIRECT_URL"); err != nil { + log.Fatal(err) + } else { + if _, err := url.Parse(REDIRECT_URL); err != nil { + log.Fatalf("invalid REDIRECT_URL %q", err) + } + + if !strings.HasPrefix(REDIRECT_URL, "http") { + log.Fatalf("invalid REDIRECT_URL %q - must begin with \"http(s)://\"", REDIRECT_URL) + } + } + + if token, err := getvar("API_TOKEN"); err != nil { + log.Fatal(err) + } else { + API_TOKEN = "token " + token + } + + if PORT, err = getvar("PORT"); err != nil { + log.Fatal(err) + } else { + if _, err = strconv.ParseInt(PORT, 10, 64); err != nil { + log.Fatalf("invalid port number %q", PORT) + } + } + + redirect := func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, REDIRECT_URL, http.StatusTemporaryRedirect) + } + + sv := new(Server) + sv.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + redirect(w, r) + }) + + sv.HandleFunc("/{user}/{repo}/", func(w http.ResponseWriter, r *http.Request) { + user := strings.TrimSpace(r.PathValue("user")) + repo := strings.TrimSpace(r.PathValue("repo")) + + if len(user) == 0 || len(repo) == 0 { + redirect(w, r) + return + } + + path := r.URL.Path + path = strings.TrimSpace(path[strings.Index(path, repo)+len(repo):]) + path = strings.TrimPrefix(path, "/") + + if len(path) == 0 { + path = "docs/index.html" + } + + log.Printf("fetching file content %s:%s - %q", user, repo, path) + + content, err := sv.getfile(user, repo, path) + if err != nil { + log.Println(err) + redirect(w, r) + return + } + + if _, err := w.Write(content); err != nil { + log.Println(err) + redirect(w, r) + return + } + }) + + addr := fmt.Sprintf(":%s", PORT) + log.Printf("Listening at %s", addr) + + if err := http.ListenAndServe(addr, sv); err != nil { + log.Fatal(err) + } +} + +func getvar(name string) (string, error) { + envvar := strings.TrimSpace(os.Getenv(name)) + if len(envvar) == 0 { + return "", fmt.Errorf("required environment variable %q was not set", name) + } + return envvar, nil +} + +func endpoint(endpoint string, args ...any) string { + url, _ := url.JoinPath(API_URL, "api/v1", fmt.Sprintf(endpoint, args...)) + return url +}