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.

533 lines
14 KiB

  1. package main
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "errors"
  6. "flag"
  7. "fmt"
  8. "io"
  9. "io/ioutil"
  10. "log"
  11. "net/http"
  12. "os"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "text/template"
  17. "time"
  18. "golang.org/x/crypto/acme/autocert"
  19. )
  20. var (
  21. httpFlag = flag.String("http", ":8080", "Serve HTTP at given address")
  22. httpsFlag = flag.String("https", "", "Serve HTTPS at given address")
  23. certFlag = flag.String("cert", "", "Use the provided TLS certificate")
  24. keyFlag = flag.String("key", "", "Use the provided TLS key")
  25. acmeFlag = flag.String("acme", "", "Auto-request TLS certs and store in given directory")
  26. )
  27. var httpServer = &http.Server{
  28. ReadTimeout: 30 * time.Second,
  29. WriteTimeout: 5 * time.Minute,
  30. }
  31. var httpClient = &http.Client{
  32. Timeout: 10 * time.Second,
  33. }
  34. var bulkClient = &http.Client{
  35. Timeout: 5 * time.Minute,
  36. }
  37. func main() {
  38. if err := run(); err != nil {
  39. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  40. os.Exit(1)
  41. }
  42. }
  43. func run() error {
  44. flag.Parse()
  45. http.HandleFunc("/", handler)
  46. if *httpFlag == "" && *httpsFlag == "" {
  47. return fmt.Errorf("must provide -http and/or -https")
  48. }
  49. if *acmeFlag != "" && *httpsFlag == "" {
  50. return fmt.Errorf("cannot use -acme without -https")
  51. }
  52. if *acmeFlag != "" && (*certFlag != "" || *keyFlag != "") {
  53. return fmt.Errorf("cannot provide -acme with -key or -cert")
  54. }
  55. if *acmeFlag == "" && (*httpsFlag != "" || *certFlag != "" || *keyFlag != "") && (*httpsFlag == "" || *certFlag == "" || *keyFlag == "") {
  56. return fmt.Errorf("-https -cert and -key must be used together")
  57. }
  58. ch := make(chan error, 2)
  59. if *acmeFlag != "" {
  60. // So a potential error is seen upfront.
  61. if err := os.MkdirAll(*acmeFlag, 0700); err != nil {
  62. return err
  63. }
  64. }
  65. if *httpFlag != "" && (*httpsFlag == "" || *acmeFlag == "") {
  66. server := *httpServer
  67. server.Addr = *httpFlag
  68. go func() {
  69. ch <- server.ListenAndServe()
  70. }()
  71. }
  72. if *httpsFlag != "" {
  73. server := *httpServer
  74. server.Addr = *httpsFlag
  75. if *acmeFlag != "" {
  76. m := autocert.Manager{
  77. ForceRSA: true,
  78. Prompt: autocert.AcceptTOS,
  79. Cache: autocert.DirCache(*acmeFlag),
  80. RenewBefore: 24 * 30 * time.Hour,
  81. HostPolicy: autocert.HostWhitelist(
  82. "localhost",
  83. "gopkg.in",
  84. "p1.gopkg.in",
  85. "p2.gopkg.in",
  86. "p3.gopkg.in",
  87. "mup.labix.org",
  88. ),
  89. Email: "gustavo@niemeyer.net",
  90. }
  91. server.TLSConfig = &tls.Config{
  92. GetCertificate: m.GetCertificate,
  93. }
  94. go func() {
  95. ch <- http.ListenAndServe(":80", m.HTTPHandler(nil))
  96. }()
  97. }
  98. go func() {
  99. ch <- server.ListenAndServeTLS(*certFlag, *keyFlag)
  100. }()
  101. }
  102. return <-ch
  103. }
  104. var gogetTemplate = template.Must(template.New("").Parse(`
  105. <html>
  106. <head>
  107. <meta name="go-import" content="{{.Original.GopkgRoot}} git https://{{.Original.GopkgRoot}}">
  108. {{$root := .GitHubRoot}}{{$tree := .GitHubTree}}<meta name="go-source" content="{{.Original.GopkgRoot}} _ https://{{$root}}/tree/{{$tree}}{/dir} https://{{$root}}/blob/{{$tree}}{/dir}/{file}#L{line}">
  109. </head>
  110. <body>
  111. go get {{.GopkgPath}}
  112. </body>
  113. </html>
  114. `))
  115. // Repo represents a source code repository on GitHub.
  116. type Repo struct {
  117. User string
  118. Name string
  119. SubPath string
  120. OldFormat bool // The old /v2/pkg format.
  121. MajorVersion Version
  122. // FullVersion is the best version in AllVersions that matches MajorVersion.
  123. // It defaults to InvalidVersion if there are no matches.
  124. FullVersion Version
  125. // AllVersions holds all versions currently available in the repository,
  126. // either coming from branch names or from tag names. Version zero (v0)
  127. // is only present in the list if it really exists in the repository.
  128. AllVersions VersionList
  129. // When there is a redirect in place, these are from the original request.
  130. RedirUser string
  131. RedirName string
  132. }
  133. // SetVersions records in the relevant fields the details about which
  134. // package versions are available in the repository.
  135. func (repo *Repo) SetVersions(all []Version) {
  136. repo.AllVersions = all
  137. for _, v := range repo.AllVersions {
  138. if v.Major == repo.MajorVersion.Major && v.Unstable == repo.MajorVersion.Unstable && repo.FullVersion.Less(v) {
  139. repo.FullVersion = v
  140. }
  141. }
  142. }
  143. // When there is a redirect in place, this will return the original repository
  144. // but preserving the data for the new repository.
  145. func (repo *Repo) Original() *Repo {
  146. if repo.RedirName == "" {
  147. return repo
  148. }
  149. orig := *repo
  150. orig.User = repo.RedirUser
  151. orig.Name = repo.RedirName
  152. return &orig
  153. }
  154. type repoBase struct {
  155. user string
  156. name string
  157. }
  158. var redirect = map[repoBase]repoBase{
  159. // https://github.com/go-fsnotify/fsnotify/issues/1
  160. {"", "fsnotify"}: {"fsnotify", "fsnotify"},
  161. }
  162. // GitHubRoot returns the repository root at GitHub, without a schema.
  163. func (repo *Repo) GitHubRoot() string {
  164. if repo.User == "" {
  165. return "github.com/go-" + repo.Name + "/" + repo.Name
  166. } else {
  167. return "github.com/" + repo.User + "/" + repo.Name
  168. }
  169. }
  170. // GitHubTree returns the repository tree name at GitHub for the selected version.
  171. func (repo *Repo) GitHubTree() string {
  172. if repo.FullVersion == InvalidVersion {
  173. return "master"
  174. }
  175. return repo.FullVersion.String()
  176. }
  177. // GopkgRoot returns the package root at gopkg.in, without a schema.
  178. func (repo *Repo) GopkgRoot() string {
  179. return repo.GopkgVersionRoot(repo.MajorVersion)
  180. }
  181. // GopkgPath returns the package path at gopkg.in, without a schema.
  182. func (repo *Repo) GopkgPath() string {
  183. return repo.GopkgVersionRoot(repo.MajorVersion) + repo.SubPath
  184. }
  185. // GopkgVersionRoot returns the package root in gopkg.in for the
  186. // provided version, without a schema.
  187. func (repo *Repo) GopkgVersionRoot(version Version) string {
  188. version.Minor = -1
  189. version.Patch = -1
  190. v := version.String()
  191. if repo.OldFormat {
  192. if repo.User == "" {
  193. return "gopkg.in/" + v + "/" + repo.Name
  194. } else {
  195. return "gopkg.in/" + repo.User + "/" + v + "/" + repo.Name
  196. }
  197. } else {
  198. if repo.User == "" {
  199. return "gopkg.in/" + repo.Name + "." + v
  200. } else {
  201. return "gopkg.in/" + repo.User + "/" + repo.Name + "." + v
  202. }
  203. }
  204. }
  205. 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]*)*)$`)
  206. 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]*)*)$`)
  207. func handler(resp http.ResponseWriter, req *http.Request) {
  208. if req.URL.Path == "/health-check" {
  209. resp.Write([]byte("ok"))
  210. return
  211. }
  212. log.Printf("%s requested %s", req.RemoteAddr, req.URL)
  213. if req.URL.Path == "/" {
  214. resp.Header().Set("Location", "http://labix.org/gopkg.in")
  215. resp.WriteHeader(http.StatusTemporaryRedirect)
  216. return
  217. }
  218. m := patternNew.FindStringSubmatch(req.URL.Path)
  219. oldFormat := false
  220. if m == nil {
  221. m = patternOld.FindStringSubmatch(req.URL.Path)
  222. if m == nil {
  223. sendNotFound(resp, "Unsupported URL pattern; see the documentation at gopkg.in for details.")
  224. return
  225. }
  226. // "/v2/name" <= "/name.v2"
  227. m[2], m[3] = m[3], m[2]
  228. oldFormat = true
  229. }
  230. if strings.Contains(m[3], ".") {
  231. sendNotFound(resp, "Import paths take the major version only (.%s instead of .%s); see docs at gopkg.in for the reasoning.",
  232. m[3][:strings.Index(m[3], ".")], m[3])
  233. return
  234. }
  235. repo := &Repo{
  236. User: m[1],
  237. Name: m[2],
  238. SubPath: m[4],
  239. OldFormat: oldFormat,
  240. FullVersion: InvalidVersion,
  241. }
  242. if r, ok := redirect[repoBase{repo.User, repo.Name}]; ok {
  243. repo.RedirUser, repo.RedirName = repo.User, repo.Name
  244. repo.User, repo.Name = r.user, r.name
  245. }
  246. var ok bool
  247. repo.MajorVersion, ok = parseVersion(m[3])
  248. if !ok {
  249. sendNotFound(resp, "Version %q improperly considered invalid; please warn the service maintainers.", m[3])
  250. return
  251. }
  252. var changed []byte
  253. var versions VersionList
  254. original, err := fetchRefs(repo)
  255. if err == nil {
  256. changed, versions, err = changeRefs(original, repo.MajorVersion)
  257. repo.SetVersions(versions)
  258. }
  259. switch err {
  260. case nil:
  261. // all ok
  262. case ErrNoRepo:
  263. sendNotFound(resp, "GitHub repository not found at https://%s", repo.GitHubRoot())
  264. return
  265. case ErrNoVersion:
  266. major := repo.MajorVersion
  267. suffix := ""
  268. if major.Unstable {
  269. major.Unstable = false
  270. suffix = unstableSuffix
  271. }
  272. v := major.String()
  273. 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)
  274. return
  275. default:
  276. resp.WriteHeader(http.StatusBadGateway)
  277. resp.Write([]byte(fmt.Sprintf("Cannot obtain refs from GitHub: %v", err)))
  278. return
  279. }
  280. if repo.SubPath == "/git-upload-pack" {
  281. proxyUploadPack(resp, req, repo)
  282. return
  283. }
  284. if repo.SubPath == "/info/refs" {
  285. resp.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
  286. resp.Write(changed)
  287. return
  288. }
  289. resp.Header().Set("Content-Type", "text/html")
  290. if req.FormValue("go-get") == "1" {
  291. // execute simple template when this is a go-get request
  292. err = gogetTemplate.Execute(resp, repo)
  293. if err != nil {
  294. log.Printf("error executing go get template: %s\n", err)
  295. }
  296. return
  297. }
  298. renderPackagePage(resp, req, repo)
  299. }
  300. func sendNotFound(resp http.ResponseWriter, msg string, args ...interface{}) {
  301. if len(args) > 0 {
  302. msg = fmt.Sprintf(msg, args...)
  303. }
  304. resp.WriteHeader(http.StatusNotFound)
  305. resp.Write([]byte(msg))
  306. }
  307. const refsSuffix = ".git/info/refs?service=git-upload-pack"
  308. func proxyUploadPack(resp http.ResponseWriter, req *http.Request, repo *Repo) {
  309. preq, err := http.NewRequest(req.Method, "https://"+repo.GitHubRoot()+"/git-upload-pack", req.Body)
  310. if err != nil {
  311. resp.WriteHeader(http.StatusInternalServerError)
  312. resp.Write([]byte(fmt.Sprintf("Cannot create GitHub request: %v", err)))
  313. return
  314. }
  315. preq.Header = req.Header
  316. presp, err := bulkClient.Do(preq)
  317. if err != nil {
  318. resp.WriteHeader(http.StatusBadGateway)
  319. resp.Write([]byte(fmt.Sprintf("Cannot obtain data pack from GitHub: %v", err)))
  320. return
  321. }
  322. defer presp.Body.Close()
  323. header := resp.Header()
  324. for key, values := range presp.Header {
  325. header[key] = values
  326. }
  327. resp.WriteHeader(presp.StatusCode)
  328. // Ignore errors. Dropped connections are usual and will make this fail.
  329. _, err = io.Copy(resp, presp.Body)
  330. if err != nil {
  331. log.Printf("Error copying data from GitHub: %v", err)
  332. }
  333. }
  334. var ErrNoRepo = errors.New("repository not found in GitHub")
  335. var ErrNoVersion = errors.New("version reference not found in GitHub")
  336. func fetchRefs(repo *Repo) (data []byte, err error) {
  337. resp, err := httpClient.Get("https://" + repo.GitHubRoot() + refsSuffix)
  338. if err != nil {
  339. return nil, fmt.Errorf("cannot talk to GitHub: %v", err)
  340. }
  341. defer resp.Body.Close()
  342. switch resp.StatusCode {
  343. case 200:
  344. // ok
  345. case 401, 404:
  346. return nil, ErrNoRepo
  347. default:
  348. return nil, fmt.Errorf("error from GitHub: %v", resp.Status)
  349. }
  350. data, err = ioutil.ReadAll(resp.Body)
  351. if err != nil {
  352. return nil, fmt.Errorf("error reading from GitHub: %v", err)
  353. }
  354. return data, err
  355. }
  356. func changeRefs(data []byte, major Version) (changed []byte, versions VersionList, err error) {
  357. var hlinei, hlinej int // HEAD reference line start/end
  358. var mlinei, mlinej int // master reference line start/end
  359. var vrefhash string
  360. var vrefname string
  361. var vrefv = InvalidVersion
  362. // Record all available versions, the locations of the master and HEAD lines,
  363. // and details of the best reference satisfying the requested major version.
  364. versions = make([]Version, 0)
  365. sdata := string(data)
  366. for i, j := 0, 0; i < len(data); i = j {
  367. size, err := strconv.ParseInt(sdata[i:i+4], 16, 32)
  368. if err != nil {
  369. return nil, nil, fmt.Errorf("cannot parse refs line size: %s", string(data[i:i+4]))
  370. }
  371. if size == 0 {
  372. size = 4
  373. }
  374. j = i + int(size)
  375. if j > len(sdata) {
  376. return nil, nil, fmt.Errorf("incomplete refs data received from GitHub")
  377. }
  378. if sdata[0] == '#' {
  379. continue
  380. }
  381. hashi := i + 4
  382. hashj := strings.IndexByte(sdata[hashi:j], ' ')
  383. if hashj < 0 || hashj != 40 {
  384. continue
  385. }
  386. hashj += hashi
  387. namei := hashj + 1
  388. namej := strings.IndexAny(sdata[namei:j], "\n\x00")
  389. if namej < 0 {
  390. namej = j
  391. } else {
  392. namej += namei
  393. }
  394. name := sdata[namei:namej]
  395. if name == "HEAD" {
  396. hlinei = i
  397. hlinej = j
  398. }
  399. if name == "refs/heads/master" {
  400. mlinei = i
  401. mlinej = j
  402. }
  403. if strings.HasPrefix(name, "refs/heads/v") || strings.HasPrefix(name, "refs/tags/v") {
  404. if strings.HasSuffix(name, "^{}") {
  405. // Annotated tag is peeled off and overrides the same version just parsed.
  406. name = name[:len(name)-3]
  407. }
  408. v, ok := parseVersion(name[strings.IndexByte(name, 'v'):])
  409. if ok && major.Contains(v) && (v == vrefv || !vrefv.IsValid() || vrefv.Less(v)) {
  410. vrefv = v
  411. vrefhash = sdata[hashi:hashj]
  412. vrefname = name
  413. }
  414. if ok {
  415. versions = append(versions, v)
  416. }
  417. }
  418. }
  419. // If there were absolutely no versions, and v0 was requested, accept the master as-is.
  420. if len(versions) == 0 && major == (Version{0, -1, -1, false}) {
  421. return data, nil, nil
  422. }
  423. // If the file has no HEAD line or the version was not found, report as unavailable.
  424. if hlinei == 0 || vrefhash == "" {
  425. return nil, nil, ErrNoVersion
  426. }
  427. var buf bytes.Buffer
  428. buf.Grow(len(data) + 256)
  429. // Copy the header as-is.
  430. buf.Write(data[:hlinei])
  431. // Extract the original capabilities.
  432. caps := ""
  433. if i := strings.Index(sdata[hlinei:hlinej], "\x00"); i > 0 {
  434. caps = strings.Replace(sdata[hlinei+i+1:hlinej-1], "symref=", "oldref=", -1)
  435. }
  436. // Insert the HEAD reference line with the right hash and a proper symref capability.
  437. var line string
  438. if strings.HasPrefix(vrefname, "refs/heads/") {
  439. if caps == "" {
  440. line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s\n", vrefhash, vrefname)
  441. } else {
  442. line = fmt.Sprintf("%s HEAD\x00symref=HEAD:%s %s\n", vrefhash, vrefname, caps)
  443. }
  444. } else {
  445. if caps == "" {
  446. line = fmt.Sprintf("%s HEAD\n", vrefhash)
  447. } else {
  448. line = fmt.Sprintf("%s HEAD\x00%s\n", vrefhash, caps)
  449. }
  450. }
  451. fmt.Fprintf(&buf, "%04x%s", 4+len(line), line)
  452. // Insert the master reference line.
  453. line = fmt.Sprintf("%s refs/heads/master\n", vrefhash)
  454. fmt.Fprintf(&buf, "%04x%s", 4+len(line), line)
  455. // Append the rest, dropping the original master line if necessary.
  456. if mlinei > 0 {
  457. buf.Write(data[hlinej:mlinei])
  458. buf.Write(data[mlinej:])
  459. } else {
  460. buf.Write(data[hlinej:])
  461. }
  462. return buf.Bytes(), versions, nil
  463. }