diff --git a/c.yml b/c.yml index ddc4335..1a32bac 100644 --- a/c.yml +++ b/c.yml @@ -48,3 +48,41 @@ m: expand: mail/u/2 maps: expand: maps.google.com + +z: + expand: zero.com + ssl_off: yes +zz: + expand: zero.ssl.on.com + ssl_off: no +l: + expand: localhost + ssl_off: yes + a: + port: 8080 + s: + expand: service +# Wildcard expansions allow you to query specific java versions. +# Example: "ak/11/j" -> "https://kafka.apache.org/11/javadoc/index.html?overview-summary.html" +ak: + expand: kafka.apache.org + hi: + expand: contact + "*": + d: + expand: documentation.html + j: + expand: javadoc/index.html?overview-summary.html + +# Note: chrome will block redirects to the "chrome://" schema. This makes sense, otherwise folks could abuse +# chrome://restart or change settings without users knowing. That said, this expansion is kept here as a reference +# on the "schema" usage. If you often need to use sftp:// or other schemas, this should work for you. +ch: + # expand: "/" + v: + expand: version # should expand to chrome://version + 'n': + expand: net-internals + d: + expand: '#dns' + schema: chrome \ No newline at end of file diff --git a/config.go b/config.go index 6a013fc..39d4bf8 100644 --- a/config.go +++ b/config.go @@ -24,9 +24,9 @@ const ( portKey = "port" passKey = "*" sslKey = "ssl_off" + schemaKey = "schema" httpsPrefix = "https:/" // second slash appended in expandPath() call httpPrefix = "http:/" // second slash appended in expandPath() call - ) // Sentinel value used to indicate set membership. @@ -36,6 +36,17 @@ var exists = struct{}{} // and easy test mocks. var Afero = &afero.Afero{Fs: afero.NewOsFs()} +// parseYamlString takes a raw string and attempts to load it. +func parseYamlString(config string) (*gabs.Container, error) { + d, jsonErr := yaml.YAMLToJSON([]byte(config)) + if jsonErr != nil { + fmt.Printf("Error encoding input to JSON.\n%s\n", jsonErr.Error()) + return nil, jsonErr + } + j, _ := gabs.ParseJSON(d) + return j, nil +} + // parseYaml takes a file name and returns a gabs config object. func parseYaml(fname string) (*gabs.Container, error) { data, err := Afero.ReadFile(fname) @@ -71,18 +82,19 @@ func validateConfig(c *gabs.Container) error { // Validate all children switch k { case - "expand", - "query": + expandKey, + schemaKey, + queryKey: // check that v is a string, else return error. if _, ok := v.Data().(string); !ok { errors = multierror.Append(errors, fmt.Errorf("expected string value for %T, got: %v", k, v.Data())) } - case "port": + case portKey: // check that v is a float64, else return error. if _, ok := v.Data().(float64); !ok { errors = multierror.Append(errors, fmt.Errorf("expected float64 value for %T, got: %v", k, v.Data())) } - case "ssl_off": + case sslKey: // check that v is a boolean, else return error. if _, ok := v.Data().(bool); !ok { errors = multierror.Append(errors, fmt.Errorf("expected bool value for %T, got: %v", k, v.Data())) diff --git a/config_test.go b/config_test.go index ac79255..a7908f9 100644 --- a/config_test.go +++ b/config_test.go @@ -3,6 +3,8 @@ package main import ( "testing" + "github.com/Jeffail/gabs/v2" + . "github.com/smartystreets/goconvey/convey" "github.com/spf13/afero" ) @@ -55,6 +57,65 @@ l: port: "not_int" ` +const cYaml = ` +e: + expand: example.com + a: + expand: apples + b: + expand: bananas +g: + expand: github.com + d: + expand: issmirnov/dotfiles + z: + expand: issmirnov/zap + s: + query: "search?q=" +z: + expand: zero.com + ssl_off: yes +zz: + expand: zero.ssl.on.com + ssl_off: no +l: + expand: localhost + ssl_off: yes + a: + port: 8080 + s: + expand: service +ak: + expand: kafka.apache.org + hi: + expand: contact + "*": + d: + expand: documentation.html + j: + expand: javadoc/index.html?overview-summary.html +wc: + expand: wildcard.com + "*": + "*": + "*": + four: + expand: "4" +ch: + # expand: "/" + v: + expand: version # should expand to chrome://version + 'n': + expand: net-internals + d: + expand: '#dns' + schema: chrome +` + +func loadTestYaml() (*gabs.Container, error) { + return parseYamlString(cYaml) +} + func TestParseYaml(t *testing.T) { Convey("Given a valid 'c.yml' file", t, func() { Afero = &afero.Afero{Fs: afero.NewMemMapFs()} diff --git a/go.mod b/go.mod index 2ce793b..290d985 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/smartystreets/assertions v1.0.0 // indirect github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 github.com/spf13/afero v1.2.2 + golang.org/x/exp/errors v0.0.0-20200901203048-c4f52b2c50aa golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect golang.org/x/text v0.3.2 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect diff --git a/go.sum b/go.sum index 138ade0..a1ec20c 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,9 @@ github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:s github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20200901203048-c4f52b2c50aa h1:i1+omYRtqpxiCaQJB4MQhUToKvMPFqUUJKvRiRp0gtE= +golang.org/x/exp/errors v0.0.0-20200901203048-c4f52b2c50aa h1:KuOC783xRi5lkhiU5v/+uXV++4UbZgc3o/STU05zqeg= +golang.org/x/exp/errors v0.0.0-20200901203048-c4f52b2c50aa/go.mod h1:YgqsNsAu4fTvlab/7uiYK9LJrCIzKg/NiZUIH1/ayqo= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= diff --git a/main_test.go b/main_test.go deleted file mode 100644 index f1d4db6..0000000 --- a/main_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/Jeffail/gabs/v2" - "github.com/ghodss/yaml" -) - -const cYaml = ` -e: - expand: example.com - a: - expand: apples - b: - expand: bananas -g: - expand: github.com - d: - expand: issmirnov/dotfiles - z: - expand: issmirnov/zap - s: - query: "search?q=" -z: - expand: zero.com - ssl_off: yes -zz: - expand: zero.ssl.on.com - ssl_off: no -l: - expand: localhost - ssl_off: yes - a: - port: 8080 - s: - expand: service -ak: - expand: kafka.apache.org - hi: - expand: contact - "*": - d: - expand: documentation.html - j: - expand: javadoc/index.html?overview-summary.html -wc: - expand: wildcard.com - "*": - "*": - "*": - four: - expand: "4" -` - -func loadTestYaml() (*gabs.Container, error) { - return parseYamlString(cYaml) -} - -func parseYamlString(config string) (*gabs.Container, error) { - d, jsonErr := yaml.YAMLToJSON([]byte(config)) - if jsonErr != nil { - fmt.Printf("Error encoding input to JSON.\n%s\n", jsonErr.Error()) - return nil, jsonErr - } - j, _ := gabs.ParseJSON(d) - return j, nil -} diff --git a/text.go b/text.go index 57af5ef..d5a16a4 100644 --- a/text.go +++ b/text.go @@ -59,7 +59,7 @@ func getPrefix(c *gabs.Container) (string, int, error) { return "", 0, fmt.Errorf("casting port key to float64 failed for %T:%v", p, p) } - return "", 0, fmt.Errorf("error in config, no key matching 'expand', 'query' or 'port' in %s", c.String()) + return "", 0, fmt.Errorf("error in config, no key matching 'expand', 'query', 'port' or 'schema' in %s", c.String()) } // expandPath takes a config, list of tokens (parsed from request) and the results buffer @@ -121,6 +121,7 @@ func isReserved(pathElem string) bool { queryKey, portKey, passKey, + schemaKey, sslKey: return true default: diff --git a/web.go b/web.go index 0bc742b..7565dae 100644 --- a/web.go +++ b/web.go @@ -39,7 +39,7 @@ func (cw ctxWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // IndexHandler handles all the non status expansions. -func IndexHandler(a *context, w http.ResponseWriter, r *http.Request) (int, error) { +func IndexHandler(ctx *context, w http.ResponseWriter, r *http.Request) (int, error) { var host string if r.Header.Get("X-Forwarded-Host") != "" { host = r.Header.Get("X-Forwarded-Host") @@ -51,20 +51,31 @@ func IndexHandler(a *context, w http.ResponseWriter, r *http.Request) (int, erro var ok bool // Check if host present in config. - children := a.config.ChildrenMap() + children := ctx.config.ChildrenMap() if hostConfig, ok = children[host]; !ok { return 404, fmt.Errorf("Shortcut '%s' not found in config.", host) } tokens := tokenize(host + r.URL.Path) + + // Set up handles on token and config. We might need to skip ahead if there's a custom schema set. + tokensStart := tokens.Front() + conf := ctx.config + var path bytes.Buffer if s := hostConfig.Path(sslKey).Data(); s != nil && s.(bool) { path.WriteString(httpPrefix) + } else if s := hostConfig.Path(schemaKey).Data(); s != nil && s.(string) != "" { + path.WriteString(hostConfig.Path(schemaKey).Data().(string) + ":/") + // move one token ahead to parse expansions correctly. + conf = conf.ChildrenMap()[tokensStart.Value.(string)] + tokensStart = tokensStart.Next() } else { + // Default to regular https prefix. path.WriteString(httpsPrefix) } - expandPath(a.config, tokens.Front(), &path) + expandPath(conf, tokensStart, &path) // send result http.Redirect(w, r, path.String(), http.StatusFound) diff --git a/web_test.go b/web_test.go index 02cb8e0..7887a0f 100644 --- a/web_test.go +++ b/web_test.go @@ -5,6 +5,8 @@ import ( "net/http/httptest" "testing" + "golang.org/x/exp/errors/fmt" + "github.com/ghodss/yaml" . "github.com/smartystreets/goconvey/convey" ) @@ -230,6 +232,66 @@ func TestIndexHandler(t *testing.T) { }) }) + Convey("When we GET http://ch/ with schema set to 'chrome' ", func() { + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + req.Host = "ch" + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + expected := "chrome://" + Convey(fmt.Sprintf("The result should be a 302 to %s", expected), func() { + So(rr.Code, ShouldEqual, http.StatusFound) + So(rr.Header().Get("Location"), ShouldEqual, expected) + }) + }) + + Convey("When we GET http://ch/foobar with schema set to 'chrome' where 'foobar' isn't in the config ", func() { + req, err := http.NewRequest("GET", "/foobar", nil) + So(err, ShouldBeNil) + req.Host = "ch" + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + expected := "chrome://foobar" + Convey(fmt.Sprintf("The result should be a 302 to %s", expected), func() { + So(rr.Code, ShouldEqual, http.StatusFound) + So(rr.Header().Get("Location"), ShouldEqual, expected) + }) + }) + + Convey("When we GET http://ch/v with schema set to 'chrome' ", func() { + req, err := http.NewRequest("GET", "/v", nil) + So(err, ShouldBeNil) + req.Host = "ch" + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + expected := "chrome://version" + Convey(fmt.Sprintf("The result should be a 302 to %s", expected), func() { + So(rr.Code, ShouldEqual, http.StatusFound) + So(rr.Header().Get("Location"), ShouldEqual, expected) + }) + }) + + Convey("When we GET http://ch/n/d with schema set to 'chrome' ", func() { + req, err := http.NewRequest("GET", "/n/d", nil) + So(err, ShouldBeNil) + req.Host = "ch" + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + expected := "chrome://net-internals/#dns" + Convey(fmt.Sprintf("The result should be a 302 to %s", expected), func() { + So(rr.Code, ShouldEqual, http.StatusFound) + So(rr.Header().Get("Location"), ShouldEqual, expected) + }) + }) + }) }