internal/short: allow relative paths from the shortend URLs golang.org/s/key URL shortener supported only simple redirects from key to a link. This change allows the short link to have extra path elements, and computes the redirects by appending the extra path elements to the resolved link. For example, if golang.org/s/foo is configured to be resolved to example.com, golang.org/s/foo/bar will result in a redirects to example.com/bar. Change-Id: I6aa9f4aab2d5a74c76fda446a29aae998fe48ad6 Reviewed-on: https://go-review.googlesource.com/c/website/+/227654 Run-TryBot: Andrew Bonventre <andybons@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/internal/short/short.go b/internal/short/short.go index c04e789..0b81ef7 100644 --- a/internal/short/short.go +++ b/internal/short/short.go
@@ -21,6 +21,7 @@ "net/http" "net/url" "regexp" + "strings" "cloud.google.com/go/datastore" "golang.org/x/website/internal/memcache" @@ -61,14 +62,16 @@ } // linkHandler services requests to short URLs. -// http://golang.org/s/key +// http://golang.org/s/key[/remaining/path] // It consults memcache and datastore for the Link for key. // It then sends a redirects or an error message. +// If the remaining path part is not empty, the redirects +// will be the relative path from the resolved Link. func (h server) linkHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - key := r.URL.Path[len(prefix)+1:] - if !validKey.MatchString(key) { + key, remainingPath, err := extractKey(r) + if err != nil { // invalid key or url http.Error(w, "not found", http.StatusNotFound) return } @@ -96,7 +99,28 @@ } } - http.Redirect(w, r, link.Target, http.StatusFound) + target := link.Target + if remainingPath != "" { + target += remainingPath + } + http.Redirect(w, r, target, http.StatusFound) +} + +func extractKey(r *http.Request) (key, remainingPath string, err error) { + path := r.URL.Path + if !strings.HasPrefix(path, prefix+"/") { + return "", "", errors.New("invalid path") + } + + key, remainingPath = path[len(prefix)+1:], "" + if slash := strings.Index(key, "/"); slash > 0 { + key, remainingPath = key[:slash], key[slash:] + } + + if !validKey.MatchString(key) { + return "", "", errors.New("invalid key") + } + return key, remainingPath, nil } var adminTemplate = template.Must(template.New("admin").Parse(templateHTML))
diff --git a/internal/short/short_test.go b/internal/short/short_test.go new file mode 100644 index 0000000..5ae4443 --- /dev/null +++ b/internal/short/short_test.go
@@ -0,0 +1,39 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build golangorg + +package short + +import ( + "net/http/httptest" + "testing" +) + +func TestExtractKey(t *testing.T) { + testCases := []struct { + in string + wantKey, wantRemaining string + wantErr bool + }{ + {in: "/s/foo", wantKey: "foo", wantRemaining: ""}, + {in: "/s/foo/", wantKey: "foo", wantRemaining: "/"}, + {in: "/s/foo/bar/", wantKey: "foo", wantRemaining: "/bar/"}, + {in: "/s/foo.bar/baz", wantKey: "foo.bar", wantRemaining: "/baz"}, + {in: "/s/s/s/s", wantKey: "s", wantRemaining: "/s/s"}, + {in: "/", wantErr: true}, + {in: "/s/", wantErr: true}, + {in: "/s", wantErr: true}, + {in: "/t/foo", wantErr: true}, + {in: "/s/foo*", wantErr: true}, + } + + for _, tc := range testCases { + req := httptest.NewRequest("GET", tc.in, nil) + gotKey, gotRemaining, gotErr := extractKey(req) + if gotKey != tc.wantKey || gotRemaining != tc.wantRemaining || (gotErr != nil) != tc.wantErr { + t.Errorf("extractKey(%q) = (%q, %q, %v), want (%q, %q, err=%v)", tc.in, gotKey, gotRemaining, gotErr, tc.wantKey, tc.wantRemaining, tc.wantErr) + } + } +}