fierj
PublicTiny personal git forge
package main
import (
"context"
"embed"
"html/template"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
//go:embed templates/*.html
var templateFS embed.FS
type contextKey string
func main() {
if len(os.Args) >= 2 && os.Args[1] == "hook" {
os.Exit(hookCmd(os.Args[2:]))
}
cfgPath := "config.json"
if len(os.Args) > 1 {
cfgPath = os.Args[1]
}
cfg := LoadConfig(cfgPath)
os.MkdirAll(cfg.Dir, 0755)
tmpl := template.Must(template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.html"))
// Load users and cookie secret for auth.
var users UserStore
var cookieSecret []byte
if cfg.UsersPath != "" && cfg.CookieSecret != "" {
var err error
users, err = LoadUsers(cfg.UsersPath)
if err != nil {
slog.Error("failed to load users", "path", cfg.UsersPath, "error", err)
os.Exit(1)
}
cookieSecret = []byte(cfg.CookieSecret)
if len(users) > 0 {
slog.Info("auth enabled", "users", len(users))
} else {
slog.Info("auth enabled, waiting for first-run setup")
}
}
mux := http.NewServeMux()
mux.HandleFunc("GET /", Repos(cfg, tmpl))
mux.HandleFunc("GET /new", NewRepoGet(tmpl))
mux.HandleFunc("POST /new", NewRepoPost(cfg, tmpl))
mux.HandleFunc("GET /setup", SetupGet(tmpl))
mux.HandleFunc("POST /setup", SetupPost(users, cfg.UsersPath, cookieSecret, tmpl))
mux.HandleFunc("GET /login", LoginGet(tmpl))
mux.HandleFunc("POST /login", LoginPost(users, cookieSecret, tmpl))
mux.HandleFunc("POST /logout", Logout())
mux.HandleFunc("GET /users", UsersGet(users, cfg.UsersPath, tmpl))
mux.HandleFunc("POST /users", UsersPost(users, cfg.UsersPath, tmpl))
mux.HandleFunc("GET /{repo}", Tree(cfg, tmpl))
mux.HandleFunc("GET /{repo}/tree/{ref}/{path...}", Tree(cfg, tmpl))
mux.HandleFunc("GET /{repo}/blob/{ref}/{path...}", Blob(cfg, tmpl))
mux.HandleFunc("GET /{repo}/log/{ref}", Log(cfg, tmpl))
mux.HandleFunc("GET /{repo}/commit/{ref}", Diff(cfg, tmpl))
mux.HandleFunc("GET /{repo}/refs/", Refs(cfg, tmpl))
mux.HandleFunc("GET /{repo}/settings", SettingsGet(cfg, tmpl))
mux.HandleFunc("POST /{repo}/settings", SettingsPost(cfg, tmpl))
mux.HandleFunc("GET /{repo}/threads", ThreadsList(cfg, tmpl))
mux.HandleFunc("GET /{repo}/threads/new", ThreadNewGet(cfg, tmpl))
mux.HandleFunc("POST /{repo}/threads/new", ThreadNewPost(cfg, tmpl))
mux.HandleFunc("GET /{repo}/threads/{threadId}", ThreadView(cfg, tmpl))
mux.HandleFunc("POST /{repo}/threads/{threadId}/reply", ThreadReplyPost(cfg, tmpl))
mux.HandleFunc("POST /{repo}/threads/{threadId}/close", ThreadClosePost(cfg, tmpl))
mux.HandleFunc("POST /{repo}/threads/{threadId}/reopen", ThreadReopenPost(cfg, tmpl))
mux.HandleFunc("GET /{repo}/patches", PatchesList(cfg, tmpl))
mux.HandleFunc("GET /{repo}/patches/new", PatchNewGet(cfg, tmpl))
mux.HandleFunc("POST /{repo}/patches/new", PatchNewPost(cfg, tmpl))
mux.HandleFunc("GET /{repo}/patches/{patchId}", PatchView(cfg, tmpl))
mux.HandleFunc("POST /{repo}/patches/{patchId}/merge", PatchMergePost(cfg, tmpl))
mux.HandleFunc("POST /{repo}/patches/{patchId}/close", PatchClosePost(cfg, tmpl))
// Wrap with auth middleware and setup redirect.
var handler http.Handler = mux
if len(cookieSecret) > 0 {
handler = SetupRedirect(users)(handler)
handler = authMiddleware(cookieSecret)(handler)
}
srv := &http.Server{
Addr: cfg.Addr,
Handler: handler,
}
// Start SSH server if configured
if cfg.SSHAddr != "" {
sshSrv, err := NewSSHServer(cfg.Dir, cfg.SSHHostKey, users)
if err != nil {
slog.Error("failed to create SSH server", "error", err)
os.Exit(1)
}
go func() {
if err := sshSrv.ListenAndServe(cfg.SSHAddr); err != nil {
slog.Error("ssh server failed", "error", err)
}
}()
}
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
slog.Info("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
slog.Info("fierj started", "addr", cfg.Addr, "host", cfg.Host, "repos", cfg.Dir)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("failed to start server", "error", err)
}
slog.Info("stopped")
}
// hookCmd dispatches git hook invocations.
func hookCmd(args []string) int {
if len(args) < 1 {
return 0
}
switch args[0] {
case "pre-receive":
return runPreReceive()
}
return 0
}
// User returns the logged-in username from the request context, or "" if anonymous.
func User(r *http.Request) string {
u, _ := r.Context().Value(contextKey("user")).(string)
return u
}
func authMiddleware(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username := AuthUser(r, secret)
ctx := context.WithValue(r.Context(), contextKey("user"), username)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}