Add a configuration file.

This commit is contained in:
Catherine
2025-09-15 05:20:02 +00:00
parent b9a26e528f
commit 11145f407e
9 changed files with 86 additions and 43 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/bin
/data
/config.toml

View File

@@ -21,7 +21,8 @@ You will need [Go](https://go.dev/) 1.24 or newer. Run:
```console
$ mkdir -p data
$ go run . data :3333
$ cp config.toml.example config.toml
$ go run ./src
```
This starts an HTTP server on `0.0.0.0:3333` whose behavior is fully determined by the `data` directory. It will accept requests to any virtual host, but must first be provisioned. For example:

5
config.toml.example Normal file
View File

@@ -0,0 +1,5 @@
data-dir = "./data"
[listen]
protocol = "tcp"
address = ":3333"

1
go.mod
View File

@@ -20,6 +20,7 @@ require (
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
golang.org/x/crypto v0.41.0 // indirect

2
go.sum
View File

@@ -36,6 +36,8 @@ github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7Dmvb
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

27
src/config.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"os"
"github.com/pelletier/go-toml/v2"
)
type Config struct {
DataDir string `toml:"data-dir"`
Listen struct {
Protocol string `toml:"protocol"`
Address string `toml:"address"`
} `toml:"listen"`
}
func readConfig(path string, config *Config) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
decoder := toml.NewDecoder(file)
decoder.DisallowUnknownFields()
return decoder.Decode(config)
}

View File

