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)
+ }
+ }
+}