blob: 938a4262b0ba8bbae4b0faa4085f431c9d5cf8ab [file] [log] [blame]
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02001package main
2
3import (
4 "flag"
5 "fmt"
6 "net/http"
7 "path/filepath"
8 "regexp"
9 "strings"
10
11 "github.com/golang/glog"
12
13 "code.hackerspace.pl/hscloud/devtools/hackdoc/config"
14 "code.hackerspace.pl/hscloud/devtools/hackdoc/source"
15)
16
17var (
18 flagListen = "127.0.0.1:8080"
19 flagDocRoot = "./docroot"
20 flagHackdocURL = ""
21 flagGitwebURLPattern = "https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/%s/%s"
22 flagGitwebDefaultBranch = "master"
23
24 rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`)
25)
26
27func init() {
28 flag.Set("logtostderr", "true")
29}
30
31func main() {
32 flag.StringVar(&flagListen, "listen", flagListen, "Address to listen on for HTTP traffic")
33 flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents")
34 flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes")
35 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 //)")
36 flag.StringVar(&flagGitwebDefaultBranch, "gitweb_default_rev", flagGitwebDefaultBranch, "Default Git rev to render/link to")
37 flag.Parse()
38
39 if flagHackdocURL == "" {
40 flagHackdocURL = fmt.Sprintf("http://%s", flagListen)
41 }
42
43 path, err := filepath.Abs(flagDocRoot)
44 if err != nil {
45 glog.Fatalf("Could not dereference path %q: %w", path, err)
46 }
47
48 s := &service{
49 source: source.NewLocal(path),
50 }
51
52 http.HandleFunc("/", s.handler)
53
54 glog.Infof("Listening on %q...", flagListen)
55 if err := http.ListenAndServe(flagListen, nil); err != nil {
56 glog.Fatal(err)
57 }
58}
59
60type service struct {
61 source source.Source
62}
63
64func (s *service) handler(w http.ResponseWriter, r *http.Request) {
65 if r.Method != "GET" && r.Method != "HEAD" {
66 w.WriteHeader(http.StatusMethodNotAllowed)
67 fmt.Fprintf(w, "method not allowed")
68 return
69 }
70
71 glog.Infof("%+v", r.URL.Query())
72 rev := r.URL.Query().Get("rev")
73 if rev == "" {
74 rev = flagGitwebDefaultBranch
75 }
76
77 path := r.URL.Path
78
79 if match := rePagePath.FindStringSubmatch(path); match != nil {
80 s.handlePage(w, r, rev, match[1])
81 return
82 }
83 handle404(w, r)
84}
85
86func logRequest(w http.ResponseWriter, r *http.Request, format string, args ...interface{}) {
87 result := fmt.Sprintf(format, args...)
88 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)
89}
90
91func urlPathToDepotPath(url string) string {
92 // Sanitize request.
93 parts := strings.Split(url, "/")
94 for i, p := range parts {
95 // Allow last part to be "", ie, for a path to end in /
96 if p == "" {
97 if i != len(parts)-1 {
98 return ""
99 }
100 }
101
102 // net/http sanitizes this anyway, but we better be sure.
103 if p == "." || p == ".." {
104 return ""
105 }
106 }
107 path := "//" + strings.Join(parts, "/")
108
109 return path
110}
111
112func (s *service) handlePageAuto(w http.ResponseWriter, r *http.Request, rev, rpath, dirpath string) {
113 cfg, err := config.ForPath(s.source, dirpath)
114 if err != nil {
115 glog.Errorf("could not get config for path %q: %w", dirpath, err)
116 handle500(w, r)
117 return
118 }
119 for _, f := range cfg.DefaultIndex {
120 fpath := dirpath + f
121 file, err := s.source.IsFile(fpath)
122 if err != nil {
123 glog.Errorf("IsFile(%q): %w", fpath, err)
124 handle500(w, r)
125 return
126 }
127
128 if file {
129 s.handleMarkdown(w, r, s.source, rev, fpath, cfg)
130 return
131 }
132 }
133
134 handle404(w, r)
135}
136
137func (s *service) handlePage(w http.ResponseWriter, r *http.Request, rev, page string) {
138 path := urlPathToDepotPath(page)
139
140 if strings.HasSuffix(path, "/") {
141 // Directory path given, autoresolve.
142 dirpath := path
143 if path != "//" {
144 dirpath = strings.TrimSuffix(path, "/") + "/"
145 }
146 s.handlePageAuto(w, r, rev, path, dirpath)
147 return
148 }
149
150 // Otherwise, try loading the file.
151 file, err := s.source.IsFile(path)
152 if err != nil {
153 glog.Errorf("IsFile(%q): %w", path, err)
154 handle500(w, r)
155 return
156 }
157
158 // File exists, render that.
159 if file {
160 parts := strings.Split(path, "/")
161 dirpath := strings.Join(parts[:(len(parts)-1)], "/")
162 cfg, err := config.ForPath(s.source, dirpath)
163 if err != nil {
164 glog.Errorf("could not get config for path %q: %w", dirpath, err)
165 handle500(w, r)
166 return
167 }
168 s.handleMarkdown(w, r, s.source, rev, path, cfg)
169 return
170 }
171
172 // Otherwise assume directory, try all posibilities.
173 dirpath := path
174 if path != "//" {
175 dirpath = strings.TrimSuffix(path, "/") + "/"
176 }
177 s.handlePageAuto(w, r, rev, path, dirpath)
178}