Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 4 | "context" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 5 | "flag" |
| 6 | "fmt" |
| 7 | "net/http" |
| 8 | "path/filepath" |
| 9 | "regexp" |
| 10 | "strings" |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 11 | "time" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 12 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 13 | "code.hackerspace.pl/hscloud/go/mirko" |
| 14 | "code.hackerspace.pl/hscloud/go/pki" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 15 | "github.com/golang/glog" |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 16 | "google.golang.org/grpc" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 17 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 18 | dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 19 | "code.hackerspace.pl/hscloud/devtools/hackdoc/config" |
| 20 | "code.hackerspace.pl/hscloud/devtools/hackdoc/source" |
| 21 | ) |
| 22 | |
| 23 | var ( |
| 24 | flagListen = "127.0.0.1:8080" |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 25 | flagDocRoot = "" |
| 26 | flagDepotViewAddress = "" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 27 | flagHackdocURL = "" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 28 | flagGitwebDefaultBranch = "master" |
| 29 | |
| 30 | rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`) |
| 31 | ) |
| 32 | |
| 33 | func init() { |
| 34 | flag.Set("logtostderr", "true") |
| 35 | } |
| 36 | |
| 37 | func main() { |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 38 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 41 | flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes") |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 42 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 43 | 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 Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 50 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 55 | } |
| 56 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 57 | m := mirko.New() |
| 58 | if err := m.Listen(); err != nil { |
| 59 | glog.Exitf("Listen(): %v", err) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 60 | } |
| 61 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 62 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 88 | |
| 89 | glog.Infof("Listening on %q...", flagListen) |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 90 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 101 | } |
| 102 | |
| 103 | type service struct { |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 104 | source source.SourceProvider |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 105 | } |
| 106 | |
| 107 | func (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 Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 114 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 129 | } |
| 130 | |
| 131 | path := r.URL.Path |
| 132 | |
| 133 | if match := rePagePath.FindStringSubmatch(path); match != nil { |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 134 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 142 | return |
| 143 | } |
| 144 | handle404(w, r) |
| 145 | } |
| 146 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 147 | type 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 | |
| 158 | func (r *request) handle500() { |
| 159 | handle500(r.w, r.r) |
| 160 | } |
| 161 | |
| 162 | func (r *request) handle404() { |
| 163 | handle404(r.w, r.r) |
| 164 | } |
| 165 | |
| 166 | func (r *request) logRequest(format string, args ...interface{}) { |
| 167 | logRequest(r.w, r.r, format, args...) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 168 | } |
| 169 | |
| 170 | func 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 Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 191 | func (r *request) handlePageAuto(dirpath string) { |
| 192 | cfg, err := config.ForPath(r.ctx, r.source, dirpath) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 193 | if err != nil { |
| 194 | glog.Errorf("could not get config for path %q: %w", dirpath, err) |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 195 | r.handle500() |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 196 | return |
| 197 | } |
| 198 | for _, f := range cfg.DefaultIndex { |
| 199 | fpath := dirpath + f |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 200 | file, err := r.source.IsFile(r.ctx, fpath) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 201 | if err != nil { |
| 202 | glog.Errorf("IsFile(%q): %w", fpath, err) |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 203 | r.handle500() |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 204 | return |
| 205 | } |
| 206 | |
| 207 | if file { |
Serge Bazanski | 81262ff | 2021-03-06 20:44:56 +0000 | [diff] [blame] | 208 | 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 Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 217 | return |
| 218 | } |
| 219 | } |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 220 | r.handle404() |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 221 | } |
| 222 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 223 | func (r *request) handlePage(page string) { |
| 224 | r.rpath = urlPathToDepotPath(page) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 225 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 226 | if strings.HasSuffix(r.rpath, "/") { |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 227 | // Directory path given, autoresolve. |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 228 | dirpath := r.rpath |
| 229 | if r.rpath != "//" { |
| 230 | dirpath = strings.TrimSuffix(r.rpath, "/") + "/" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 231 | } |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 232 | r.handlePageAuto(dirpath) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 233 | return |
| 234 | } |
| 235 | |
| 236 | // Otherwise, try loading the file. |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 237 | file, err := r.source.IsFile(r.ctx, r.rpath) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 238 | if err != nil { |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 239 | glog.Errorf("IsFile(%q): %w", r.rpath, err) |
| 240 | r.handle500() |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 241 | return |
| 242 | } |
| 243 | |
| 244 | // File exists, render that. |
| 245 | if file { |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 246 | parts := strings.Split(r.rpath, "/") |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 247 | dirpath := strings.Join(parts[:(len(parts)-1)], "/") |
Sergiusz Bazanski | 8adbd49 | 2020-04-10 21:20:53 +0200 | [diff] [blame] | 248 | // TODO(q3k): figure out this hack, hopefully by implementing a real path type |
| 249 | if dirpath == "/" { |
| 250 | dirpath = "//" |
| 251 | } |
| 252 | |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 253 | cfg, err := config.ForPath(r.ctx, r.source, dirpath) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 254 | if err != nil { |
| 255 | glog.Errorf("could not get config for path %q: %w", dirpath, err) |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 256 | r.handle500() |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 257 | return |
| 258 | } |
Sergiusz Bazanski | 8adbd49 | 2020-04-10 21:20:53 +0200 | [diff] [blame] | 259 | r.handleFile(r.rpath, cfg) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 260 | return |
| 261 | } |
| 262 | |
| 263 | // Otherwise assume directory, try all posibilities. |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 264 | dirpath := r.rpath |
| 265 | if r.rpath != "//" { |
| 266 | dirpath = strings.TrimSuffix(r.rpath, "/") + "/" |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 267 | } |
Sergiusz Bazanski | f157b4d | 2020-04-10 17:39:43 +0200 | [diff] [blame] | 268 | r.handlePageAuto(dirpath) |
Sergiusz Bazanski | c881cf3 | 2020-04-08 20:03:12 +0200 | [diff] [blame] | 269 | } |