|
|
|
|
@ -4,7 +4,20 @@ |
|
|
|
|
|
|
|
|
|
package ipnlocal |
|
|
|
|
|
|
|
|
|
import "testing" |
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"context" |
|
|
|
|
"crypto/tls" |
|
|
|
|
"fmt" |
|
|
|
|
"net/http" |
|
|
|
|
"net/http/httptest" |
|
|
|
|
"net/url" |
|
|
|
|
"os" |
|
|
|
|
"path/filepath" |
|
|
|
|
"testing" |
|
|
|
|
|
|
|
|
|
"tailscale.com/ipn" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
func TestExpandProxyArg(t *testing.T) { |
|
|
|
|
type res struct { |
|
|
|
|
@ -31,3 +44,211 @@ func TestExpandProxyArg(t *testing.T) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestGetServeHandler(t *testing.T) { |
|
|
|
|
const serverName = "example.ts.net" |
|
|
|
|
conf1 := &ipn.ServeConfig{ |
|
|
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{ |
|
|
|
|
serverName + ":443": { |
|
|
|
|
Handlers: map[string]*ipn.HTTPHandler{ |
|
|
|
|
"/": {}, |
|
|
|
|
"/bar": {}, |
|
|
|
|
"/foo/": {}, |
|
|
|
|
"/foo/bar": {}, |
|
|
|
|
"/foo/bar/": {}, |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
tests := []struct { |
|
|
|
|
name string |
|
|
|
|
port uint16 // or 443 is zero
|
|
|
|
|
path string // http.Request.URL.Path
|
|
|
|
|
conf *ipn.ServeConfig |
|
|
|
|
want string // mountPoint
|
|
|
|
|
}{ |
|
|
|
|
{ |
|
|
|
|
name: "nothing", |
|
|
|
|
path: "/", |
|
|
|
|
conf: nil, |
|
|
|
|
want: "", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "root", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/", |
|
|
|
|
want: "/", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "root-other", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/other", |
|
|
|
|
want: "/", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "bar", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/bar", |
|
|
|
|
want: "/bar", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "foo-bar", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/foo/bar", |
|
|
|
|
want: "/foo/bar", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "foo-bar-slash", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/foo/bar/", |
|
|
|
|
want: "/foo/bar/", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "foo-bar-other", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/foo/bar/other", |
|
|
|
|
want: "/foo/bar/", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "foo-other", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/foo/other", |
|
|
|
|
want: "/foo/", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "foo-no-trailing-slash", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/foo", |
|
|
|
|
want: "/foo/", |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "dot-dots", |
|
|
|
|
conf: conf1, |
|
|
|
|
path: "/foo/../../../../../../../../etc/passwd", |
|
|
|
|
want: "/", |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
for _, tt := range tests { |
|
|
|
|
t.Run(tt.name, func(t *testing.T) { |
|
|
|
|
b := &LocalBackend{ |
|
|
|
|
serveConfig: tt.conf.View(), |
|
|
|
|
logf: t.Logf, |
|
|
|
|
} |
|
|
|
|
req := &http.Request{ |
|
|
|
|
URL: &url.URL{ |
|
|
|
|
Path: tt.path, |
|
|
|
|
}, |
|
|
|
|
TLS: &tls.ConnectionState{ServerName: serverName}, |
|
|
|
|
} |
|
|
|
|
port := tt.port |
|
|
|
|
if port == 0 { |
|
|
|
|
port = 443 |
|
|
|
|
} |
|
|
|
|
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{ |
|
|
|
|
DestPort: port, |
|
|
|
|
})) |
|
|
|
|
|
|
|
|
|
h, got, ok := b.getServeHandler(req) |
|
|
|
|
if (got != "") != ok { |
|
|
|
|
t.Fatalf("got ok=%v, but got mountPoint=%q", ok, got) |
|
|
|
|
} |
|
|
|
|
if h.Valid() != ok { |
|
|
|
|
t.Fatalf("got ok=%v, but valid=%v", ok, h.Valid()) |
|
|
|
|
} |
|
|
|
|
if got != tt.want { |
|
|
|
|
t.Errorf("got handler at mount %q, want %q", got, tt.want) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestServeFileOrDirectory(t *testing.T) { |
|
|
|
|
td := t.TempDir() |
|
|
|
|
writeFile := func(suffix, contents string) { |
|
|
|
|
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil { |
|
|
|
|
t.Fatal(err) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
writeFile("foo", "this is foo") |
|
|
|
|
writeFile("bar", "this is bar") |
|
|
|
|
os.MkdirAll(filepath.Join(td, "subdir"), 0700) |
|
|
|
|
writeFile("subdir/file-a", "this is A") |
|
|
|
|
writeFile("subdir/file-b", "this is B") |
|
|
|
|
writeFile("subdir/file-c", "this is C") |
|
|
|
|
|
|
|
|
|
contains := func(subs ...string) func([]byte, *http.Response) error { |
|
|
|
|
return func(resBody []byte, res *http.Response) error { |
|
|
|
|
for _, sub := range subs { |
|
|
|
|
if !bytes.Contains(resBody, []byte(sub)) { |
|
|
|
|
return fmt.Errorf("response body does not contain %q: %s", sub, resBody) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
isStatus := func(wantCode int) func([]byte, *http.Response) error { |
|
|
|
|
return func(resBody []byte, res *http.Response) error { |
|
|
|
|
if res.StatusCode != wantCode { |
|
|
|
|
return fmt.Errorf("response status = %d; want %d", res.StatusCode, wantCode) |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
isRedirect := func(wantLocation string) func([]byte, *http.Response) error { |
|
|
|
|
return func(resBody []byte, res *http.Response) error { |
|
|
|
|
switch res.StatusCode { |
|
|
|
|
case 301, 302, 303, 307, 308: |
|
|
|
|
if got := res.Header.Get("Location"); got != wantLocation { |
|
|
|
|
return fmt.Errorf("got Location = %q; want %q", got, wantLocation) |
|
|
|
|
} |
|
|
|
|
default: |
|
|
|
|
return fmt.Errorf("response status = %d; want redirect. body: %s", res.StatusCode, resBody) |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
b := &LocalBackend{} |
|
|
|
|
|
|
|
|
|
tests := []struct { |
|
|
|
|
req string |
|
|
|
|
mount string |
|
|
|
|
want func(resBody []byte, res *http.Response) error |
|
|
|
|
}{ |
|
|
|
|
// Mounted at /
|
|
|
|
|
|
|
|
|
|
{"/", "/", contains("foo", "bar", "subdir")}, |
|
|
|
|
{"/../../.../../../../../../../etc/passwd", "/", isStatus(404)}, |
|
|
|
|
{"/foo", "/", contains("this is foo")}, |
|
|
|
|
{"/bar", "/", contains("this is bar")}, |
|
|
|
|
{"/bar/inside-file", "/", isStatus(404)}, |
|
|
|
|
{"/subdir", "/", isRedirect("/subdir/")}, |
|
|
|
|
{"/subdir/", "/", contains("file-a", "file-b", "file-c")}, |
|
|
|
|
{"/subdir/file-a", "/", contains("this is A")}, |
|
|
|
|
{"/subdir/file-z", "/", isStatus(404)}, |
|
|
|
|
|
|
|
|
|
{"/doc", "/doc/", isRedirect("/doc/")}, |
|
|
|
|
{"/doc/", "/doc/", contains("foo", "bar", "subdir")}, |
|
|
|
|
{"/doc/../../.../../../../../../../etc/passwd", "/doc/", isStatus(404)}, |
|
|
|
|
{"/doc/foo", "/doc/", contains("this is foo")}, |
|
|
|
|
{"/doc/bar", "/doc/", contains("this is bar")}, |
|
|
|
|
{"/doc/bar/inside-file", "/doc/", isStatus(404)}, |
|
|
|
|
{"/doc/subdir", "/doc/", isRedirect("/doc/subdir/")}, |
|
|
|
|
{"/doc/subdir/", "/doc/", contains("file-a", "file-b", "file-c")}, |
|
|
|
|
{"/doc/subdir/file-a", "/doc/", contains("this is A")}, |
|
|
|
|
{"/doc/subdir/file-z", "/doc/", isStatus(404)}, |
|
|
|
|
} |
|
|
|
|
for _, tt := range tests { |
|
|
|
|
rec := httptest.NewRecorder() |
|
|
|
|
req := httptest.NewRequest("GET", tt.req, nil) |
|
|
|
|
b.serveFileOrDirectory(rec, req, td, tt.mount) |
|
|
|
|
if tt.want == nil { |
|
|
|
|
t.Errorf("no want for path %q", tt.req) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if err := tt.want(rec.Body.Bytes(), rec.Result()); err != nil { |
|
|
|
|
t.Errorf("error for req %q (mount %v): %v", tt.req, tt.mount, err) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|