@@ -37,7 +37,6 @@ func splitHash(hash plumbing.Hash) string {
}
func fetch(
dataDir string,
webRoot string,
repoURL string,
branch string,
@@ -61,10 +60,10 @@ func fetch(
}
head := ref.Hash()
destDir := filepath.Join(dataDir, "tree", splitHash(head))
destDir := filepath.Join(config.DataDir, "tree", splitHash(head))
if _, err := os.Stat(destDir); errors.Is(err, os.ErrNotExist) {
// check out to a temporary directory to avoid TOCTTOU race on destDir
tempDir, err := os.MkdirTemp(dataDir, ".tree")
tempDir, err := os.MkdirTemp(config.DataDir, ".tree")
if err != nil {
return FetchResult{err: fmt.Errorf("mkdir temp: %s", err)}
}
@@ -96,10 +95,10 @@ func fetch(
}
}
webLink := filepath.Join(dataDir, "www", webRoot)
webLink := filepath.Join(config.DataDir, "www", webRoot)
destDirRel, _ := filepath.Rel(filepath.Dir(webLink), destDir)
tempLink := filepath.Join(dataDir,
tempLink := filepath.Join(config.DataDir,
fmt.Sprintf(".link.%s.%s", strings.ReplaceAll(webRoot, "/", ".."), head.String()))
if err := os.Symlink(destDirRel, tempLink); err != nil {
return FetchResult{err: fmt.Errorf("symlink temp: %s", err)}
@@ -131,13 +130,12 @@ func fetch(
}
func Fetch(
dataDir string,
webRoot string,
repoURL string,
branch string,
) FetchResult {
log.Println("fetch:", webRoot, repoURL, branch)
result := fetch(dataDir, webRoot, repoURL, branch)
result := fetch(webRoot, repoURL, branch)
if result.err == nil {
status := ""
switch result.outcome {
@@ -156,7 +154,6 @@ func Fetch(
}
func FetchWithTimeout(
dataDir string,
webRoot string,
repoURL string,
branch string,
@@ -165,7 +162,7 @@ func FetchWithTimeout(
// fetch the updated content with a timeout
c := make(chan FetchResult, 1)
go func() {
result := Fetch(dataDir, webRoot, repoURL, branch)
result := Fetch(webRoot, repoURL, branch)
c <- result
}()
select {

View File

@@ -1,18 +1,29 @@
package main
import (
"flag"
"log"
"net"
"net/http"
"os"
)
func main() {
dataDir := os.Args[1]
listenAddr := os.Args[2]
var config Config
http.HandleFunc("/", Serve(dataDir))
err := http.ListenAndServe(listenAddr, nil)
func main() {
configPath := flag.String("config", "config.toml", "path to configuration file")
flag.Parse()
if err := readConfig(*configPath, &config); err != nil {
log.Fatalln("failed to read configuration:", err)
}
listener, err := net.Listen(config.Listen.Protocol, config.Listen.Address)
if err != nil {
log.Fatalln("failed to listen:", err)
}
http.HandleFunc("/", Serve)
if err := http.Serve(listener, nil); err != nil {
log.Fatalln("failed to serve:", err)
}
}

View File

@@ -19,7 +19,7 @@ import (
const fetchTimeout = 30 * time.Second
func getPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
func getPage(w http.ResponseWriter, r *http.Request) error {
host := getHost(r)
// if the first directory of the path exists under `www/$host`, use it as the root,
@@ -29,26 +29,26 @@ func getPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
requestPath := path
if projectName, projectPath, found := strings.Cut(path, "/"); found {
projectRoot := filepath.Join("www", host, projectName)
if file, _ := securejoin.OpenInRoot(dataDir, projectRoot); file != nil {
if file, _ := securejoin.OpenInRoot(config.DataDir, projectRoot); file != nil {
file.Close()
wwwRoot, requestPath = projectRoot, projectPath
}
}
// try to serve `$root/$path` first
file, err := securejoin.OpenInRoot(dataDir, filepath.Join(wwwRoot, requestPath))
file, err := securejoin.OpenInRoot(config.DataDir, filepath.Join(wwwRoot, requestPath))
if err == nil {
// if it's a directory, serve `$root/$path/index.html`
stat, statErr := file.Stat()
if statErr == nil && stat.IsDir() {
defer file.Close()
file, err = securejoin.OpenInRoot(dataDir,
file, err = securejoin.OpenInRoot(config.DataDir,
filepath.Join(wwwRoot, requestPath, "index.html"))
}
}
// if whatever we were serving doesn't exist, try to serve `$root/404.html`
if errors.Is(err, os.ErrNotExist) {
file, _ = securejoin.OpenInRoot(dataDir, filepath.Join(wwwRoot, "404.html"))
file, _ = securejoin.OpenInRoot(config.DataDir, filepath.Join(wwwRoot, "404.html"))
}
// acquire read capability to the file being served (if possible)
@@ -103,7 +103,7 @@ func getProjectName(w http.ResponseWriter, r *http.Request) (string, error) {
}
}
func putPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
func putPage(w http.ResponseWriter, r *http.Request) error {
host := getHost(r)
err := authorize(w, r)
@@ -130,7 +130,7 @@ func putPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
branch = "pages"
}
result := FetchWithTimeout(dataDir, webRoot, repoURL, branch, fetchTimeout)
result := FetchWithTimeout(webRoot, repoURL, branch, fetchTimeout)
if result.err == nil {
w.Header().Add("Content-Location", r.URL.String())
}
@@ -155,7 +155,7 @@ func putPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
return result.err
}
func postPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
func postPage(w http.ResponseWriter, r *http.Request) error {
host := getHost(r)
err := authorize(w, r)
@@ -199,7 +199,7 @@ func postPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
webRoot := fmt.Sprintf("%s/%s", host, projectName)
repoURL := event["repository"].(map[string]any)["clone_url"].(string)
result := FetchWithTimeout(dataDir, webRoot, repoURL, "pages", fetchTimeout)
result := FetchWithTimeout(webRoot, repoURL, "pages", fetchTimeout)
switch result.outcome {
case FetchError:
w.WriteHeader(http.StatusServiceUnavailable)
@@ -214,23 +214,21 @@ func postPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
return result.err
}
func Serve(dataDir string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("serve:", r.Method, r.Host, r.URL)
err := error(nil)
switch r.Method {
case http.MethodGet:
err = getPage(dataDir, w, r)
case http.MethodPut:
err = putPage(dataDir, w, r)
case http.MethodPost:
err = postPage(dataDir, w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
err = fmt.Errorf("method %s not allowed", r.Method)
}
if err != nil {
log.Println("serve err:", err)
}
func Serve(w http.ResponseWriter, r *http.Request) {
log.Println("serve:", r.Method, r.Host, r.URL)
err := error(nil)
switch r.Method {
case http.MethodGet:
err = getPage(w, r)
case http.MethodPut:
err = putPage(w, r)
case http.MethodPost:
err = postPage(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
err = fmt.Errorf("method %s not allowed", r.Method)
}
if err != nil {
log.Println("serve err:", err)
}
}