| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "net/http" |
| "path/filepath" |
| "regexp" |
| "strings" |
| |
| "github.com/golang/glog" |
| |
| "code.hackerspace.pl/hscloud/devtools/hackdoc/config" |
| "code.hackerspace.pl/hscloud/devtools/hackdoc/source" |
| ) |
| |
| var ( |
| flagListen = "127.0.0.1:8080" |
| flagDocRoot = "./docroot" |
| flagHackdocURL = "" |
| flagGitwebURLPattern = "https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/%s/%s" |
| flagGitwebDefaultBranch = "master" |
| |
| rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`) |
| ) |
| |
| func init() { |
| flag.Set("logtostderr", "true") |
| } |
| |
| func main() { |
| flag.StringVar(&flagListen, "listen", flagListen, "Address to listen on for HTTP traffic") |
| flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents") |
| flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes") |
| flag.StringVar(&flagGitwebURLPattern, "gitweb_url_pattern", 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) |
| } |
| |
| path, err := filepath.Abs(flagDocRoot) |
| if err != nil { |
| glog.Fatalf("Could not dereference path %q: %w", path, err) |
| } |
| |
| s := &service{ |
| source: source.NewLocal(path), |
| } |
| |
| http.HandleFunc("/", s.handler) |
| |
| glog.Infof("Listening on %q...", flagListen) |
| if err := http.ListenAndServe(flagListen, nil); err != nil { |
| glog.Fatal(err) |
| } |
| } |
| |
| type service struct { |
| source source.Source |
| } |
| |
| 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 |
| } |
| |
| glog.Infof("%+v", r.URL.Query()) |
| rev := r.URL.Query().Get("rev") |
| if rev == "" { |
| rev = flagGitwebDefaultBranch |
| } |
| |
| path := r.URL.Path |
| |
| if match := rePagePath.FindStringSubmatch(path); match != nil { |
| s.handlePage(w, r, rev, match[1]) |
| return |
| } |
| handle404(w, r) |
| } |
| |
| func logRequest(w http.ResponseWriter, r *http.Request, format string, args ...interface{}) { |
| result := fmt.Sprintf(format, args...) |
| glog.Infof("result: %s, remote: %q, ua: %q, referrer: %q, host: %q path: %q", result, r.RemoteAddr, r.Header.Get("User-Agent"), r.Header.Get("Referrer"), r.Host, r.URL.Path) |
| } |
| |
| 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 (s *service) handlePageAuto(w http.ResponseWriter, r *http.Request, rev, rpath, dirpath string) { |
| cfg, err := config.ForPath(s.source, dirpath) |
| if err != nil { |
| glog.Errorf("could not get config for path %q: %w", dirpath, err) |
| handle500(w, r) |
| return |
| } |
| for _, f := range cfg.DefaultIndex { |
| fpath := dirpath + f |
| file, err := s.source.IsFile(fpath) |
| if err != nil { |
| glog.Errorf("IsFile(%q): %w", fpath, err) |
| handle500(w, r) |
| return |
| } |
| |
| if file { |
| s.handleMarkdown(w, r, s.source, rev, fpath, cfg) |
| return |
| } |
| } |
| |
| handle404(w, r) |
| } |
| |
| func (s *service) handlePage(w http.ResponseWriter, r *http.Request, rev, page string) { |
| path := urlPathToDepotPath(page) |
| |
| if strings.HasSuffix(path, "/") { |
| // Directory path given, autoresolve. |
| dirpath := path |
| if path != "//" { |
| dirpath = strings.TrimSuffix(path, "/") + "/" |
| } |
| s.handlePageAuto(w, r, rev, path, dirpath) |
| return |
| } |
| |
| // Otherwise, try loading the file. |
| file, err := s.source.IsFile(path) |
| if err != nil { |
| glog.Errorf("IsFile(%q): %w", path, err) |
| handle500(w, r) |
| return |
| } |
| |
| // File exists, render that. |
| if file { |
| parts := strings.Split(path, "/") |
| dirpath := strings.Join(parts[:(len(parts)-1)], "/") |
| cfg, err := config.ForPath(s.source, dirpath) |
| if err != nil { |
| glog.Errorf("could not get config for path %q: %w", dirpath, err) |
| handle500(w, r) |
| return |
| } |
| s.handleMarkdown(w, r, s.source, rev, path, cfg) |
| return |
| } |
| |
| // Otherwise assume directory, try all posibilities. |
| dirpath := path |
| if path != "//" { |
| dirpath = strings.TrimSuffix(path, "/") + "/" |
| } |
| s.handlePageAuto(w, r, rev, path, dirpath) |
| } |