Stable APIs for Go. https://go.code.as
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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