package main import ( "bytes" "context" "fmt" "io" "log" "net/http" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "time" ) func (web *Web) spawnHTTP80() { web.mux.Lock() defer web.mux.Unlock() web.srv80 = &http.Server{ Addr: ":80", Handler: http.HandlerFunc(web.handle80), } go func() { err := web.srv80.ListenAndServe() log.Printf("gruau: srv80: %v", err) }() } func (web *Web) killHTTP80() { ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) web.srv80.Shutdown(ctx) } func (web *Web) handle80(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "gruau") rawurl := (&url.URL{ Scheme: "https", Host: web.GitHost, Path: r.URL.Path, RawPath: r.URL.RawPath, RawQuery: r.URL.RawQuery, }).String() w.Header().Set("Location", percentEscapeNonASCII(rawurl)) plain(w, http.StatusPermanentRedirect, "please consider using HTTPS") } func (web *Web) spawnHTTP443(auth Authenticator) { web.mux.Lock() defer web.mux.Unlock() web.srv443 = &http.Server{ Addr: ":443", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { web.handle443(auth, w, r) }), } fullchain := filepath.Join("/etc/letsencrypt/live", web.GitHost, "fullchain.pem") privkey := filepath.Join("/etc/letsencrypt/live", web.GitHost, "privkey.pem") go func() { err := web.srv443.ListenAndServeTLS(fullchain, privkey) log.Printf("gruau: srv80: %v", err) }() } func (web *Web) killHTTP443() { ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) web.srv443.Shutdown(ctx) } func (web *Web) handle443(auth Authenticator, w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "gruau") if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) plain(w, http.StatusMethodNotAllowed, "please consider using %s, not %s", http.MethodGet, r.Method) return } path := r.URL.Path if strings.HasSuffix(path, "/") { path += "index.html" } if strings.HasSuffix(path, ".html") { rawurl := (&url.URL{ Scheme: "https", Host: web.WebgitHost, Path: r.URL.Path, RawPath: r.URL.RawPath, RawQuery: r.URL.RawQuery, }).String() w.Header().Set("Location", percentEscapeNonASCII(rawurl)) plain(w, http.StatusTemporaryRedirect, "please refer to a webgit endpoint: %s", rawurl) return } if !filepath.IsAbs(path) || path != filepath.Clean(path) { plain(w, http.StatusNotFound, "") return } re := regexp.MustCompile(`^/(~[0-9a-z_.-]+/[0-9a-z_.-]+\.git)(/[0-9A-Za-z./_-]+)$`) smatchs := re.FindStringSubmatch(path) if len(smatchs) != 1+2 { plain(w, http.StatusNotFound, "") return } repo, ok := ParseGitRepositoryID(smatchs[1]) repopath := smatchs[2] if !ok || repopath == "" || !filepath.IsAbs(repopath) || filepath.Clean(repopath) != repopath { plain(w, http.StatusNotFound, "") return } authenticated := auth(r.Header.Get(GruauAuthtokenHeader), repo) if !authenticated { plain(w, http.StatusForbidden, "") return } /*** AUTHENTICATED ***/ err := MakeWebServicable(repo) if err != nil { log.Printf("gruau: cannot make public repository %s web servicable: %v", repo.ID(), err) plain(w, http.StatusNotFound, "") return } rooted := filepath.Join(GruauRepositoriesRoot, repo.ID(), repopath) info, err := os.Lstat(rooted) if err != nil || !info.Mode().IsRegular() { plain(w, http.StatusNotFound, "") return } f, err := os.Open(rooted) if err != nil { plain(w, http.StatusNotFound, "") return } defer f.Close() w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Encoding", "identity") w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) w.WriteHeader(http.StatusOK) io.Copy(w, f) } func plain(w http.ResponseWriter, code int, format string, a ...interface{}) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(code) fmt.Fprintf(w, "%d %s", code, http.StatusText(code)) if format != "" { fmt.Fprintf(w, format, a...) } if len(format) == 0 || format[len(format)-1] != '\n' { fmt.Fprintf(w, "\n") } } func percentEscapeNonASCII(raw string) string { buf := new(bytes.Buffer) for _, b := range []byte(raw) { if b >= ' ' || b <= '~' { buf.WriteByte(b) } else { fmt.Fprintf(buf, "%02X", b) } } return buf.String() }