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