diff --git a/cmd/m3o-go-url/LICENSE b/cmd/m3o-go-url/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/cmd/m3o-go-url/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cmd/m3o-go-url/README.md b/cmd/m3o-go-url/README.md new file mode 100644 index 0000000..fd36d5f --- /dev/null +++ b/cmd/m3o-go-url/README.md @@ -0,0 +1,134 @@ +# M3O Go Vanity URL + +M3O Go Vanity URL is a simple Go server that sets the custom url `go.m3o.com` + +## Quickstart + +Install and run the binary: + +``` +$ go get -u github.com/m3o-go/cmd/m3o-go-url +$ # update vanity.yaml +$ m3o-go-url +$ # open http://localhost:8080 +``` + +### Google App Engine + +Install [gcloud](https://cloud.google.com/sdk/downloads) and install Go App Engine component: + +``` +$ gcloud components install app-engine-go +``` + +Setup a [custom domain](https://cloud.google.com/appengine/docs/standard/python/using-custom-domains-and-ssl) for your app. + +Get the application: +``` +git clone https://github.com/GoogleCloudPlatform/govanityurls +cd govanityurls +``` + +Edit `vanity.yaml` to add any number of git repos. E.g., `customdomain.com/portmidi` will +serve the [https://github.com/rakyll/portmidi](https://github.com/rakyll/portmidi) repo. + +``` +paths: + /portmidi: + repo: https://github.com/rakyll/portmidi +``` + +You can add as many rules as you wish. + +Deploy the app: + +``` +$ gcloud app deploy +``` + +That's it! You can use `go get` to get the package from your custom domain. + +``` +$ go get customdomain.com/portmidi +``` + +### Running in other environments + +You can also deploy this as an App Engine Flexible app by changing the +`app.yaml` file: + +``` +runtime: go +env: flex +``` + +This project is a normal Go HTTP server, so you can also incorporate the +handler into larger Go servers. + +## Configuration File + +``` +host: example.com +cache_max_age: 3600 +paths: + /foo: + repo: https://github.com/example/foo + display: "https://github.com/example/foo https://github.com/example/foo/tree/master{/dir} https://github.com/example/foo/blob/master{/dir}/{file}#L{line}" + vcs: git +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyRequiredDescription
cache_max_ageoptionalThe amount of time to cache package pages in seconds. Controls the max-age directive sent in the Cache-Control HTTP header.
hostoptionalHost name to use in meta tags. If omitted, uses the App Engine default version host or the Host header on non-App Engine Standard environments. You can use this option to fix the host when using this service behind a reverse proxy or a custom dispatch file.
pathsrequiredMap of paths to path configurations. Each key is a path that will point to the root of a repository hosted elsewhere. The fields are documented in the Path Configuration section below.
+ +### Path Configuration + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyRequiredDescription
displayoptionalThe last three fields of the go-source meta tag. If omitted, it is inferred from the code hosting service if possible.
reporequiredRoot URL of the repository as it would appear in go-import meta tag.
vcsrequired if ambiguousIf the version control system cannot be inferred (e.g. for Bitbucket or a custom domain), then this specifies the version control system as it would appear in go-import meta tag. This can be one of git, hg, svn, or bzr.
diff --git a/cmd/m3o-go-url/go.mod b/cmd/m3o-go-url/go.mod new file mode 100644 index 0000000..aa2290a --- /dev/null +++ b/cmd/m3o-go-url/go.mod @@ -0,0 +1,10 @@ +module github.com/m3o-go/cmd/m3o-go-url + +go 1.13 + +require ( + github.com/golang/protobuf v1.3.3 // indirect + golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect + google.golang.org/appengine v1.6.5 + gopkg.in/yaml.v2 v2.2.8 +) diff --git a/cmd/m3o-go-url/go.sum b/cmd/m3o-go-url/go.sum new file mode 100644 index 0000000..84f1a9c --- /dev/null +++ b/cmd/m3o-go-url/go.sum @@ -0,0 +1,13 @@ +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/cmd/m3o-go-url/handler.go b/cmd/m3o-go-url/handler.go new file mode 100644 index 0000000..c37c5a0 --- /dev/null +++ b/cmd/m3o-go-url/handler.go @@ -0,0 +1,225 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// govanityurls serves Go vanity URLs. +package main + +import ( + "errors" + "fmt" + "html/template" + "net/http" + "sort" + "strings" + + "gopkg.in/yaml.v2" +) + +type handler struct { + host string + cacheControl string + paths pathConfigSet +} + +type pathConfig struct { + path string + repo string + display string + vcs string +} + +func newHandler(config []byte) (*handler, error) { + var parsed struct { + Host string `yaml:"host,omitempty"` + CacheAge *int64 `yaml:"cache_max_age,omitempty"` + Paths map[string]struct { + Repo string `yaml:"repo,omitempty"` + Display string `yaml:"display,omitempty"` + VCS string `yaml:"vcs,omitempty"` + } `yaml:"paths,omitempty"` + } + if err := yaml.Unmarshal(config, &parsed); err != nil { + return nil, err + } + h := &handler{host: parsed.Host} + cacheAge := int64(86400) // 24 hours (in seconds) + if parsed.CacheAge != nil { + cacheAge = *parsed.CacheAge + if cacheAge < 0 { + return nil, errors.New("cache_max_age is negative") + } + } + h.cacheControl = fmt.Sprintf("public, max-age=%d", cacheAge) + for path, e := range parsed.Paths { + pc := pathConfig{ + path: strings.TrimSuffix(path, "/"), + repo: e.Repo, + display: e.Display, + vcs: e.VCS, + } + switch { + case e.Display != "": + // Already filled in. + case strings.HasPrefix(e.Repo, "https://github.com/"): + pc.display = fmt.Sprintf("%v %v/tree/master{/dir} %v/blob/master{/dir}/{file}#L{line}", e.Repo, e.Repo, e.Repo) + case strings.HasPrefix(e.Repo, "https://bitbucket.org"): + pc.display = fmt.Sprintf("%v %v/src/default{/dir} %v/src/default{/dir}/{file}#{file}-{line}", e.Repo, e.Repo, e.Repo) + } + switch { + case e.VCS != "": + // Already filled in. + if e.VCS != "bzr" && e.VCS != "git" && e.VCS != "hg" && e.VCS != "svn" { + return nil, fmt.Errorf("configuration for %v: unknown VCS %s", path, e.VCS) + } + case strings.HasPrefix(e.Repo, "https://github.com/"): + pc.vcs = "git" + default: + return nil, fmt.Errorf("configuration for %v: cannot infer VCS from %s", path, e.Repo) + } + h.paths = append(h.paths, pc) + } + sort.Sort(h.paths) + return h, nil +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + current := r.URL.Path + pc, subpath := h.paths.find(current) + if pc == nil && current == "/" { + h.serveIndex(w, r) + return + } + if pc == nil { + http.NotFound(w, r) + return + } + + w.Header().Set("Cache-Control", h.cacheControl) + if err := vanityTmpl.Execute(w, struct { + Import string + Subpath string + Repo string + Display string + VCS string + }{ + Import: h.Host(r) + pc.path, + Subpath: subpath, + Repo: pc.repo, + Display: pc.display, + VCS: pc.vcs, + }); err != nil { + http.Error(w, "cannot render the page", http.StatusInternalServerError) + } +} + +func (h *handler) serveIndex(w http.ResponseWriter, r *http.Request) { + host := h.Host(r) + handlers := make([]string, len(h.paths)) + for i, h := range h.paths { + handlers[i] = host + h.path + } + if err := indexTmpl.Execute(w, struct { + Host string + Handlers []string + }{ + Host: host, + Handlers: handlers, + }); err != nil { + http.Error(w, "cannot render the page", http.StatusInternalServerError) + } +} + +func (h *handler) Host(r *http.Request) string { + host := h.host + if host == "" { + host = defaultHost(r) + } + return host +} + +var indexTmpl = template.Must(template.New("index").Parse(` + +

{{.Host}}

+ + +`)) + +var vanityTmpl = template.Must(template.New("vanity").Parse(` + + + + + + + + +Nothing to see here; see the package on pkg.go.dev. + +`)) + +type pathConfigSet []pathConfig + +func (pset pathConfigSet) Len() int { + return len(pset) +} + +func (pset pathConfigSet) Less(i, j int) bool { + return pset[i].path < pset[j].path +} + +func (pset pathConfigSet) Swap(i, j int) { + pset[i], pset[j] = pset[j], pset[i] +} + +func (pset pathConfigSet) find(path string) (pc *pathConfig, subpath string) { + // Fast path with binary search to retrieve exact matches + // e.g. given pset ["/", "/abc", "/xyz"], path "/def" won't match. + i := sort.Search(len(pset), func(i int) bool { + return pset[i].path >= path + }) + if i < len(pset) && pset[i].path == path { + return &pset[i], "" + } + if i > 0 && strings.HasPrefix(path, pset[i-1].path+"/") { + return &pset[i-1], path[len(pset[i-1].path)+1:] + } + + // Slow path, now looking for the longest prefix/shortest subpath i.e. + // e.g. given pset ["/", "/abc/", "/abc/def/", "/xyz"/] + // * query "/abc/foo" returns "/abc/" with a subpath of "foo" + // * query "/x" returns "/" with a subpath of "x" + lenShortestSubpath := len(path) + var bestMatchConfig *pathConfig + + // After binary search with the >= lexicographic comparison, + // nothing greater than i will be a prefix of path. + max := i + for i := 0; i < max; i++ { + ps := pset[i] + if len(ps.path) >= len(path) { + // We previously didn't find the path by search, so any + // route with equal or greater length is NOT a match. + continue + } + sSubpath := strings.TrimPrefix(path, ps.path) + if len(sSubpath) < lenShortestSubpath { + subpath = sSubpath + lenShortestSubpath = len(sSubpath) + bestMatchConfig = &pset[i] + } + } + return bestMatchConfig, subpath +} diff --git a/cmd/m3o-go-url/handler_test.go b/cmd/m3o-go-url/handler_test.go new file mode 100644 index 0000000..8e4c7db --- /dev/null +++ b/cmd/m3o-go-url/handler_test.go @@ -0,0 +1,315 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "sort" + "testing" +) + +func TestHandler(t *testing.T) { + tests := []struct { + name string + config string + path string + + goImport string + goSource string + }{ + { + name: "explicit display", + config: "host: example.com\n" + + "paths:\n" + + " /portmidi:\n" + + " repo: https://github.com/rakyll/portmidi\n" + + " display: https://github.com/rakyll/portmidi _ _\n", + path: "/portmidi", + goImport: "example.com/portmidi git https://github.com/rakyll/portmidi", + goSource: "example.com/portmidi https://github.com/rakyll/portmidi _ _", + }, + { + name: "display GitHub inference", + config: "host: example.com\n" + + "paths:\n" + + " /portmidi:\n" + + " repo: https://github.com/rakyll/portmidi\n", + path: "/portmidi", + goImport: "example.com/portmidi git https://github.com/rakyll/portmidi", + goSource: "example.com/portmidi https://github.com/rakyll/portmidi https://github.com/rakyll/portmidi/tree/master{/dir} https://github.com/rakyll/portmidi/blob/master{/dir}/{file}#L{line}", + }, + { + name: "Bitbucket Mercurial", + config: "host: example.com\n" + + "paths:\n" + + " /gopdf:\n" + + " repo: https://bitbucket.org/zombiezen/gopdf\n" + + " vcs: hg\n", + path: "/gopdf", + goImport: "example.com/gopdf hg https://bitbucket.org/zombiezen/gopdf", + goSource: "example.com/gopdf https://bitbucket.org/zombiezen/gopdf https://bitbucket.org/zombiezen/gopdf/src/default{/dir} https://bitbucket.org/zombiezen/gopdf/src/default{/dir}/{file}#{file}-{line}", + }, + { + name: "Bitbucket Git", + config: "host: example.com\n" + + "paths:\n" + + " /mygit:\n" + + " repo: https://bitbucket.org/zombiezen/mygit\n" + + " vcs: git\n", + path: "/mygit", + goImport: "example.com/mygit git https://bitbucket.org/zombiezen/mygit", + goSource: "example.com/mygit https://bitbucket.org/zombiezen/mygit https://bitbucket.org/zombiezen/mygit/src/default{/dir} https://bitbucket.org/zombiezen/mygit/src/default{/dir}/{file}#{file}-{line}", + }, + { + name: "subpath", + config: "host: example.com\n" + + "paths:\n" + + " /portmidi:\n" + + " repo: https://github.com/rakyll/portmidi\n" + + " display: https://github.com/rakyll/portmidi _ _\n", + path: "/portmidi/foo", + goImport: "example.com/portmidi git https://github.com/rakyll/portmidi", + goSource: "example.com/portmidi https://github.com/rakyll/portmidi _ _", + }, + { + name: "subpath with trailing config slash", + config: "host: example.com\n" + + "paths:\n" + + " /portmidi/:\n" + + " repo: https://github.com/rakyll/portmidi\n" + + " display: https://github.com/rakyll/portmidi _ _\n", + path: "/portmidi/foo", + goImport: "example.com/portmidi git https://github.com/rakyll/portmidi", + goSource: "example.com/portmidi https://github.com/rakyll/portmidi _ _", + }, + } + for _, test := range tests { + h, err := newHandler([]byte(test.config)) + if err != nil { + t.Errorf("%s: newHandler: %v", test.name, err) + continue + } + s := httptest.NewServer(h) + resp, err := http.Get(s.URL + test.path) + if err != nil { + s.Close() + t.Errorf("%s: http.Get: %v", test.name, err) + continue + } + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + s.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("%s: status code = %s; want 200 OK", test.name, resp.Status) + } + if err != nil { + t.Errorf("%s: ioutil.ReadAll: %v", test.name, err) + continue + } + if got := findMeta(data, "go-import"); got != test.goImport { + t.Errorf("%s: meta go-import = %q; want %q", test.name, got, test.goImport) + } + if got := findMeta(data, "go-source"); got != test.goSource { + t.Errorf("%s: meta go-source = %q; want %q", test.name, got, test.goSource) + } + } +} + +func TestBadConfigs(t *testing.T) { + badConfigs := []string{ + "paths:\n" + + " /missingvcs:\n" + + " repo: https://bitbucket.org/zombiezen/gopdf\n", + "paths:\n" + + " /unknownvcs:\n" + + " repo: https://bitbucket.org/zombiezen/gopdf\n" + + " vcs: xyzzy\n", + "cache_max_age: -1\n" + + "paths:\n" + + " /portmidi:\n" + + " repo: https://github.com/rakyll/portmidi\n", + } + for _, config := range badConfigs { + _, err := newHandler([]byte(config)) + if err == nil { + t.Errorf("expected config to produce an error, but did not:\n%s", config) + } + } +} + +func findMeta(data []byte, name string) string { + var sep []byte + sep = append(sep, `" + } + return s + } + for _, test := range tests { + pset := make(pathConfigSet, len(test.paths)) + for i := range test.paths { + pset[i].path = test.paths[i] + } + sort.Sort(pset) + pc, subpath := pset.find(test.query) + var got string + if pc != nil { + got = pc.path + } + if got != test.want || subpath != test.subpath { + t.Errorf("pathConfigSet(%v).find(%q) = %v, %v; want %v, %v", + test.paths, test.query, emptyToNil(got), subpath, emptyToNil(test.want), test.subpath) + } + } +} + +func TestCacheHeader(t *testing.T) { + tests := []struct { + name string + config string + cacheControl string + }{ + { + name: "default", + cacheControl: "public, max-age=86400", + }, + { + name: "specify time", + config: "cache_max_age: 60\n", + cacheControl: "public, max-age=60", + }, + { + name: "zero", + config: "cache_max_age: 0\n", + cacheControl: "public, max-age=0", + }, + } + for _, test := range tests { + h, err := newHandler([]byte("paths:\n /portmidi:\n repo: https://github.com/rakyll/portmidi\n" + + test.config)) + if err != nil { + t.Errorf("%s: newHandler: %v", test.name, err) + continue + } + s := httptest.NewServer(h) + resp, err := http.Get(s.URL + "/portmidi") + if err != nil { + t.Errorf("%s: http.Get: %v", test.name, err) + continue + } + resp.Body.Close() + got := resp.Header.Get("Cache-Control") + if got != test.cacheControl { + t.Errorf("%s: Cache-Control header = %q; want %q", test.name, got, test.cacheControl) + } + } +} diff --git a/cmd/m3o-go-url/main.go b/cmd/m3o-go-url/main.go new file mode 100644 index 0000000..6b6d35f --- /dev/null +++ b/cmd/m3o-go-url/main.go @@ -0,0 +1,55 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "io/ioutil" + "log" + "net/http" + "os" +) + +func main() { + var configPath string + switch len(os.Args) { + case 1: + configPath = "vanity.yaml" + case 2: + configPath = os.Args[1] + default: + log.Fatal("usage: m3o-go-url [CONFIG]") + } + vanity, err := ioutil.ReadFile(configPath) + if err != nil { + log.Fatal(err) + } + h, err := newHandler(vanity) + if err != nil { + log.Fatal(err) + } + http.Handle("/", h) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func defaultHost(r *http.Request) string { + return r.Host +} diff --git a/cmd/m3o-go-url/vanity.yaml b/cmd/m3o-go-url/vanity.yaml new file mode 100644 index 0000000..c76ab7f --- /dev/null +++ b/cmd/m3o-go-url/vanity.yaml @@ -0,0 +1,5 @@ +host: go.m3o.com + +paths: + /: + repo: https://github.com/m3o/m3o-go