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.

326 lines
8.3 KiB

  1. package main
  2. import (
  3. "errors"
  4. "flag"
  5. "fmt"
  6. "io/ioutil"
  7. "log"
  8. "net/http"
  9. "os"
  10. "regexp"
  11. "strconv"
  12. "strings"
  13. "text/template"
  14. "time"
  15. )
  16. var httpFlag = flag.String("http", ":8080", "Serve HTTP at given address")
  17. var httpsFlag = flag.String("https", "", "Serve HTTPS at given address")
  18. var certFlag = flag.String("cert", "", "Use the provided TLS certificate")
  19. var keyFlag = flag.String("key", "", "Use the provided TLS key")
  20. var domainFlag = flag.String("domain", "gopkg.in", "Domain name")
  21. var userFlag = flag.String("username", "", "Github username")
  22. func main() {
  23. if err := run(); err != nil {
  24. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  25. os.Exit(1)
  26. }
  27. }
  28. func run() error {
  29. flag.Parse()
  30. //if len(flag.Args()) > 0 {
  31. // return fmt.Errorf("too many arguments: %s", flag.Args()[0])
  32. //}
  33. http.HandleFunc("/", handler)
  34. if *httpFlag == "" && *httpsFlag == "" {
  35. return fmt.Errorf("must provide -http and/or -https")
  36. }
  37. if (*httpsFlag != "" || *certFlag != "" || *keyFlag != "") && (*httpsFlag == "" || *certFlag == "" || *keyFlag == "") {
  38. return fmt.Errorf("-https -cert and -key must be used together")
  39. }
  40. ch := make(chan error, 2)
  41. if *httpFlag != "" {
  42. go func() {
  43. ch <- http.ListenAndServe(*httpFlag, nil)
  44. }()
  45. }
  46. if *httpsFlag != "" {
  47. go func() {
  48. ch <- http.ListenAndServeTLS(*httpsFlag, *certFlag, *keyFlag, nil)
  49. }()
  50. }
  51. return <-ch
  52. }
  53. var gogetTemplate = template.Must(template.New("").Parse(`
  54. <html>
  55. <head>
  56. <meta name="go-import" content="{{.GopkgRoot}} git https://{{.GopkgRoot}}">
  57. </head>
  58. <body>
  59. go get {{.GopkgPath}}
  60. </body>
  61. </html>
  62. `))
  63. type Repo struct {
  64. User string
  65. Name string
  66. SubPath string
  67. OldFormat bool
  68. MajorVersion Version
  69. AllVersions VersionList
  70. }
  71. // GitHubRoot returns the repository root at GitHub, without a schema.
  72. func (repo *Repo) GitHubRoot() string {
  73. if *userFlag != "" {
  74. return "github.com/" + *userFlag + "/" + repo.Name
  75. }
  76. if repo.User == "" {
  77. return "github.com/go-" + repo.Name + "/" + repo.Name
  78. }
  79. return "github.com/" + repo.User + "/" + repo.Name
  80. }
  81. // GopkgRoot returns the package root at gopkg.in, without a schema.
  82. func (repo *Repo) GopkgRoot() string {
  83. return repo.GopkgVersionRoot(repo.MajorVersion)
  84. }
  85. // GopkgRoot returns the package path at gopkg.in, without a schema.
  86. func (repo *Repo) GopkgPath() string {
  87. return repo.GopkgVersionRoot(repo.MajorVersion) + repo.SubPath
  88. }
  89. // GopkgVerisonRoot returns the package root in gopkg.in for the
  90. // provided version, without a schema.
  91. func (repo *Repo) GopkgVersionRoot(version Version) string {
  92. version.Minor = -1
  93. version.Patch = -1
  94. v := version.String()
  95. if repo.OldFormat {
  96. if repo.User == "" {
  97. return *domainFlag + "/" + v + "/" + repo.Name
  98. } else {
  99. return *domainFlag + "/" + repo.User + "/" + v + "/" + repo.Name
  100. }
  101. } else {
  102. if repo.User == "" {
  103. return *domainFlag + "/" + repo.Name + "." + v
  104. } else {
  105. return *domainFlag + "/" + repo.User + "/" + repo.Name + "." + v
  106. }
  107. }
  108. }
  109. var patternOld = regexp.MustCompile(`^/(?:([a-z0-9][-a-z0-9]+)/)?((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2})/([a-zA-Z][-a-zA-Z0-9]*)(?:\.git)?((?:/[a-zA-Z][-a-zA-Z0-9]*)*)$`)
  110. 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})(?:\.git)?((?:/[a-zA-Z][-a-zA-Z0-9]*)*)$`)
  111. func handler(resp http.ResponseWriter, req *http.Request) {
  112. if req.URL.Path == "/health-check" {
  113. resp.Write([]byte("ok"))
  114. return
  115. }
  116. log.Printf("%s requested %s", req.RemoteAddr, req.URL)
  117. if req.URL.Path == "/" {
  118. resp.Header().Set("Location", "http://labix.org/gopkg.in")
  119. resp.WriteHeader(http.StatusTemporaryRedirect)
  120. return
  121. }
  122. m := patternNew.FindStringSubmatch(req.URL.Path)
  123. oldFormat := false
  124. if m == nil {
  125. m = patternOld.FindStringSubmatch(req.URL.Path)
  126. if m == nil {
  127. sendNotFound(resp, "Unsupported URL pattern; see the documentation at gopkg.in for details.")
  128. return
  129. }
  130. m[2], m[3] = m[3], m[2]
  131. oldFormat = true
  132. }
  133. if strings.Contains(m[3], ".") {
  134. sendNotFound(resp, "Import paths take the major version only (.%s instead of .%s); see docs at gopkg.in for the reasoning.",
  135. m[3][:strings.Index(m[3], ".")], m[3])
  136. return
  137. }
  138. repo := &Repo{
  139. User: m[1],
  140. Name: m[2],
  141. SubPath: m[4],
  142. OldFormat: oldFormat,
  143. }
  144. var ok bool
  145. repo.MajorVersion, ok = parseVersion(m[3])
  146. if !ok {
  147. sendNotFound(resp, "Version %q improperly considered invalid; please warn the service maintainers.", m[3])
  148. return
  149. }
  150. var err error
  151. var refs []byte
  152. refs, repo.AllVersions, err = hackedRefs(repo)
  153. switch err {
  154. case nil:
  155. // all ok
  156. case ErrNoRepo:
  157. sendNotFound(resp, "GitHub repository not found at https://%s", repo.GitHubRoot())
  158. return
  159. case ErrNoVersion:
  160. v := repo.MajorVersion.String()
  161. sendNotFound(resp, `GitHub repository at https://%s has no branch or tag "%s", "%s.N" or "%s.N.M"`, repo.GitHubRoot(), v, v, v)
  162. return
  163. default:
  164. resp.WriteHeader(http.StatusBadGateway)
  165. resp.Write([]byte(fmt.Sprintf("Cannot obtain refs from GitHub: %v", err)))
  166. return
  167. }
  168. if repo.SubPath == "/git-upload-pack" {
  169. resp.Header().Set("Location", "https://"+repo.GitHubRoot()+"/git-upload-pack")
  170. resp.WriteHeader(http.StatusMovedPermanently)
  171. return
  172. }
  173. if repo.SubPath == "/info/refs" {
  174. resp.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
  175. resp.Write(refs)
  176. return
  177. }
  178. resp.Header().Set("Content-Type", "text/html")
  179. if req.FormValue("go-get") == "1" {
  180. // execute simple template when this is a go-get request
  181. err = gogetTemplate.Execute(resp, repo)
  182. if err != nil {
  183. log.Printf("error executing go get template: %s\n", err)
  184. }
  185. return
  186. }
  187. renderPackagePage(resp, req, repo)
  188. }
  189. func sendNotFound(resp http.ResponseWriter, msg string, args ...interface{}) {
  190. if len(args) > 0 {
  191. msg = fmt.Sprintf(msg, args...)
  192. }
  193. resp.WriteHeader(http.StatusNotFound)
  194. resp.Write([]byte(msg))
  195. }
  196. var httpClient = &http.Client{Timeout: 10 * time.Second}
  197. const refsSuffix = ".git/info/refs?service=git-upload-pack"
  198. var ErrNoRepo = errors.New("repository not found in github")
  199. var ErrNoVersion = errors.New("version reference not found in github")
  200. func hackedRefs(repo *Repo) (data []byte, versions []Version, err error) {
  201. resp, err := httpClient.Get("https://" + repo.GitHubRoot() + refsSuffix)
  202. if err != nil {
  203. return nil, nil, fmt.Errorf("cannot talk to GitHub: %v", err)
  204. }
  205. defer resp.Body.Close()
  206. switch resp.StatusCode {
  207. case 200:
  208. // ok
  209. case 401, 404:
  210. return nil, nil, ErrNoRepo
  211. default:
  212. return nil, nil, fmt.Errorf("error from GitHub: %v", resp.Status)
  213. }
  214. data, err = ioutil.ReadAll(resp.Body)
  215. if err != nil {
  216. return nil, nil, fmt.Errorf("error reading from GitHub: %v", err)
  217. }
  218. var mrefi, mrefj int
  219. var vrefi, vrefj int
  220. var vrefv = InvalidVersion
  221. versions = make([]Version, 0)
  222. sdata := string(data)
  223. for i, j := 0, 0; i < len(data); i = j {
  224. size, err := strconv.ParseInt(sdata[i:i+4], 16, 32)
  225. if err != nil {
  226. return nil, nil, fmt.Errorf("cannot parse refs line size: %s", string(data[i:i+4]))
  227. }
  228. if size == 0 {
  229. size = 4
  230. }
  231. j = i + int(size)
  232. if j > len(sdata) {
  233. return nil, nil, fmt.Errorf("incomplete refs data received from GitHub")
  234. }
  235. if sdata[0] == '#' {
  236. continue
  237. }
  238. hashi := i + 4
  239. hashj := strings.IndexByte(sdata[hashi:j], ' ')
  240. if hashj < 0 || hashj != 40 {
  241. continue
  242. }
  243. hashj += hashi
  244. namei := hashj + 1
  245. namej := strings.IndexAny(sdata[namei:j], "\n\x00")
  246. if namej < 0 {
  247. namej = j
  248. } else {
  249. namej += namei
  250. }
  251. name := sdata[namei:namej]
  252. if name == "refs/heads/master" {
  253. mrefi = hashi
  254. mrefj = hashj
  255. }
  256. if strings.HasPrefix(name, "refs/heads/v") || strings.HasPrefix(name, "refs/tags/v") {
  257. if strings.HasSuffix(name, "^{}") {
  258. // Annotated tag is peeled off and overrides the same version just parsed.
  259. name = name[:len(name)-3]
  260. }
  261. v, ok := parseVersion(name[strings.IndexByte(name, 'v'):])
  262. if ok && repo.MajorVersion.Contains(v) && (v == vrefv || !vrefv.IsValid() || vrefv.Less(v)) {
  263. vrefv = v
  264. vrefi = hashi
  265. vrefj = hashj
  266. }
  267. if ok {
  268. versions = append(versions, v)
  269. }
  270. }
  271. }
  272. // If there were absolutely no versions, and v0 was requested, accept the master as-is.
  273. if len(versions) == 0 && repo.MajorVersion == (Version{0, -1, -1}) {
  274. return data, nil, nil
  275. }
  276. if mrefi == 0 || vrefi == 0 {
  277. return nil, nil, ErrNoVersion
  278. }
  279. copy(data[mrefi:mrefj], data[vrefi:vrefj])
  280. return data, versions, nil
  281. }