forked from hswaw/hscloud
270 lines
6.3 KiB
Go
270 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.hackerspace.pl/hscloud/go/mirko"
|
|
"code.hackerspace.pl/hscloud/go/pki"
|
|
"github.com/golang/glog"
|
|
"google.golang.org/grpc"
|
|
|
|
dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
|
|
"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
|
|
"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
|
|
)
|
|
|
|
var (
|
|
flagListen = "127.0.0.1:8080"
|
|
flagDocRoot = ""
|
|
flagDepotViewAddress = ""
|
|
flagHackdocURL = ""
|
|
flagGitwebDefaultBranch = "master"
|
|
|
|
rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`)
|
|
)
|
|
|
|
func init() {
|
|
flag.Set("logtostderr", "true")
|
|
}
|
|
|
|
func main() {
|
|
flag.StringVar(&flagListen, "pub_listen", flagListen, "Address to listen on for HTTP traffic")
|
|
flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents. Either this or depotview must be set")
|
|
flag.StringVar(&flagDepotViewAddress, "depotview", flagDepotViewAddress, "gRPC endpoint of depotview to serve from Git. Either this or docroot must be set")
|
|
flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes")
|
|
flag.StringVar(&source.FlagGitwebURLPattern, "gitweb_url_pattern", source.FlagGitwebURLPattern, "Pattern to sprintf to for URL for viewing a file in Git. First string is ref/rev, second is bare file path (sans //)")
|
|
flag.StringVar(&flagGitwebDefaultBranch, "gitweb_default_rev", flagGitwebDefaultBranch, "Default Git rev to render/link to")
|
|
flag.Parse()
|
|
|
|
if flagHackdocURL == "" {
|
|
flagHackdocURL = fmt.Sprintf("http://%s", flagListen)
|
|
}
|
|
|
|
if flagDocRoot == "" && flagDepotViewAddress == "" {
|
|
glog.Errorf("Either -docroot or -depotview must be set")
|
|
}
|
|
if flagDocRoot != "" && flagDepotViewAddress != "" {
|
|
glog.Errorf("Only one of -docroot or -depotview must be set")
|
|
}
|
|
|
|
m := mirko.New()
|
|
if err := m.Listen(); err != nil {
|
|
glog.Exitf("Listen(): %v", err)
|
|
}
|
|
|
|
var s *service
|
|
if flagDocRoot != "" {
|
|
path, err := filepath.Abs(flagDocRoot)
|
|
if err != nil {
|
|
glog.Exitf("Could not dereference path %q: %w", path, err)
|
|
}
|
|
glog.Infof("Starting in docroot mode for %q -> %q", flagDocRoot, path)
|
|
|
|
s = &service{
|
|
source: source.NewSingleRefProvider(source.NewLocal(path)),
|
|
}
|
|
} else {
|
|
glog.Infof("Starting in depotview mode (server %q)", flagDepotViewAddress)
|
|
conn, err := grpc.Dial(flagDepotViewAddress, pki.WithClientHSPKI())
|
|
if err != nil {
|
|
glog.Exitf("grpc.Dial(%q): %v", flagDepotViewAddress, err)
|
|
}
|
|
stub := dvpb.NewDepotViewClient(conn)
|
|
s = &service{
|
|
source: source.NewDepotView(stub),
|
|
}
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", s.handler)
|
|
srv := &http.Server{Addr: flagListen, Handler: mux}
|
|
|
|
glog.Infof("Listening on %q...", flagListen)
|
|
go func() {
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
glog.Error(err)
|
|
}
|
|
}()
|
|
|
|
<-m.Done()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
srv.Shutdown(ctx)
|
|
|
|
}
|
|
|
|
type service struct {
|
|
source source.SourceProvider
|
|
}
|
|
|
|
func (s *service) handler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" && r.Method != "HEAD" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
fmt.Fprintf(w, "method not allowed")
|
|
return
|
|
}
|
|
|
|
ref := r.URL.Query().Get("ref")
|
|
if ref == "" {
|
|
ref = flagGitwebDefaultBranch
|
|
}
|
|
|
|
ctx := r.Context()
|
|
source, err := s.source.Source(ctx, ref)
|
|
switch {
|
|
case err != nil:
|
|
glog.Errorf("Source(%q): %v", ref, err)
|
|
handle500(w, r)
|
|
return
|
|
case source == nil:
|
|
handle404(w, r)
|
|
return
|
|
}
|
|
|
|
path := r.URL.Path
|
|
|
|
if match := rePagePath.FindStringSubmatch(path); match != nil {
|
|
req := &request{
|
|
w: w,
|
|
r: r,
|
|
ctx: r.Context(),
|
|
ref: ref,
|
|
source: source,
|
|
}
|
|
req.handlePage(match[1])
|
|
return
|
|
}
|
|
handle404(w, r)
|
|
}
|
|
|
|
type request struct {
|
|
w http.ResponseWriter
|
|
r *http.Request
|
|
ctx context.Context
|
|
|
|
ref string
|
|
source source.Source
|
|
// rpath is the path requested by the client
|
|
rpath string
|
|
}
|
|
|
|
func (r *request) handle500() {
|
|
handle500(r.w, r.r)
|
|
}
|
|
|
|
func (r *request) handle404() {
|
|
handle404(r.w, r.r)
|
|
}
|
|
|
|
func (r *request) logRequest(format string, args ...interface{}) {
|
|
logRequest(r.w, r.r, format, args...)
|
|
}
|
|
|
|
func urlPathToDepotPath(url string) string {
|
|
// Sanitize request.
|
|
parts := strings.Split(url, "/")
|
|
for i, p := range parts {
|
|
// Allow last part to be "", ie, for a path to end in /
|
|
if p == "" {
|
|
if i != len(parts)-1 {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// net/http sanitizes this anyway, but we better be sure.
|
|
if p == "." || p == ".." {
|
|
return ""
|
|
}
|
|
}
|
|
path := "//" + strings.Join(parts, "/")
|
|
|
|
return path
|
|
}
|
|
|
|
func (r *request) handlePageAuto(dirpath string) {
|
|
cfg, err := config.ForPath(r.ctx, r.source, dirpath)
|
|
if err != nil {
|
|
glog.Errorf("could not get config for path %q: %w", dirpath, err)
|
|
r.handle500()
|
|
return
|
|
}
|
|
for _, f := range cfg.DefaultIndex {
|
|
fpath := dirpath + f
|
|
file, err := r.source.IsFile(r.ctx, fpath)
|
|
if err != nil {
|
|
glog.Errorf("IsFile(%q): %w", fpath, err)
|
|
r.handle500()
|
|
return
|
|
}
|
|
|
|
if file {
|
|
ref := r.ref
|
|
if ref == flagGitwebDefaultBranch {
|
|
ref = ""
|
|
}
|
|
path := "/" + fpath
|
|
if ref != "" {
|
|
path += "?ref=" + ref
|
|
}
|
|
http.Redirect(r.w, r.r, path, 302)
|
|
return
|
|
}
|
|
}
|
|
r.handle404()
|
|
}
|
|
|
|
func (r *request) handlePage(page string) {
|
|
r.rpath = urlPathToDepotPath(page)
|
|
|
|
if strings.HasSuffix(r.rpath, "/") {
|
|
// Directory path given, autoresolve.
|
|
dirpath := r.rpath
|
|
if r.rpath != "//" {
|
|
dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
|
|
}
|
|
r.handlePageAuto(dirpath)
|
|
return
|
|
}
|
|
|
|
// Otherwise, try loading the file.
|
|
file, err := r.source.IsFile(r.ctx, r.rpath)
|
|
if err != nil {
|
|
glog.Errorf("IsFile(%q): %w", r.rpath, err)
|
|
r.handle500()
|
|
return
|
|
}
|
|
|
|
// File exists, render that.
|
|
if file {
|
|
parts := strings.Split(r.rpath, "/")
|
|
dirpath := strings.Join(parts[:(len(parts)-1)], "/")
|
|
// TODO(q3k): figure out this hack, hopefully by implementing a real path type
|
|
if dirpath == "/" {
|
|
dirpath = "//"
|
|
}
|
|
|
|
cfg, err := config.ForPath(r.ctx, r.source, dirpath)
|
|
if err != nil {
|
|
glog.Errorf("could not get config for path %q: %w", dirpath, err)
|
|
r.handle500()
|
|
return
|
|
}
|
|
r.handleFile(r.rpath, cfg)
|
|
return
|
|
}
|
|
|
|
// Otherwise assume directory, try all posibilities.
|
|
dirpath := r.rpath
|
|
if r.rpath != "//" {
|
|
dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
|
|
}
|
|
r.handlePageAuto(dirpath)
|
|
}
|