Stable APIs for Go. https://go.code.as
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

414 lines
11 KiB

  1. package main
  2. import (
  3. "bytes"
  4. "errors"
  5. "flag"
  6. "fmt"
  7. "io/ioutil"
  8. "log"
  9. "net/http"
  10. "os"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "text/template"
  15. "time"
  16. )
  17. var httpFlag = flag.String("http", ":8080", "Serve HTTP at given address")
  18. var httpsFlag = flag.String("https", "", "Serve HTTPS at given address")
  19. var certFlag = flag.String("cert", "", "Use the provided TLS certificate")
  20. var keyFlag = flag.String("key", "", "Use the provided TLS key")
  21. func main() {
  22. if err := run(); err != nil {
  23. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  24. os.Exit(1)
  25. }
  26. }
  27. func run() error {
  28. flag.Parse()
  29. http.HandleFunc("/", handler)
  30. if *httpFlag == "" && *httpsFlag == "" {
  31. return fmt.Errorf("must provide -http and/or -https")
  32. }
  33. if (*httpsFlag != "" || *certFlag != "" || *keyFlag != "") && (*httpsFlag == "" || *certFlag == "" || *keyFlag == "") {
  34. return fmt.Errorf("-https -cert and -key must be used together")
  35. }
  36. ch := make(chan error, 2)
  37. if *httpFlag != "" {
  38. go func() {
  39. ch <- http.ListenAndServe(*httpFlag, nil)
  40. }()
  41. }
  42. if *httpsFlag != "" {
  43. go func() {
  44. ch <- http.ListenAndServeTLS(*httpsFlag, *certFlag, *keyFlag, nil)
  45. }()
  46. }
  47. return <-ch
  48. }
  49. var gogetTemplate = template.Must(template.New("").Parse(`
  50. <html>
  51. <head>
  52. <meta name="go-import" content="{{.GopkgRoot}} git https://{{.GopkgRoot}}">
  53. {{$root := .GitHubRoot}}{{$tree := .GitHubTree}}<meta name="go-source" content="{{.GopkgRoot}} _ https://{{$root}}/tree/{{$tree}}{/dir} https://{{$root}}/blob/{{$tree}}{/dir}/{file}#L{line}">
  54. </head>
  55. <body>
  56. go get {{.GopkgPath}}
  57. </body>
  58. </html>
  59. `))
  60. // Repo represents a source code repository on GitHub.
  61. type Repo struct {
  62. User string
  63. Name string
  64. SubPath string
  65. OldFormat bool // The old /v2/pkg format.
  66. MajorVersion Version
  67. // FullVersion is the best version in AllVersions that matches MajorVersion.
  68. // It defaults to InvalidVersion if there are no matches.
  69. FullVersion Version
  70. // AllVersions holds all versions currently available in the repository,
  71. // either coming from branch names or from tag names. Version zero (v0)
  72. // is only present in the list if it really exists in the repository.
  73. AllVersions VersionList
  74. }
  75. // SetVersions records in the relevant fields the details about which
  76. // package versions are available in the repository.
  77. func (repo *Repo) SetVersions(all []Version) {
  78. repo.AllVersions = all
  79. for _, v := range repo.AllVersions {
  80. if v.Major == repo.MajorVersion.Major && v.Unstable == repo.MajorVersion.Unstable && repo.FullVersion.Less(v) {
  81. repo.FullVersion = v
  82. }
  83. }
  84. }
  85. // GitHubRoot returns the repository root at GitHub, without a schema.
  86. func (repo *Repo) GitHubRoot() string {
  87. if repo.User == "" {
  88. return "github.com/go-" + repo.Name + "/" + repo.Name
  89. } else {
  90. return "github.com/" + repo.User + "/" + repo.Name
  91. }
  92. }
  93. // GitHubTree returns the repository tree name at GitHub for the selected version.
  94. func (repo *Repo) GitHubTree() string {
  95. if repo.FullVersion == InvalidVersion {
  96. return "master"
  97. }
  98. return repo.FullVersion.String()
  99. }
  100. // GopkgRoot returns the package root at gopkg.in, without a schema.
  101. func (repo *Repo) GopkgRoot() string {
  102. return repo.GopkgVersionRoot(repo.MajorVersion)
  103. }
  104. // GopkgPath returns the package path at gopkg.in, without a schema.
  105. func (repo *Repo) GopkgPath() string {
  106. return repo.GopkgVersionRoot(repo.MajorVersion) + repo.SubPath
  107. }
  108. // GopkgVersionRoot returns the package root in gopkg.in for the
  109. // provided version, without a schema.
  110. func (repo *Repo) GopkgVersionRoot(version Version) string {
  111. version.Minor = -1
  112. version.Patch = -1
  113. v := version.String()
  114. if repo.OldFormat {
  115. if repo.User == "" {
  116. return "gopkg.in/" + v + "/" + repo.Name
  117. } else {
  118. return "gopkg.in/" + repo.User + "/" + v + "/" + repo.Name
  119. }
  120. } else {
  121. if repo.User == "" {
  122. return "gopkg.in/" + repo.Name + "." + v
  123. } else {
  124. return "gopkg.in/" + repo.User + "/" + repo.Name + "." + v
  125. }
  126. }
  127. }
  128. var patternOld = regexp.MustCompile(`^/(?:([a-z0-9][-a-z0-9]+)/)?((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(-unstable)?)/([a-zA-Z][-a-zA-Z0-9]*)(?:\.git)?((?:/[a-zA-Z][-a-zA-Z0-9]*)*)$`)
  129. var patternNew = regexp.MustCompile(`^/(?:([a-zA-Z0-9][-a-zA-Z0-9]+)/)?([a-zA-Z][-.a-zA-Z0-9]*)\.((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(-unstable)?)(?:\.git)?((?:/[a-zA-Z0-9][-.a-zA-Z0-9]*)*)$`)
  130. func handler(resp http.ResponseWriter, req *http.Request) {
  131. if req.URL.Path == "/health-check" {
  132. resp.Write([]byte("ok"))
  133. return
  134. }
  135. log.Printf("%s requested %s", req.RemoteAddr, req.URL)
  136. if req.URL.Path == "/" {
  137. resp.Header().Set("Location", "http://labix.org/gopkg.in")
  138. resp.WriteHeader(http.StatusTemporaryRedirect)
  139. return
  140. }
  141. m := patternNew.FindStringSubmatch(req.URL.Path)
  142. oldFormat := false
  143. if m == nil {
  144. m = patternOld.FindStringSubmatch(req.URL.Path)
  145. if m == nil {
  146. sendNotFound(resp, "Unsupported URL pattern; see the documentation at gopkg.in for details.")
  147. return
  148. }
  149. // "/v2/name" <= "/name.v2"
  150. m[2], m[3] = m[3], m[2]
  151. oldFormat = true
  152. }
  153. if strings.Contains(m[3], ".") {
  154. sendNotFound(resp, "Import paths take the major version only (.%s instead of .%s); see docs at gopkg.in for the reasoning.",
  155. m[3][:strings.Index(m[3], ".")], m[3])
  156. return
  157. }
  158. repo := &Repo{
  159. User: m[1],
  160. Name: m[2],
  161. SubPath: m[5],
  162. OldFormat: oldFormat,
  163. FullVersion: InvalidVersion,
  164. }
  165. var ok bool
  166. repo.MajorVersion, ok = parseVersion(m[3])
  167. if !ok {
  168. sendNotFound(resp, "Version %q improperly considered invalid; please warn the service maintainers.", m[3])
  169. return
  170. }
  171. var changed []byte
  172. var versions VersionList
  173. original, err := fetchRefs(repo)
  174. if err == nil {
  175. changed, versions, err = changeRefs(original, repo.MajorVersion)
  176. repo.SetVersions(versions)
  177. }
  178. switch err {
  179. case nil:
  180. // all ok
  181. case ErrNoRepo:
  182. sendNotFound(resp, "GitHub repository not found at https://%s", repo.GitHubRoot())
  183. return
  184. case ErrNoVersion:
  185. major := repo.MajorVersion
  186. suffix := ""
  187. if major.Unstable {
  188. major.Unstable = false
  189. suffix = unstableSuffix
  190. }
  191. v := major.String()
  192. sendNotFound(resp, `GitHub repository at https://%s has no branch or tag "%s%s", "%s.N%s" or "%s.N.M%s"`, repo.GitHubRoot(), v, suffix, v, suffix, v, suffix)
  193. return
  194. default:
  195. resp.WriteHeader(http.StatusBadGateway)
  196. resp.Write([]byte(fmt.Sprintf("Cannot obtain refs from GitHub: %v", err)))
  197. return
  198. }
  199. if repo.SubPath == "/git-upload-pack" {
  200. resp.Header().Set("Location", "https://"+repo.GitHubRoot()+"/git-upload-pack")
  201. resp.WriteHeader(http.StatusMovedPermanently)
  202. return
  203. }
  204. if repo.SubPath == "/info/refs" {
  205. resp.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
  206. resp.Write(changed)
  207. return
  208. }
  209. resp.Header().Set("Content-Type", "text/html")
  210. if req.FormValue("go-get") == "1" {
  211. // execute simple template when this is a go-get request
  212. err = gogetTemplate.Execute(resp, repo)
  213. if err != nil {
  214. log.Printf("error executing go get template: %s\n", err)
  215. }
  216. return
  217. }
  218. renderPackagePage(resp, req, repo)
  219. }
  220. func sendNotFound(resp http.ResponseWriter, msg string, args ...interface{}) {
  221. if len(args) > 0 {
  222. msg = fmt.Sprintf(msg, args...)
  223. }
  224. resp.WriteHeader(http.StatusNotFound)
  225. resp.Write([]byte(msg))
  226. }
  227. var httpClient = &http.Client{Timeout: 10 * time.Second}
  228. const refsSuffix = ".git/info/refs?service=git-upload-pack"
  229. var ErrNoRepo = errors.New("repository not found in GitHub")
  230. var ErrNoVersion = errors.New("version reference not found in GitHub")
  231. func fetchRefs(repo *Repo) (data []byte, err error) {
  232. resp, err := httpClient.Get("https://" + repo.GitHubRoot() + refsSuffix)
  233. if err != nil {
  234. return nil, fmt.Errorf("cannot talk to GitHub: %v", err)
  235. }
  236. defer resp.Body.Close()
  237. switch resp.StatusCode {
  238. case 200:
  239. // ok
  240. case 401, 404:
  241. return nil, ErrNoRepo
  242. default:
  243. return nil, fmt.Errorf("error from GitHub: %v", resp.Status)
  244. }
  245. data, err = ioutil.ReadAll(resp.Body)
  246. if err != nil {
  247. return nil, fmt.Errorf("error reading from GitHub: %v", err)
  248. }
  249. return data, err
  250. }
  251. func changeRefs(data []byte, major Version) (changed []byte, versions VersionList, err error) {
  252. var hlinei, hlinej int // HEAD reference line start/end
  253. var mlinei, mlinej int // master reference line start/end
  254. var vrefhash string
  255. var vrefname string
  256. var vrefv = InvalidVersion
  257. // Record all available versions, the locations of the master and HEAD lines,
  258. // and details of the best reference satisfying the requested major version.
  259. versions = make([]Version, 0)
  260. sdata := string(data)
  261. for i, j := 0, 0; i < len(data); i = j {
  262. size, err := strconv.ParseInt(sdata[i:i+4], 16, 32)
  263. if err != nil {
  264. return nil, nil, fmt.Errorf("cannot parse refs line size: %s", string(data[i:i+4]))
  265. }
  266. if size == 0 {
  267. size = 4
  268. }
  269. j = i + int(size)
  270. if j > len(sdata) {
  271. return nil, nil, fmt.Errorf("incomplete refs data received from GitHub")
  272. }
  273. if sdata[0] == '#' {
  274. continue
  275. }
  276. hashi := i + 4
  277. hashj := strings.IndexByte(sdata[hashi:j], ' ')
  278. if hashj < 0 || hashj != 40 {
  279. continue
  280. }
  281. hashj += hashi
  282. namei := hashj + 1
  283. namej := strings.IndexAny(sdata[namei:j], "\n\x00")
  284. if namej < 0 {
  285. namej = j
  286. } else {
  287. namej += namei
  288. }
  289. name := sdata[namei:namej]
  290. if name == "HEAD" {
  291. hlinei = i
  292. hlinej = j
  293. }
  294. if name == "refs/heads/master" {
  295. mlinei = i
  296. mlinej = j
  297. }
  298. if strings.HasPrefix(name, "refs/heads/v") || strings.HasPrefix(name, "refs/tags/v") {
  299. if strings.HasSuffix(name, "^{}") {
  300. // Annotated tag is peeled off and overrides the same version just parsed.
  301. name = name[:len(name)-3]
  302. }
  303. v, ok := parseVersion(name[strings.IndexByte(name, 'v'):])
  304. if ok && major.Contains(v) && (v == vrefv || !vrefv.IsValid() || vrefv.Less(v)) {
  305. vrefv = v
  306. vrefhash = sdata[hashi:hashj]
  307. vrefname = name
  308. }
  309. if ok {
  310. versions = append(versions, v)
  311. }
  312. }
  313. }
  314. // If there were absolutely no versions, and v0 was requested, accept the master as-is.
  315. if len(versions) == 0 && major == (Version{0, -1, -1, false}) {
  316. return data, nil, nil
  317. }
  318. // If the file has no HEAD line or the version was not found, report as unavailable.
  319. if hlinei == 0 || vrefhash == "" {
  320. return nil, nil, ErrNoVersion
  321. }
  322. var buf bytes.Buffer
  323. buf.Grow(len(data) + 256)
  324. // Copy the header as-is.
  325. buf.Write(data[:hlinei])
  326. // Extract the original capabilities.
  327. caps := ""
  328. if i := strings.Index(sdata[hlinei:hlinej], "\x00"); i > 0 {
  329. caps = strings.Replace(sdata[hlinei+i+1:hlinej-1], "symref=", "oldref=", -1)
  330. }
  331. // Insert the HEAD reference line with the right hash and a proper symref capability.
  332. var line string
  333. if strings.HasPrefix(vrefname, "refs/heads/") {
  334. if caps == "" {
  335. line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s\n", vrefhash, vrefname)
  336. } else {
  337. line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s %s\n", vrefhash, vrefname, caps)
  338. }
  339. } else {
  340. if caps == "" {
  341. line = fmt.Sprintf("%s HEAD\n", vrefhash)
  342. } else {
  343. line = fmt.Sprintf("%s HEAD\x00%s\n", vrefhash, caps)
  344. }
  345. }
  346. fmt.Fprintf(&buf, "%04x%s", 4+len(line), line)
  347. // Insert the master reference line.
  348. line = fmt.Sprintf("%s refs/heads/master\n", vrefhash)
  349. fmt.Fprintf(&buf, "%04x%s", 4+len(line), line)
  350. // Append the rest, dropping the original master line if necessary.
  351. if mlinei > 0 {
  352. buf.Write(data[hlinej:mlinei])
  353. buf.Write(data[mlinej:])
  354. } else {
  355. buf.Write(data[hlinej:])
  356. }
  357. return buf.Bytes(), versions, nil
  358. }