353 lines
9.5 KiB
Go
353 lines
9.5 KiB
Go
/*
|
|
* Copyright 2020 The Dragonfly Authors
|
|
*
|
|
* 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 proxy
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"d7y.io/dragonfly/v2/client/config"
|
|
)
|
|
|
|
type testItem struct {
|
|
URL string
|
|
Direct bool
|
|
UseHTTPS bool
|
|
Redirect string
|
|
}
|
|
|
|
type testCase struct {
|
|
Error error
|
|
Rules []*config.ProxyRule
|
|
RegistryMirror *config.RegistryMirror
|
|
Items []testItem
|
|
}
|
|
|
|
func newTestCase() *testCase {
|
|
return &testCase{}
|
|
}
|
|
|
|
func (tc *testCase) WithRule(regx string, direct bool, useHTTPS bool, redirect string) *testCase {
|
|
if tc.Error != nil {
|
|
return tc
|
|
}
|
|
|
|
var r *config.ProxyRule
|
|
r, tc.Error = config.NewProxyRule(regx, useHTTPS, direct, redirect)
|
|
tc.Rules = append(tc.Rules, r)
|
|
return tc
|
|
}
|
|
|
|
func (tc *testCase) WithRegistryMirror(rawURL string, direct bool, dynamic bool, useProxies bool) *testCase {
|
|
if tc.Error != nil {
|
|
return tc
|
|
}
|
|
|
|
var u *url.URL
|
|
u, tc.Error = url.Parse(rawURL)
|
|
tc.RegistryMirror = &config.RegistryMirror{
|
|
Remote: &config.URL{URL: u},
|
|
DynamicRemote: dynamic,
|
|
Direct: direct,
|
|
UseProxies: useProxies,
|
|
}
|
|
return tc
|
|
}
|
|
|
|
func (tc *testCase) WithTest(url string, direct bool, useHTTPS bool, redirect string) *testCase {
|
|
tc.Items = append(tc.Items, testItem{url, direct, useHTTPS, redirect})
|
|
return tc
|
|
}
|
|
|
|
func (tc *testCase) Test(t *testing.T) {
|
|
a := assert.New(t)
|
|
if !a.Nil(tc.Error) {
|
|
return
|
|
}
|
|
tp, err := NewProxy(WithRules(tc.Rules))
|
|
if !a.Nil(err) {
|
|
return
|
|
}
|
|
for _, item := range tc.Items {
|
|
req, err := http.NewRequest("GET", item.URL, nil)
|
|
if !a.Nil(err) {
|
|
continue
|
|
}
|
|
if !a.Equal(tp.shouldUseDragonfly(req), !item.Direct) {
|
|
fmt.Println(item.URL)
|
|
}
|
|
if item.UseHTTPS {
|
|
a.Equal(req.URL.Scheme, "https")
|
|
} else {
|
|
a.Equal(req.URL.Scheme, "http")
|
|
}
|
|
if item.Redirect != "" {
|
|
a.Equal(item.Redirect, req.URL.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tc *testCase) TestMirror(t *testing.T) {
|
|
a := assert.New(t)
|
|
if !a.Nil(tc.Error) {
|
|
return
|
|
}
|
|
tp, err := NewProxy(WithRegistryMirror(tc.RegistryMirror), WithRules(tc.Rules))
|
|
if !a.Nil(err) {
|
|
return
|
|
}
|
|
for _, item := range tc.Items {
|
|
req, err := http.NewRequest("GET", item.URL, nil)
|
|
if !a.Nil(err) {
|
|
continue
|
|
}
|
|
if !a.Equal(tp.shouldUseDragonflyForMirror(req), !item.Direct) {
|
|
fmt.Println(item.URL)
|
|
}
|
|
if item.UseHTTPS {
|
|
a.Equal(req.URL.Scheme, "https")
|
|
} else {
|
|
a.Equal(req.URL.Scheme, "http")
|
|
}
|
|
if item.Redirect != "" {
|
|
a.Equal(item.Redirect, req.URL.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tc *testCase) TestEventStream(t *testing.T) {
|
|
a := assert.New(t)
|
|
if !a.Nil(tc.Error) {
|
|
return
|
|
}
|
|
tp, err := NewProxy(WithRules(tc.Rules))
|
|
if !a.Nil(err) {
|
|
return
|
|
}
|
|
tp.transport = &mockTransport{}
|
|
for _, item := range tc.Items {
|
|
req, err := http.NewRequest("GET", item.URL, nil)
|
|
if !a.Nil(err) {
|
|
continue
|
|
}
|
|
if !a.Equal(tp.shouldUseDragonfly(req), !item.Direct) {
|
|
fmt.Println(item.URL)
|
|
}
|
|
if item.UseHTTPS {
|
|
a.Equal(req.URL.Scheme, "https")
|
|
} else {
|
|
a.Equal(req.URL.Scheme, "http")
|
|
}
|
|
if item.Redirect != "" {
|
|
a.Equal(item.Redirect, req.URL.String())
|
|
}
|
|
if strings.Contains(req.URL.Path, "event-stream") {
|
|
batch := 10
|
|
_, span := tp.tracer.Start(req.Context(), config.SpanProxy)
|
|
w := &mockResponseWriter{}
|
|
req.Header.Set("X-Response-Batch", strconv.Itoa(batch))
|
|
if req.URL.Path == "/event-stream" {
|
|
req.Header.Set("X-Event-Stream", "true")
|
|
req.Header.Set("X-Response-Content-Length", "-1")
|
|
req.Header.Set("X-Response-Content-Encoding", "chunked")
|
|
req.Header.Set("X-Response-Content-Type", "text/event-stream")
|
|
tp.handleHTTP(span, w, req)
|
|
a.GreaterOrEqual(w.flushCount, batch)
|
|
} else {
|
|
req.Header.Set("X-Event-Stream", "false")
|
|
req.Header.Set("X-Response-Content-Length", strconv.Itoa(batch))
|
|
req.Header.Set("X-Response-Content-Encoding", "")
|
|
req.Header.Set("X-Response-Content-Type", "application/octet-stream")
|
|
tp.handleHTTP(span, w, req)
|
|
a.Less(w.flushCount, batch)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMatch(t *testing.T) {
|
|
newTestCase().
|
|
WithRule("/blobs/sha256/", false, false, "").
|
|
WithTest("http://index.docker.io/v2/blobs/sha256/xxx", false, false, "").
|
|
WithTest("http://index.docker.io/v2/auth", true, false, "").
|
|
Test(t)
|
|
|
|
newTestCase().
|
|
WithRule("/a/d", false, true, "").
|
|
WithRule("/a/b", true, false, "").
|
|
WithRule("/a", false, false, "").
|
|
WithRule("/a/c", true, false, "").
|
|
WithRule("/a/e", false, true, "").
|
|
WithTest("http://h/a", false, false, ""). // should match /a
|
|
WithTest("http://h/a/b", true, false, ""). // should match /a/b
|
|
WithTest("http://h/a/c", false, false, ""). // should match /a, not /a/c
|
|
WithTest("http://h/a/d", false, true, ""). // should match /a/d and use https
|
|
WithTest("http://h/a/e", false, false, ""). // should match /a, not /a/e
|
|
Test(t)
|
|
|
|
newTestCase().
|
|
WithRule("/a/f", false, false, "r").
|
|
WithTest("http://h/a/f", false, false, "http://r/a/f"). // should match /a/f and redirect
|
|
Test(t)
|
|
|
|
newTestCase().
|
|
WithTest("http://h/a", true, false, "").
|
|
TestMirror(t)
|
|
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, false).
|
|
WithTest("http://h/a", true, false, "").
|
|
TestMirror(t)
|
|
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, false).
|
|
WithTest("http://index.docker.io/v2/blobs/sha256/xxx", false, false, "").
|
|
TestMirror(t)
|
|
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", true, false, false).
|
|
WithTest("http://index.docker.io/v2/blobs/sha256/xxx", true, false, "").
|
|
TestMirror(t)
|
|
}
|
|
|
|
func TestMatchWithUseProxies(t *testing.T) {
|
|
// should direct as registry is set with direct=false and no proxies are defined
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, true).
|
|
WithTest("http://index.docker.io/v2/blobs/sha256/1", true, false, "").
|
|
TestMirror(t)
|
|
|
|
// should cache as registry is set with direct=false, and one proxy matches
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, true).
|
|
WithRule("/blobs/sha256/", false, false, "").
|
|
WithTest("http://index.docker.io/v2/blobs/sha256/2", false, false, "").
|
|
TestMirror(t)
|
|
|
|
// should direct as registry is set with direct=true, even if one proxy matches
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", true, false, true).
|
|
WithRule("/blobs/sha256/", false, false, "").
|
|
WithTest("http://index.docker.io/v2/blobs/sha256/3", true, false, "").
|
|
TestMirror(t)
|
|
}
|
|
|
|
func TestMatchWithRedirect(t *testing.T) {
|
|
// redirect as hostname, in direct mode
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, true).
|
|
WithRule("index.docker.io", false, false, "r").
|
|
WithTest("http://index.docker.io/1", false, false, "http://r/1").
|
|
TestMirror(t)
|
|
|
|
// redirect as hostname, in proxy mode
|
|
newTestCase().
|
|
WithRule("index.docker.io", false, false, "r").
|
|
WithTest("http://index.docker.io/2", false, false, "http://r/2").
|
|
Test(t)
|
|
|
|
// redirect as url, in direct mode
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, true).
|
|
WithRule("^http://index.docker.io/(.*)", false, false, "http://r/t/$1").
|
|
WithTest("http://index.docker.io/1", false, false, "http://r/t/1").
|
|
TestMirror(t)
|
|
|
|
// redirect as url, in proxy mode
|
|
newTestCase().
|
|
WithRule("^http://index.docker.io/(.*)", false, false, "http://r/t/$1").
|
|
WithTest("http://index.docker.io/2", false, false, "http://r/t/2").
|
|
Test(t)
|
|
|
|
// redirect as url, in direct mode, with HTTPS rewrite
|
|
newTestCase().
|
|
WithRegistryMirror("http://index.docker.io", false, false, true).
|
|
WithRule("^http://index.docker.io/(.*)", false, false, "https://r/t/$1").
|
|
WithTest("http://index.docker.io/1", false, true, "https://r/t/1").
|
|
TestMirror(t)
|
|
|
|
}
|
|
|
|
func TestProxyEventStream(t *testing.T) {
|
|
newTestCase().
|
|
WithRule("/blobs/sha256/", false, false, "").
|
|
WithTest("http://h/event-stream", true, false, "").
|
|
WithTest("http://h/not-event-stream", true, false, "").
|
|
TestEventStream(t)
|
|
}
|
|
|
|
type mockResponseWriter struct {
|
|
flushCount int
|
|
}
|
|
|
|
func (w *mockResponseWriter) Header() http.Header {
|
|
return http.Header{}
|
|
}
|
|
|
|
func (w *mockResponseWriter) Write(p []byte) (int, error) {
|
|
return len(string(p)), nil
|
|
}
|
|
|
|
func (w *mockResponseWriter) WriteHeader(int) {}
|
|
|
|
func (w *mockResponseWriter) Flush() {
|
|
w.flushCount++
|
|
}
|
|
|
|
type mockTransport struct{}
|
|
|
|
func (rt *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
batch, _ := strconv.Atoi(r.Header.Get("X-Response-Batch"))
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: &mockReadCloser{batch: batch},
|
|
Header: http.Header{
|
|
"Content-Length": []string{r.Header.Get("X-Response-Content-Length")},
|
|
"Content-Encoding": []string{r.Header.Get("X-Response-Content-Encoding")},
|
|
"Content-Type": []string{r.Header.Get("X-Response-Content-Type")},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type mockReadCloser struct {
|
|
batch int
|
|
count int
|
|
}
|
|
|
|
func (rc *mockReadCloser) Read(p []byte) (n int, err error) {
|
|
if rc.count == rc.batch {
|
|
return 0, io.EOF
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
p[0] = '0'
|
|
p = p[:1]
|
|
rc.count++
|
|
return len(p), nil
|
|
}
|
|
|
|
func (rc *mockReadCloser) Close() error {
|
|
return nil
|
|
}
|