// Copyright (C) 2023, Jakob Wakeling
// All rights reserved.
package main
import (
"flag"
"log"
"net/http"
"os"
"os/signal"
"path"
"strings"
"time"
"github.com/Jamozed/Goit/res"
"github.com/Jamozed/Goit/src/admin"
"github.com/Jamozed/Goit/src/goit"
"github.com/Jamozed/Goit/src/repo"
"github.com/Jamozed/Goit/src/user"
"github.com/Jamozed/Goit/src/util"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/gorilla/csrf"
)
var protect func(http.Handler) http.Handler
func main() {
var backup bool
flag.BoolVar(&backup, "backup", false, "Perform a backup")
flag.BoolVar(&util.Debug, "debug", false, "Enable debug logging")
flag.Parse()
if backup {
if err := goit.Backup(); err != nil {
log.Fatalln(err.Error())
}
os.Exit(0)
}
/* Listen for and handle SIGINT */
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
goit.Cron.Stop()
os.Exit(0)
}()
/* Initialise Goit */
if err := goit.Goit(); err != nil {
log.Fatalln(err.Error())
}
h := chi.NewRouter()
h.NotFound(goit.HttpNotFound)
h.Use(middleware.RedirectSlashes)
h.Use(logHttp)
h.Use(func(h http.Handler) http.Handler {
return http.TimeoutHandler(h, 90*time.Second,
`
503 Service Unavailable
503 Service Unavailable`)
})
protect = csrf.Protect(
[]byte(goit.Conf.CsrfSecret), csrf.FieldName("csrf.Token"), csrf.CookieName("csrf"),
csrf.Secure(util.If(goit.Conf.UsesHttps, true, false)),
)
h.Group(func(r chi.Router) {
r.Use(protect)
r.Get("/", goit.HandleIndex)
r.Get("/user/login", user.HandleLogin)
r.Post("/user/login", user.HandleLogin)
r.Get("/user/logout", goit.HandleUserLogout)
r.Post("/user/logout", goit.HandleUserLogout)
r.Get("/user/sessions", user.HandleSessions)
r.Post("/user/sessions", user.HandleSessions)
r.Get("/user/edit", user.HandleEdit)
r.Post("/user/edit", user.HandleEdit)
r.Get("/repo/create", repo.HandleCreate)
r.Post("/repo/create", repo.HandleCreate)
r.Get("/admin", admin.HandleStatus)
r.Get("/admin/status", admin.HandleStatus)
r.Get("/admin/users", admin.HandleUsers)
r.Get("/admin/user/create", admin.HandleUserCreate)
r.Post("/admin/user/create", admin.HandleUserCreate)
r.Get("/admin/user/edit", admin.HandleUserEdit)
r.Post("/admin/user/edit", admin.HandleUserEdit)
r.Get("/admin/repos", admin.HandleRepos)
r.Get("/admin/repo/edit", admin.HandleRepoEdit)
r.Post("/admin/repo/edit", admin.HandleRepoEdit)
r.Get("/admin/cron", admin.HandleCron)
r.Get("/static/style.css", handleStyle)
r.Get("/static/favicon.png", handleFavicon)
r.Get("/favicon.ico", goit.HttpNotFound)
})
/* TODO figure out how to use a subrouter after manually parsing the repo path */
h.HandleFunc("/*", HandleRepo)
/* Old repository routing, doesn't support directories */
// h.Get("/{repo}", repo.HandleLog)
// h.Get("/{repo}/log", repo.HandleLog)
// h.Get("/{repo}/log/*", repo.HandleLog)
// h.Get("/{repo}/commit/{hash}", repo.HandleCommit)
// h.Get("/{repo}/tree", repo.HandleTree)
// h.Get("/{repo}/tree/*", repo.HandleTree)
// h.Get("/{repo}/file/*", repo.HandleFile)
// h.Get("/{repo}/raw/*", repo.HandleRaw)
// h.Get("/{repo}/download", repo.HandleDownload)
// h.Get("/{repo}/download/*", repo.HandleDownload)
// h.Get("/{repo}/refs", repo.HandleRefs)
// h.Get("/{repo}/edit", repo.HandleEdit)
// h.Post("/{repo}/edit", repo.HandleEdit)
// h.Get("/{repo}/info/refs", goit.HandleInfoRefs)
// h.Get("/{repo}/git-upload-pack", goit.HandleUploadPack)
// h.Post("/{repo}/git-upload-pack", goit.HandleUploadPack)
// h.Get("/{repo}/git-receive-pack", goit.HandleReceivePack)
// h.Post("/{repo}/git-receive-pack", goit.HandleReceivePack)
/* Listen for HTTP on the specified port */
if err := http.ListenAndServe(goit.Conf.HttpAddr+":"+goit.Conf.HttpPort, h); err != nil {
log.Fatalln("[http]", err.Error())
}
}
func logHttp(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
next.ServeHTTP(w, r)
ip := r.RemoteAddr
if fip := r.Header.Get("X-Forwarded-For"); goit.Conf.IpForwarded && fip != "" {
ip = fip
}
log.Println("[http]", r.Method, r.URL.String(), "from", ip, "in", time.Since(t1))
})
}
func handleStyle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
if _, err := w.Write([]byte(res.Style)); err != nil {
log.Println("[style]", err.Error())
}
}
func handleFavicon(w http.ResponseWriter, r *http.Request) {
if goit.Favicon == nil {
goit.HttpError(w, http.StatusNotFound)
} else {
w.Header().Set("Content-Type", "image/png")
if _, err := w.Write(goit.Favicon); err != nil {
log.Println("[favicon]", err.Error())
}
}
}
func HandleRepo(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
repos, err := goit.GetRepos()
if err != nil {
goit.HttpError(w, http.StatusInternalServerError)
return
}
var rpath string
for _, p := range parts {
rpath = path.Join(rpath, p)
for _, r := range repos {
if rpath == r.Name {
goto found
}
}
}
goit.HttpError(w, http.StatusNotFound)
return
found:
spath := strings.TrimPrefix(r.URL.Path, "/"+rpath)
rctx := chi.RouteContext(r.Context())
if rctx == nil {
log.Println("[route] NULL route context")
goit.HttpError(w, http.StatusInternalServerError)
return
}
rctx.URLParams.Add("repo", rpath)
rctx.URLParams.Add("*", "")
switch r.Method {
case http.MethodGet:
switch {
case strings.HasPrefix(spath, "/log"), len(spath) == 0:
rctx.URLParams.Add("*", strings.TrimLeft(strings.TrimPrefix(spath, "/log"), "/"))
protect(http.HandlerFunc(repo.HandleLog)).ServeHTTP(w, r)
case strings.HasPrefix(spath, "/commit/"):
hash := strings.TrimPrefix(spath, "/commit/")
if strings.Contains(hash, "/") {
goit.HttpError(w, http.StatusNotFound)
}
rctx.URLParams.Add("hash", hash)
protect(http.HandlerFunc(repo.HandleCommit)).ServeHTTP(w, r)
case strings.HasPrefix(spath, "/tree"):
rctx.URLParams.Add("*", strings.TrimLeft(strings.TrimPrefix(spath, "/tree"), "/"))
protect(http.HandlerFunc(repo.HandleTree)).ServeHTTP(w, r)
case strings.HasPrefix(spath, "/file/"):
rctx.URLParams.Add("*", strings.TrimPrefix(spath, "/file/"))
protect(http.HandlerFunc(repo.HandleFile)).ServeHTTP(w, r)
case strings.HasPrefix(spath, "/raw/"):
rctx.URLParams.Add("*", strings.TrimPrefix(spath, "/raw/"))
protect(http.HandlerFunc(repo.HandleRaw)).ServeHTTP(w, r)
case strings.HasPrefix(spath, "/blame/"):
rctx.URLParams.Add("*", strings.TrimPrefix(spath, "/blame/"))
protect(http.HandlerFunc(repo.HandleBlame)).ServeHTTP(w, r)
case strings.HasPrefix(spath, "/download"):
rctx.URLParams.Add("*", strings.TrimLeft(strings.TrimPrefix(spath, "/download"), "/"))
protect(http.HandlerFunc(repo.HandleDownload)).ServeHTTP(w, r)
case spath == "/refs":
protect(http.HandlerFunc(repo.HandleRefs)).ServeHTTP(w, r)
case spath == "/edit":
protect(http.HandlerFunc(repo.HandleEdit)).ServeHTTP(w, r)
case spath == "/info/refs":
goit.HandleInfoRefs(w, r)
case spath == "/git-upload-pack":
goit.HandleUploadPack(w, r)
case spath == "/git-receive-pack":
goit.HandleReceivePack(w, r)
default:
goit.HttpError(w, http.StatusNotFound)
}
case http.MethodPost:
switch {
case spath == "/edit":
protect(http.HandlerFunc(repo.HandleEdit)).ServeHTTP(w, r)
case spath == "/git-upload-pack":
goit.HandleUploadPack(w, r)
case spath == "/git-receive-pack":
goit.HandleReceivePack(w, r)
}
default:
goit.HttpError(w, http.StatusNotFound)
}
}