blob: 10fb65c78d2140e67afd0221e48b150faeaf305d [file] [log] [blame]
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02001package main
2
3import (
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +02004 "context"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +02005 "flag"
6 "fmt"
7 "net/http"
8 "path/filepath"
9 "regexp"
10 "strings"
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020011 "time"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020012
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020013 "code.hackerspace.pl/hscloud/go/mirko"
14 "code.hackerspace.pl/hscloud/go/pki"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020015 "github.com/golang/glog"
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020016 "google.golang.org/grpc"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020017
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020018 dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020019 "code.hackerspace.pl/hscloud/devtools/hackdoc/config"
20 "code.hackerspace.pl/hscloud/devtools/hackdoc/source"
21)
22
23var (
24 flagListen = "127.0.0.1:8080"
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020025 flagDocRoot = ""
26 flagDepotViewAddress = ""
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020027 flagHackdocURL = ""
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020028 flagGitwebDefaultBranch = "master"
29
30 rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`)
31)
32
33func init() {
34 flag.Set("logtostderr", "true")
35}
36
37func main() {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020038 flag.StringVar(&flagListen, "pub_listen", flagListen, "Address to listen on for HTTP traffic")
39 flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents. Either this or depotview must be set")
40 flag.StringVar(&flagDepotViewAddress, "depotview", flagDepotViewAddress, "gRPC endpoint of depotview to serve from Git. Either this or docroot must be set")
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020041 flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes")
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020042 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 //)")
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020043 flag.StringVar(&flagGitwebDefaultBranch, "gitweb_default_rev", flagGitwebDefaultBranch, "Default Git rev to render/link to")
44 flag.Parse()
45
46 if flagHackdocURL == "" {
47 flagHackdocURL = fmt.Sprintf("http://%s", flagListen)
48 }
49
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020050 if flagDocRoot == "" && flagDepotViewAddress == "" {
51 glog.Errorf("Either -docroot or -depotview must be set")
52 }
53 if flagDocRoot != "" && flagDepotViewAddress != "" {
54 glog.Errorf("Only one of -docroot or -depotview must be set")
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020055 }
56
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020057 m := mirko.New()
58 if err := m.Listen(); err != nil {
59 glog.Exitf("Listen(): %v", err)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020060 }
61
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020062 var s *service
63 if flagDocRoot != "" {
64 path, err := filepath.Abs(flagDocRoot)
65 if err != nil {
66 glog.Exitf("Could not dereference path %q: %w", path, err)
67 }
68 glog.Infof("Starting in docroot mode for %q -> %q", flagDocRoot, path)
69
70 s = &service{
71 source: source.NewSingleRefProvider(source.NewLocal(path)),
72 }
73 } else {
74 glog.Infof("Starting in depotview mode (server %q)", flagDepotViewAddress)
75 conn, err := grpc.Dial(flagDepotViewAddress, pki.WithClientHSPKI())
76 if err != nil {
77 glog.Exitf("grpc.Dial(%q): %v", flagDepotViewAddress, err)
78 }
79 stub := dvpb.NewDepotViewClient(conn)
80 s = &service{
81 source: source.NewDepotView(stub),
82 }
83 }
84
85 mux := http.NewServeMux()
86 mux.HandleFunc("/", s.handler)
87 srv := &http.Server{Addr: flagListen, Handler: mux}
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +020088
89 glog.Infof("Listening on %q...", flagListen)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020090 go func() {
91 if err := srv.ListenAndServe(); err != nil {
92 glog.Error(err)
93 }
94 }()
95
96 <-m.Done()
97 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
98 defer cancel()
99 srv.Shutdown(ctx)
100
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200101}
102
103type service struct {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200104 source source.SourceProvider
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200105}
106
107func (s *service) handler(w http.ResponseWriter, r *http.Request) {
108 if r.Method != "GET" && r.Method != "HEAD" {
109 w.WriteHeader(http.StatusMethodNotAllowed)
110 fmt.Fprintf(w, "method not allowed")
111 return
112 }
113
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200114 ref := r.URL.Query().Get("ref")
115 if ref == "" {
116 ref = flagGitwebDefaultBranch
117 }
118
119 ctx := r.Context()
120 source, err := s.source.Source(ctx, ref)
121 switch {
122 case err != nil:
123 glog.Errorf("Source(%q): %v", ref, err)
124 handle500(w, r)
125 return
126 case source == nil:
127 handle404(w, r)
128 return
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200129 }
130
131 path := r.URL.Path
132
133 if match := rePagePath.FindStringSubmatch(path); match != nil {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200134 req := &request{
135 w: w,
136 r: r,
137 ctx: r.Context(),
138 ref: ref,
139 source: source,
140 }
141 req.handlePage(match[1])
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200142 return
143 }
144 handle404(w, r)
145}
146
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200147type request struct {
148 w http.ResponseWriter
149 r *http.Request
150 ctx context.Context
151
152 ref string
153 source source.Source
154 // rpath is the path requested by the client
155 rpath string
156}
157
158func (r *request) handle500() {
159 handle500(r.w, r.r)
160}
161
162func (r *request) handle404() {
163 handle404(r.w, r.r)
164}
165
166func (r *request) logRequest(format string, args ...interface{}) {
167 logRequest(r.w, r.r, format, args...)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200168}
169
170func urlPathToDepotPath(url string) string {
171 // Sanitize request.
172 parts := strings.Split(url, "/")
173 for i, p := range parts {
174 // Allow last part to be "", ie, for a path to end in /
175 if p == "" {
176 if i != len(parts)-1 {
177 return ""
178 }
179 }
180
181 // net/http sanitizes this anyway, but we better be sure.
182 if p == "." || p == ".." {
183 return ""
184 }
185 }
186 path := "//" + strings.Join(parts, "/")
187
188 return path
189}
190
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200191func (r *request) handlePageAuto(dirpath string) {
192 cfg, err := config.ForPath(r.ctx, r.source, dirpath)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200193 if err != nil {
194 glog.Errorf("could not get config for path %q: %w", dirpath, err)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200195 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200196 return
197 }
198 for _, f := range cfg.DefaultIndex {
199 fpath := dirpath + f
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200200 file, err := r.source.IsFile(r.ctx, fpath)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200201 if err != nil {
202 glog.Errorf("IsFile(%q): %w", fpath, err)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200203 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200204 return
205 }
206
207 if file {
Serge Bazanski81262ff2021-03-06 20:44:56 +0000208 ref := r.ref
209 if ref == flagGitwebDefaultBranch {
210 ref = ""
211 }
212 path := "/" + fpath
213 if ref != "" {
214 path += "?ref=" + ref
215 }
216 http.Redirect(r.w, r.r, path, 302)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200217 return
218 }
219 }
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200220 r.handle404()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200221}
222
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200223func (r *request) handlePage(page string) {
224 r.rpath = urlPathToDepotPath(page)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200225
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200226 if strings.HasSuffix(r.rpath, "/") {
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200227 // Directory path given, autoresolve.
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200228 dirpath := r.rpath
229 if r.rpath != "//" {
230 dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200231 }
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200232 r.handlePageAuto(dirpath)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200233 return
234 }
235
236 // Otherwise, try loading the file.
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200237 file, err := r.source.IsFile(r.ctx, r.rpath)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200238 if err != nil {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200239 glog.Errorf("IsFile(%q): %w", r.rpath, err)
240 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200241 return
242 }
243
244 // File exists, render that.
245 if file {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200246 parts := strings.Split(r.rpath, "/")
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200247 dirpath := strings.Join(parts[:(len(parts)-1)], "/")
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200248 // TODO(q3k): figure out this hack, hopefully by implementing a real path type
249 if dirpath == "/" {
250 dirpath = "//"
251 }
252
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200253 cfg, err := config.ForPath(r.ctx, r.source, dirpath)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200254 if err != nil {
255 glog.Errorf("could not get config for path %q: %w", dirpath, err)
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200256 r.handle500()
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200257 return
258 }
Sergiusz Bazanski8adbd492020-04-10 21:20:53 +0200259 r.handleFile(r.rpath, cfg)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200260 return
261 }
262
263 // Otherwise assume directory, try all posibilities.
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200264 dirpath := r.rpath
265 if r.rpath != "//" {
266 dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200267 }
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200268 r.handlePageAuto(dirpath)
Sergiusz Bazanskic881cf32020-04-08 20:03:12 +0200269}