Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

Goit/src/main.go (274 lines, 7.8 KiB) -rw-r--r-- blame download

0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
// 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,
			`<!DOCTYPE html><html lang="en"><head>
		<meta charset="UTF-8"><title>503 Service Unavailable</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<link rel="stylesheet" type="text/css" href="/static/style.css">
		<link rel="icon" type="image/png" href="/static/favicon.png">
		</head><body><b>503 Service Unavailable</b></body></html>`)
	})

	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)
	}
}