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.

343 lines
8.5 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. )
  15. var httpFlag = flag.String("http", ":8080", "Serve HTTP at given address")
  16. var httpsFlag = flag.String("https", "", "Serve HTTPS at given address")
  17. var certFlag = flag.String("cert", "", "Use the provided TLS certificate")
  18. var keyFlag = flag.String("key", "", "Use the provided TLS key")
  19. func main() {
  20. if err := run(); err != nil {
  21. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  22. os.Exit(1)
  23. }
  24. }
  25. func run() error {
  26. flag.Parse()
  27. if len(flag.Args()) > 0 {
  28. return fmt.Errorf("too many arguments: %s", flag.Args()[0])
  29. }
  30. http.HandleFunc("/", handler)
  31. if *httpFlag == "" && *httpsFlag == "" {
  32. return fmt.Errorf("must provide -http and/or -https")
  33. }
  34. if (*httpsFlag != "" || *certFlag != "" || *keyFlag != "") && (*httpsFlag == "" || *certFlag == "" || *keyFlag == "") {
  35. return fmt.Errorf("-https -cert and -key must be used together")
  36. }
  37. ch := make(chan error, 2)
  38. if *httpFlag != "" {
  39. go func() {
  40. ch <- http.ListenAndServe(*httpFlag, nil)
  41. }()
  42. }
  43. if *httpsFlag != "" {
  44. go func() {
  45. ch <- http.ListenAndServeTLS(*httpsFlag, *certFlag, *keyFlag, nil)
  46. }()
  47. }
  48. return <-ch
  49. }
  50. var tmplProxy = template.Must(template.New("").Parse(`
  51. <html>
  52. <head>
  53. <meta name="go-import" content="{{.PkgRoot}} git {{.GitRoot}}">
  54. </head>
  55. <body>
  56. <script>
  57. window.location = "http://godoc.org/{{.PkgPath}}" + window.location.hash;
  58. </script>
  59. </body>
  60. </html>
  61. `))
  62. type Repo struct {
  63. User string // username or organization, might include forward slash
  64. Pkg string // repository name
  65. VersionStr string // version string ("v1")
  66. Path string // path inside repository
  67. Compat bool // when true, use old format
  68. Version Version // requested version (major only)
  69. Versions VersionList // available versions
  70. }
  71. func (repo *Repo) GitRoot() string {
  72. return "https://" + repo.PkgRoot()
  73. }
  74. func (repo *Repo) HubRoot() string {
  75. if len(repo.User) == 0 {
  76. return "https://github.com/go-" + repo.Pkg + "/" + repo.Pkg
  77. }
  78. return "https://github.com/" + repo.User + repo.Pkg
  79. }
  80. func (repo *Repo) PkgBase() string {
  81. return "gopkg.in/" + repo.User + repo.Pkg
  82. }
  83. func (repo *Repo) PkgRoot() string {
  84. if repo.Compat {
  85. return "gopkg.in/" + repo.User + repo.VersionStr + "/" + repo.Pkg
  86. }
  87. return repo.PkgBase() + "." + repo.VersionStr
  88. }
  89. func (repo *Repo) PkgPath() string {
  90. return repo.PkgRoot() + repo.Path
  91. }
  92. 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]*)*)$`)
  93. var patternNew = regexp.MustCompile(`^/([a-z0-9][-a-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]*)*)$`)
  94. func handler(resp http.ResponseWriter, req *http.Request) {
  95. if req.URL.Path == "/health-check" {
  96. resp.Write([]byte("ok"))
  97. return
  98. }
  99. log.Printf("%s requested %s", req.RemoteAddr, req.URL)
  100. if req.URL.Path == "/" {
  101. resp.Header().Set("Location", "http://labix.org/gopkg.in")
  102. resp.WriteHeader(http.StatusTemporaryRedirect)
  103. return
  104. }
  105. goget := req.FormValue("go-get") == "1"
  106. m := patternNew.FindStringSubmatch(req.URL.Path)
  107. compat := false
  108. if m == nil {
  109. m = patternOld.FindStringSubmatch(req.URL.Path)
  110. if m == nil {
  111. sendNotFound(resp, "Unsupported URL pattern; see the documentation at gopkg.in for details.")
  112. return
  113. }
  114. m[2], m[3] = m[3], m[2]
  115. compat = true
  116. }
  117. if strings.Contains(m[3], ".") {
  118. sendNotFound(resp, "Import paths take the major version only (.%s instead of .%s); see docs at gopkg.in for the reasoning.",
  119. m[3][:strings.Index(m[3], ".")], m[3])
  120. return
  121. }
  122. repo := &Repo{
  123. User: m[1],
  124. Pkg: m[2],
  125. VersionStr: m[3],
  126. Path: m[4],
  127. Compat: compat,
  128. }
  129. var ok bool
  130. repo.Version, ok = parseVersion(repo.VersionStr)
  131. if !ok {
  132. sendNotFound(resp, "Version %q improperly considered invalid; please warn the service maintainers.", m[3])
  133. return
  134. }
  135. // // Run this concurrently to avoid waiting later.
  136. // nameVersioned := nameHasVersion(repo)
  137. var err error
  138. var refs []byte
  139. refs, repo.Versions, err = hackedRefs(repo)
  140. switch err {
  141. case nil:
  142. // repo.GitRoot = "https://" + repo.PkgRoot
  143. // all ok
  144. case ErrNoRepo:
  145. // if <-nameVersioned {
  146. // v := repo.Version.String()
  147. // repo.GitRoot += "-" + v
  148. // repo.HubRoot += "-" + v
  149. // break
  150. // }
  151. sendNotFound(resp, "GitHub repository not found at %s", repo.HubRoot())
  152. return
  153. case ErrNoVersion:
  154. v := repo.Version.String()
  155. if repo.Version.Minor == -1 {
  156. sendNotFound(resp, `GitHub repository at %s has no branch or tag "%s", "%s.N" or "%s.N.M"`, repo.HubRoot(), v, v, v)
  157. } else if repo.Version.Patch == -1 {
  158. sendNotFound(resp, `GitHub repository at %s has no branch or tag "%s" or "%s.N"`, repo.HubRoot(), v, v)
  159. } else {
  160. sendNotFound(resp, `GitHub repository at %s has no branch or tag "%s"`, repo.HubRoot(), v)
  161. }
  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 m[4] == "/git-upload-pack" {
  169. resp.Header().Set("Location", repo.HubRoot()+"/git-upload-pack")
  170. resp.WriteHeader(http.StatusMovedPermanently)
  171. return
  172. }
  173. if m[4] == "/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 goget {
  180. // execute simple template when this is a go-get request
  181. err = tmplProxy.Execute(resp, repo)
  182. if err != nil {
  183. log.Printf("error executing tmplProxy: %s\n", err)
  184. }
  185. return
  186. }
  187. renderInterface(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. // TODO Timeouts for these http interactions. Use the new support coming in 1.3.
  197. const refsSuffix = ".git/info/refs?service=git-upload-pack"
  198. // // Obsolete. Drop this.
  199. // func nameHasVersion(repo *Repo) chan bool {
  200. // ch := make(chan bool, 1)
  201. // go func() {
  202. // resp, err := http.Head(repo.HubRoot + "-" + repo.Version.String() + refsSuffix)
  203. // if err != nil {
  204. // ch <- false
  205. // return
  206. // }
  207. // if resp.Body != nil {
  208. // resp.Body.Close()
  209. // }
  210. // ch <- resp.StatusCode == 200
  211. // }()
  212. // return ch
  213. // }
  214. var ErrNoRepo = errors.New("repository not found in github")
  215. var ErrNoVersion = errors.New("version reference not found in github")
  216. func hackedRefs(repo *Repo) (data []byte, versions []Version, err error) {
  217. resp, err := http.Get(repo.HubRoot() + refsSuffix)
  218. if err != nil {
  219. return nil, nil, fmt.Errorf("cannot talk to GitHub: %v", err)
  220. }
  221. switch resp.StatusCode {
  222. case 200:
  223. defer resp.Body.Close()
  224. case 401, 404:
  225. return nil, nil, ErrNoRepo
  226. default:
  227. return nil, nil, fmt.Errorf("error from GitHub: %v", resp.Status)
  228. }
  229. data, err = ioutil.ReadAll(resp.Body)
  230. if err != nil {
  231. return nil, nil, fmt.Errorf("error reading from GitHub: %v", err)
  232. }
  233. var mrefi, mrefj int
  234. var vrefi, vrefj int
  235. var vrefv = InvalidVersion
  236. var unversioned = true
  237. versions = make([]Version, 0)
  238. sdata := string(data)
  239. for i, j := 0, 0; i < len(data); i = j {
  240. size, err := strconv.ParseInt(sdata[i:i+4], 16, 32)
  241. if err != nil {
  242. return nil, nil, fmt.Errorf("cannot parse refs line size: %s", string(data[i:i+4]))
  243. }
  244. if size == 0 {
  245. size = 4
  246. }
  247. j = i + int(size)
  248. if j > len(sdata) {
  249. return nil, nil, fmt.Errorf("incomplete refs data received from GitHub")
  250. }
  251. if sdata[0] == '#' {
  252. continue
  253. }
  254. hashi := i + 4
  255. hashj := strings.IndexByte(sdata[hashi:j], ' ')
  256. if hashj < 0 || hashj != 40 {
  257. continue
  258. }
  259. hashj += hashi
  260. namei := hashj + 1
  261. namej := strings.IndexAny(sdata[namei:j], "\n\x00")
  262. if namej < 0 {
  263. namej = j
  264. } else {
  265. namej += namei
  266. }
  267. name := sdata[namei:namej]
  268. if name == "refs/heads/master" {
  269. mrefi = hashi
  270. mrefj = hashj
  271. }
  272. if strings.HasPrefix(name, "refs/heads/v") || strings.HasPrefix(name, "refs/tags/v") {
  273. v, ok := parseVersion(name[strings.IndexByte(name, 'v'):])
  274. if ok && repo.Version.Contains(v) && (!vrefv.IsValid() || vrefv.Less(v)) {
  275. vrefv = v
  276. vrefi = hashi
  277. vrefj = hashj
  278. }
  279. if ok {
  280. unversioned = false
  281. versions = append(versions, v)
  282. }
  283. }
  284. }
  285. // If there were absolutely no versions, and v0 was requested, accept the master as-is.
  286. if unversioned && repo.Version == (Version{0, -1, -1}) {
  287. return data, nil, nil
  288. }
  289. if mrefi == 0 || vrefi == 0 {
  290. return nil, nil, ErrNoVersion
  291. }
  292. copy(data[mrefi:mrefj], data[vrefi:vrefj])
  293. return data, versions, nil
  294. }