/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package main import ( "bytes" "context" "fmt" "html/template" "log" "net" "net/http" "os" "regexp" "slices" "sort" "strconv" "strings" "time" dp "idio.link/go/depager/v2" ) var goGetTmpl *template.Template func init() { const respTmpl = `` + `` + `
` + `` + `` + `` + `` + `go get https://{{.Module}}` + `` + `` goGetTmpl = template.Must(template.New("resp").Parse(respTmpl)) } func NewServer(ctx context.Context) *Server { accessToken, ok := os.LookupEnv("GOGS_ACCESS_TOKEN") if !ok { panic("Missing environment variable GOGS_ACCESS_TOKEN.") } s := &Server{ ctx: ctx, client: NewGogsClient(ctx, accessToken), shutdownTimeout: 5 * time.Second, } s.svr = &http.Server{ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, BaseContext: func(_ net.Listener) context.Context { return s.ctx }, } return s } type Server struct { ctx context.Context svr *http.Server client *GogsClient projects Cached[map[string]GitRepo] shutdownTimeout time.Duration } func (s *Server) ListenAndServe(port int) error { s.svr.Addr = fmt.Sprintf(":%d", port) s.paths() return s.svr.ListenAndServe() } func (s *Server) Shutdown() error { ctx, cancel := context.WithTimeout(s.ctx, s.shutdownTimeout) defer cancel() return s.svr.Shutdown(ctx) } func (s *Server) paths() { http.HandleFunc("/go/", s.handleGoGet()) } func (s *Server) updateProjectCache(group string) error { // TODO Push caches down into Gogs client. if s.projects.Expired() { projects := map[string]GitRepo{} pager := FetchOrgRepos(s.client, group) for p := range pager.Iter() { projects[p.Path()] = p } if err := pager.LastErr(); err != nil { log.Printf("request go-get: unable to fetch gogs projects: %v", err) return err } s.projects = NewCached(&projects, 30*time.Minute) } return nil } func (s *Server) handleGoGet() http.HandlerFunc { gpr := new(GoProxyRequest) return func(w http.ResponseWriter, req *http.Request) { parseGoProxyPath(req, gpr) switch { // /@latest case gpr.Op == GoProxyRequestLatest: handleLatest(w, gpr, s.client) // /@v/list case gpr.Op == GoProxyRequestList: handleList(w, gpr, s.client) // /@v/$version.{.info,.mod,.zip} case gpr.Op == GoProxyRequestInfo || gpr.Op == GoProxyRequestMod || gpr.Op == GoProxyRequestZip: handleVersion(w, gpr, s.client) // go-get=1 case req.URL.Query().Get("go-get") == "1": err := s.updateProjectCache(gpr.Host) if err != nil { code := http.StatusServiceUnavailable http.Error(w, http.StatusText(code), code) } repo, ok := (*s.projects.Value())[gpr.RepoPath] if !ok { code := http.StatusNotFound http.Error(w, http.StatusText(code), code) log.Printf("request go-get: unable to find git project for module %#v", gpr.Module) } handleGoGet(w, gpr, repo) default: log.Printf("malformed request: %s", req.URL.String()) code := http.StatusBadRequest http.Error(w, http.StatusText(code), code) } } } func parseVersion(str string) ([]uint, error) { str = strings.TrimPrefix(str, "v") parts := strings.Split(str, ".") ver := make([]uint, 3) for i := 0; i < len(parts) && i < 3; i++ { v, err := strconv.ParseUint(parts[i], 10, 32) if err != nil { return nil, err } ver[i] = uint(v) } return ver, nil } func handleLatest( w http.ResponseWriter, gpr *GoProxyRequest, client *GogsClient, ) { latest := "" latestTag := make([]uint, 3) pager := ListTags(client, gpr.Host, gpr.Repo) for t := range pager.Iter() { tag, err := parseVersion(t.Name) if err != nil { log.Printf("error while parsing tag %#v: %v", t, err) continue } if slices.Compare(latestTag, tag) == -1 { latestTag = tag latest = t.Name } } if pager.LastErr() != nil { log.Printf("request /@latest: %v", pager.LastErr()) code := http.StatusServiceUnavailable http.Error(w, http.StatusText(code), code) return } w.Write([]byte(latest + "\n")) } func handleVersion( w http.ResponseWriter, gpr *GoProxyRequest, client *GogsClient, ) { // TODO Dispatch to .info, .mod, or .zip handler switch gpr.Op { case GoProxyRequestInfo: handleVersionInfo(w, gpr, client) default: log.Printf("request %#v: not implemented", gpr.URI) code := http.StatusNotFound http.Error(w, http.StatusText(code), code) } } func findTag( pager dp.Pager[*GogsTag], tagName string, ) (tag *GogsTag, err error) { for t := range pager.Iter() { if t.Name == tagName { tag = t break } } if err = pager.LastErr(); err != nil { err = fmt.Errorf("get tag %#v: %w", tagName, err) return } return } func handleVersionInfo( w http.ResponseWriter, gpr *GoProxyRequest, client *GogsClient, ) { pager := ListTags(client, gpr.Host, gpr.Repo) tag, err := findTag(pager, gpr.Version) if err != nil { log.Printf("request %#v: no tag matching version %#v: %v", gpr.URI, gpr.Version, err) code := http.StatusServiceUnavailable http.Error(w, http.StatusText(code), code) return } json := fmt.Sprintf( `{"version": %#v, "timestamp": %#v}`, tag.Name, tag.Commit.Timestamp, ) w.Write([]byte(json + "\n")) } func handleList( w http.ResponseWriter, gpr *GoProxyRequest, client *GogsClient, ) { verMajor := regexp.MustCompile(`^v[01]\.`) if v := gpr.VersionMajor; v != "" { verMajor = regexp.MustCompile(fmt.Sprintf("^%s\\.", v)) } pager := ListTags(client, gpr.Host, gpr.Repo) tags := make([]*GogsTag, 0, 16) for t := range pager.Iter() { if !verMajor.MatchString(t.Name) { continue } tags = append(tags, t) } sort.SliceStable(tags, func(i, j int) bool { ti := tags[i].Name tj := tags[j].Name return strings.Compare(ti, tj) == -1 }) for _, t := range tags { w.Write([]byte(t.Name + "\n")) } if pager.LastErr() != nil { log.Printf("request %#v: list gogs releases: %v", gpr.URI, pager.LastErr()) code := http.StatusServiceUnavailable http.Error(w, http.StatusText(code), code) } } func handleGoGet( w http.ResponseWriter, gpr *GoProxyRequest, repo GitRepo, ) { buf := make([]byte, 0, 2048) writer := bytes.NewBuffer(buf) err := goGetTmpl.Execute(writer, struct { Module string // Go module name (without major) ImportURI string // Where go get will download from SourceURI string // Where browsers can go Branch string }{gpr.ImportPath, repo.GitURI(), repo.WebURI(), repo.DefaultBranch(), }) if err != nil { code := http.StatusInternalServerError http.Error(w, http.StatusText(code), code) log.Printf("request go-get: execute template: %#v: %v", gpr.Repo, err) } w.Write(writer.Bytes()) }