server.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /*
  2. * This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  5. */
  6. package main
  7. import (
  8. "bytes"
  9. "context"
  10. "fmt"
  11. "html/template"
  12. "log"
  13. "net"
  14. "net/http"
  15. "os"
  16. "regexp"
  17. "slices"
  18. "sort"
  19. "strconv"
  20. "strings"
  21. "time"
  22. dp "idio.link/go/depager/v2"
  23. )
  24. var goGetTmpl *template.Template
  25. func init() {
  26. const respTmpl = `<!doctype html>` +
  27. `<html>` +
  28. `<head>` +
  29. `<meta name="go-import"` +
  30. ` content="{{.Module}} git {{.ImportURI}}">` +
  31. `<meta name="go-source"` +
  32. ` content="{{.Module}}` +
  33. ` {{print .SourceURI}}` +
  34. ` {{.SourceURI}}/src/{{.Branch}}{/dir}` +
  35. ` {{.SourceURI}}/src/{{.Branch}}{/dir}/{file}#L{line}">` +
  36. `</head>` +
  37. `<body>` +
  38. `go get https://{{.Module}}` +
  39. `</body>` +
  40. `</html>`
  41. goGetTmpl =
  42. template.Must(template.New("resp").Parse(respTmpl))
  43. }
  44. func NewServer(ctx context.Context) *Server {
  45. accessToken, ok := os.LookupEnv("GOGS_ACCESS_TOKEN")
  46. if !ok {
  47. panic("Missing environment variable GOGS_ACCESS_TOKEN.")
  48. }
  49. s := &Server{
  50. ctx: ctx,
  51. client: NewGogsClient(ctx, accessToken),
  52. shutdownTimeout: 5 * time.Second,
  53. }
  54. s.svr = &http.Server{
  55. ReadTimeout: 10 * time.Second,
  56. WriteTimeout: 10 * time.Second,
  57. BaseContext: func(_ net.Listener) context.Context {
  58. return s.ctx
  59. },
  60. }
  61. return s
  62. }
  63. type Server struct {
  64. ctx context.Context
  65. svr *http.Server
  66. client *GogsClient
  67. projects Cached[map[string]GitRepo]
  68. shutdownTimeout time.Duration
  69. }
  70. func (s *Server) ListenAndServe(port int) error {
  71. s.svr.Addr = fmt.Sprintf(":%d", port)
  72. s.paths()
  73. return s.svr.ListenAndServe()
  74. }
  75. func (s *Server) Shutdown() error {
  76. ctx, cancel :=
  77. context.WithTimeout(s.ctx, s.shutdownTimeout)
  78. defer cancel()
  79. return s.svr.Shutdown(ctx)
  80. }
  81. func (s *Server) paths() {
  82. http.HandleFunc("/go/", s.handleGoGet())
  83. }
  84. func (s *Server) updateProjectCache(group string) error {
  85. // TODO Push caches down into Gogs client.
  86. if s.projects.Expired() {
  87. projects := map[string]GitRepo{}
  88. pager := FetchOrgRepos(s.client, group)
  89. for p := range pager.Iter() {
  90. projects[p.Path()] = p
  91. }
  92. if err := pager.LastErr(); err != nil {
  93. log.Printf("request go-get: unable to fetch gogs projects: %v", err)
  94. return err
  95. }
  96. s.projects = NewCached(&projects, 30*time.Minute)
  97. }
  98. return nil
  99. }
  100. func (s *Server) handleGoGet() http.HandlerFunc {
  101. gpr := new(GoProxyRequest)
  102. return func(w http.ResponseWriter, req *http.Request) {
  103. parseGoProxyPath(req, gpr)
  104. switch {
  105. // /@latest
  106. case gpr.Op == GoProxyRequestLatest:
  107. handleLatest(w, gpr, s.client)
  108. // /@v/list
  109. case gpr.Op == GoProxyRequestList:
  110. handleList(w, gpr, s.client)
  111. // /@v/$version.{.info,.mod,.zip}
  112. case gpr.Op == GoProxyRequestInfo ||
  113. gpr.Op == GoProxyRequestMod ||
  114. gpr.Op == GoProxyRequestZip:
  115. handleVersion(w, gpr, s.client)
  116. // go-get=1
  117. case req.URL.Query().Get("go-get") == "1":
  118. err := s.updateProjectCache(gpr.Host)
  119. if err != nil {
  120. code := http.StatusServiceUnavailable
  121. http.Error(w, http.StatusText(code), code)
  122. }
  123. repo, ok := (*s.projects.Value())[gpr.RepoPath]
  124. if !ok {
  125. code := http.StatusNotFound
  126. http.Error(w, http.StatusText(code), code)
  127. log.Printf("request go-get: unable to find git project for module %#v", gpr.Module)
  128. }
  129. handleGoGet(w, gpr, repo)
  130. default:
  131. log.Printf("malformed request: %s", req.URL.String())
  132. code := http.StatusBadRequest
  133. http.Error(w, http.StatusText(code), code)
  134. }
  135. }
  136. }
  137. func parseVersion(str string) ([]uint, error) {
  138. str = strings.TrimPrefix(str, "v")
  139. parts := strings.Split(str, ".")
  140. ver := make([]uint, 3)
  141. for i := 0; i < len(parts) && i < 3; i++ {
  142. v, err := strconv.ParseUint(parts[i], 10, 32)
  143. if err != nil {
  144. return nil, err
  145. }
  146. ver[i] = uint(v)
  147. }
  148. return ver, nil
  149. }
  150. func handleLatest(
  151. w http.ResponseWriter,
  152. gpr *GoProxyRequest,
  153. client *GogsClient,
  154. ) {
  155. latest := ""
  156. latestTag := make([]uint, 3)
  157. pager := ListTags(client, gpr.Host, gpr.Repo)
  158. for t := range pager.Iter() {
  159. tag, err := parseVersion(t.Name)
  160. if err != nil {
  161. log.Printf("error while parsing tag %#v: %v", t, err)
  162. continue
  163. }
  164. if slices.Compare(latestTag, tag) == -1 {
  165. latestTag = tag
  166. latest = t.Name
  167. }
  168. }
  169. if pager.LastErr() != nil {
  170. log.Printf("request /@latest: %v", pager.LastErr())
  171. code := http.StatusServiceUnavailable
  172. http.Error(w, http.StatusText(code), code)
  173. return
  174. }
  175. w.Write([]byte(latest + "\n"))
  176. }
  177. func handleVersion(
  178. w http.ResponseWriter,
  179. gpr *GoProxyRequest,
  180. client *GogsClient,
  181. ) {
  182. // TODO Dispatch to .info, .mod, or .zip handler
  183. switch gpr.Op {
  184. case GoProxyRequestInfo:
  185. handleVersionInfo(w, gpr, client)
  186. default:
  187. log.Printf("request %#v: not implemented", gpr.URI)
  188. code := http.StatusNotFound
  189. http.Error(w, http.StatusText(code), code)
  190. }
  191. }
  192. func findTag(
  193. pager dp.Pager[*GogsTag],
  194. tagName string,
  195. ) (tag *GogsTag, err error) {
  196. for t := range pager.Iter() {
  197. if t.Name == tagName {
  198. tag = t
  199. break
  200. }
  201. }
  202. if err = pager.LastErr(); err != nil {
  203. err = fmt.Errorf("get tag %#v: %w", tagName, err)
  204. return
  205. }
  206. return
  207. }
  208. func handleVersionInfo(
  209. w http.ResponseWriter,
  210. gpr *GoProxyRequest,
  211. client *GogsClient,
  212. ) {
  213. pager := ListTags(client, gpr.Host, gpr.Repo)
  214. tag, err := findTag(pager, gpr.Version)
  215. if err != nil {
  216. log.Printf("request %#v: no tag matching version %#v: %v", gpr.URI, gpr.Version, err)
  217. code := http.StatusServiceUnavailable
  218. http.Error(w, http.StatusText(code), code)
  219. return
  220. }
  221. json := fmt.Sprintf(
  222. `{"version": %#v, "timestamp": %#v}`,
  223. tag.Name,
  224. tag.Commit.Timestamp,
  225. )
  226. w.Write([]byte(json + "\n"))
  227. }
  228. func handleList(
  229. w http.ResponseWriter,
  230. gpr *GoProxyRequest,
  231. client *GogsClient,
  232. ) {
  233. verMajor := regexp.MustCompile(`^v[01]\.`)
  234. if v := gpr.VersionMajor; v != "" {
  235. verMajor =
  236. regexp.MustCompile(fmt.Sprintf("^%s\\.", v))
  237. }
  238. pager := ListTags(client, gpr.Host, gpr.Repo)
  239. tags := make([]*GogsTag, 0, 16)
  240. for t := range pager.Iter() {
  241. if !verMajor.MatchString(t.Name) {
  242. continue
  243. }
  244. tags = append(tags, t)
  245. }
  246. sort.SliceStable(tags, func(i, j int) bool {
  247. ti := tags[i].Name
  248. tj := tags[j].Name
  249. return strings.Compare(ti, tj) == -1
  250. })
  251. for _, t := range tags {
  252. w.Write([]byte(t.Name + "\n"))
  253. }
  254. if pager.LastErr() != nil {
  255. log.Printf("request %#v: list gogs releases: %v", gpr.URI, pager.LastErr())
  256. code := http.StatusServiceUnavailable
  257. http.Error(w, http.StatusText(code), code)
  258. }
  259. }
  260. func handleGoGet(
  261. w http.ResponseWriter,
  262. gpr *GoProxyRequest,
  263. repo GitRepo,
  264. ) {
  265. buf := make([]byte, 0, 2048)
  266. writer := bytes.NewBuffer(buf)
  267. err := goGetTmpl.Execute(writer, struct {
  268. Module string // Go module name (without major)
  269. ImportURI string // Where go get will download from
  270. SourceURI string // Where browsers can go
  271. Branch string
  272. }{gpr.ImportPath,
  273. repo.GitURI(),
  274. repo.WebURI(),
  275. repo.DefaultBranch(),
  276. })
  277. if err != nil {
  278. code := http.StatusInternalServerError
  279. http.Error(w, http.StatusText(code), code)
  280. log.Printf("request go-get: execute template: %#v: %v", gpr.Repo, err)
  281. }
  282. w.Write(writer.Bytes())
  283. }