blob: aae850f707fa8f5a1ca3de8e4ead8a383b8f9313 [file] [log] [blame]
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 {
http.Redirect(r.w, r.r, "/"+fpath, 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)
}