parent
							
								
									0d3efadf01
								
							
						
					
					
						commit
						3aa2a282f7
					
				|  | @ -715,6 +715,17 @@ Then value of ipfilteredby: | ||||||
| `aws`: IP from AWS goes to S3 directly | `aws`: IP from AWS goes to S3 directly | ||||||
| `awsregion`: IP from certain AWS regions goes to S3 directly, use together with `awsregion` | `awsregion`: IP from certain AWS regions goes to S3 directly, use together with `awsregion` | ||||||
| 
 | 
 | ||||||
|  | ### `alicdn` | ||||||
|  | 
 | ||||||
|  | `alicdn` storage middleware allows the registry to serve layers via a content delivery network provided by Alibaba Cloud. Alicdn requires the OSS storage driver. | ||||||
|  | 
 | ||||||
|  | | Parameter | Required | Description                                           | | ||||||
|  | |-----------|----------|-------------------------------------------------------| | ||||||
|  | | `baseurl` | yes      | The `SCHEME://HOST` at which Alicdn is served. | | ||||||
|  | | `authtype` | yes   | The URL authentication type for Alicdn, which should be `a`, `b` or `c`.     | | ||||||
|  | | `privatekey` | yes   | The URL authentication key for Alicdn.     | | ||||||
|  | | `duration` | no      | An integer and unit for the duration of the Alicdn session. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, or `h`.| | ||||||
|  | 
 | ||||||
| ### `redirect` | ### `redirect` | ||||||
| 
 | 
 | ||||||
| You can use the `redirect` storage middleware to specify a custom URL to a | You can use the `redirect` storage middleware to specify a custom URL to a | ||||||
|  |  | ||||||
|  | @ -0,0 +1,116 @@ | ||||||
|  | package middleware | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | 	storagedriver "github.com/docker/distribution/registry/storage/driver" | ||||||
|  | 	storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" | ||||||
|  | 
 | ||||||
|  | 	"github.com/denverdino/aliyungo/cdn/auth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // aliCdnStorageMiddleware provides a simple implementation of layerHandler that
 | ||||||
|  | // constructs temporary signed AliCDN URLs from the storagedriver layer URL,
 | ||||||
|  | // then issues HTTP Temporary Redirects to this AliCDN content URL.
 | ||||||
|  | type aliCdnStorageMiddleware struct { | ||||||
|  | 	storagedriver.StorageDriver | ||||||
|  | 	baseURL   string | ||||||
|  | 	urlSigner *auth.URLSigner | ||||||
|  | 	duration  time.Duration | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ storagedriver.StorageDriver = &aliCdnStorageMiddleware{} | ||||||
|  | 
 | ||||||
|  | // newAliCdnLayerHandler constructs and returns a new AliCDN
 | ||||||
|  | // LayerHandler implementation.
 | ||||||
|  | // Required options: baseurl, authtype, privatekey
 | ||||||
|  | // Optional options: duration
 | ||||||
|  | func newAliCdnStorageMiddleware(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (storagedriver.StorageDriver, error) { | ||||||
|  | 	// parse baseurl
 | ||||||
|  | 	base, ok := options["baseurl"] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("no baseurl provided") | ||||||
|  | 	} | ||||||
|  | 	baseURL, ok := base.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("baseurl must be a string") | ||||||
|  | 	} | ||||||
|  | 	if !strings.Contains(baseURL, "://") { | ||||||
|  | 		baseURL = "https://" + baseURL | ||||||
|  | 	} | ||||||
|  | 	if _, err := url.Parse(baseURL); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("invalid baseurl: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// parse authtype
 | ||||||
|  | 	at, ok := options["authtype"] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("no authtype provided") | ||||||
|  | 	} | ||||||
|  | 	authType, ok := at.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("authtype must be a string") | ||||||
|  | 	} | ||||||
|  | 	if authType != "a" && authType != "b" && authType != "c" { | ||||||
|  | 		return nil, fmt.Errorf("invalid authentication type") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// parse privatekey
 | ||||||
|  | 	pk, ok := options["privatekey"] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("no privatekey provided") | ||||||
|  | 	} | ||||||
|  | 	privateKey, ok := pk.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("privatekey must be a string") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	urlSigner := auth.NewURLSigner(authType, privateKey) | ||||||
|  | 
 | ||||||
|  | 	// parse duration
 | ||||||
|  | 	duration := 60 * time.Minute | ||||||
|  | 	d, ok := options["duration"] | ||||||
|  | 	if ok { | ||||||
|  | 		switch d := d.(type) { | ||||||
|  | 		case time.Duration: | ||||||
|  | 			duration = d | ||||||
|  | 		case string: | ||||||
|  | 			dur, err := time.ParseDuration(d) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("invalid duration: %s", err) | ||||||
|  | 			} | ||||||
|  | 			duration = dur | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &aliCdnStorageMiddleware{ | ||||||
|  | 		StorageDriver: storageDriver, | ||||||
|  | 		baseURL:       baseURL, | ||||||
|  | 		urlSigner:     urlSigner, | ||||||
|  | 		duration:      duration, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // URLFor attempts to find a url which may be used to retrieve the file at the given path.
 | ||||||
|  | // Returns an error if the file cannot be found.
 | ||||||
|  | func (ac *aliCdnStorageMiddleware) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { | ||||||
|  | 
 | ||||||
|  | 	if ac.StorageDriver.Name() != "oss" { | ||||||
|  | 		context.GetLogger(ctx).Warn("the AliCdn middleware does not support this backend storage driver") | ||||||
|  | 		return ac.StorageDriver.URLFor(ctx, path, options) | ||||||
|  | 	} | ||||||
|  | 	acURL, err := ac.urlSigner.Sign(ac.baseURL+path, time.Now().Add(ac.duration)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return acURL, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // init registers the alicdn layerHandler backend.
 | ||||||
|  | func init() { | ||||||
|  | 	storagemiddleware.Register("alicdn", storagemiddleware.InitFunc(newAliCdnStorageMiddleware)) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,97 @@ | ||||||
|  | package auth | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  | 	"syscall" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// Bits is the number of bits in a UUID
 | ||||||
|  | 	Bits = 128 | ||||||
|  | 
 | ||||||
|  | 	// Size is the number of bytes in a UUID
 | ||||||
|  | 	Size = Bits / 8 | ||||||
|  | 
 | ||||||
|  | 	format = "%08x%04x%04x%04x%012x" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// Loggerf can be used to override the default logging destination. Such
 | ||||||
|  | 	// log messages in this library should be logged at warning or higher.
 | ||||||
|  | 	Loggerf = func(format string, args ...interface{}) {} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // UUID represents a UUID value. UUIDs can be compared and set to other values
 | ||||||
|  | // and accessed by byte.
 | ||||||
|  | type UUID [Size]byte | ||||||
|  | 
 | ||||||
|  | // GenerateUUID creates a new, version 4 uuid.
 | ||||||
|  | func GenerateUUID() (u UUID) { | ||||||
|  | 	const ( | ||||||
|  | 		// ensures we backoff for less than 450ms total. Use the following to
 | ||||||
|  | 		// select new value, in units of 10ms:
 | ||||||
|  | 		// 	n*(n+1)/2 = d -> n^2 + n - 2d -> n = (sqrt(8d + 1) - 1)/2
 | ||||||
|  | 		maxretries = 9 | ||||||
|  | 		backoff    = time.Millisecond * 10 | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	var ( | ||||||
|  | 		totalBackoff time.Duration | ||||||
|  | 		count        int | ||||||
|  | 		retries      int | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	for { | ||||||
|  | 		// This should never block but the read may fail. Because of this,
 | ||||||
|  | 		// we just try to read the random number generator until we get
 | ||||||
|  | 		// something. This is a very rare condition but may happen.
 | ||||||
|  | 		b := time.Duration(retries) * backoff | ||||||
|  | 		time.Sleep(b) | ||||||
|  | 		totalBackoff += b | ||||||
|  | 
 | ||||||
|  | 		n, err := io.ReadFull(rand.Reader, u[count:]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			if retryOnError(err) && retries < maxretries { | ||||||
|  | 				count += n | ||||||
|  | 				retries++ | ||||||
|  | 				Loggerf("error generating version 4 uuid, retrying: %v", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// Any other errors represent a system problem. What did someone
 | ||||||
|  | 			// do to /dev/urandom?
 | ||||||
|  | 			panic(fmt.Errorf("error reading random number generator, retried for %v: %v", totalBackoff.String(), err)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	u[6] = (u[6] & 0x0f) | 0x40 // set version byte
 | ||||||
|  | 	u[8] = (u[8] & 0x3f) | 0x80 // set high order byte 0b10{8,9,a,b}
 | ||||||
|  | 
 | ||||||
|  | 	return u | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u UUID) String() string { | ||||||
|  | 	return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // retryOnError tries to detect whether or not retrying would be fruitful.
 | ||||||
|  | func retryOnError(err error) bool { | ||||||
|  | 	switch err := err.(type) { | ||||||
|  | 	case *os.PathError: | ||||||
|  | 		return retryOnError(err.Err) // unpack the target error
 | ||||||
|  | 	case syscall.Errno: | ||||||
|  | 		if err == syscall.EPERM { | ||||||
|  | 			// EPERM represents an entropy pool exhaustion, a condition under
 | ||||||
|  | 			// which we backoff and retry.
 | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return false | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								vendor/github.com/denverdino/aliyungo/cdn/auth/random_uuid_test.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										21
									
								
								vendor/github.com/denverdino/aliyungo/cdn/auth/random_uuid_test.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | package auth | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const iterations = 1000 | ||||||
|  | 
 | ||||||
|  | func TestUUID4Generation(t *testing.T) { | ||||||
|  | 	for i := 0; i < iterations; i++ { | ||||||
|  | 		u := GenerateUUID() | ||||||
|  | 
 | ||||||
|  | 		if u[6]&0xf0 != 0x40 { | ||||||
|  | 			t.Fatalf("version byte not correctly set: %v, %08b %08b", u, u[6], u[6]&0xf0) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if u[8]&0xc0 != 0x80 { | ||||||
|  | 			t.Fatalf("top order 8th byte not correctly set: %v, %b", u, u[8]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,80 @@ | ||||||
|  | package auth | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // An URLSigner provides URL signing utilities to sign URLs for Aliyun CDN
 | ||||||
|  | // resources.
 | ||||||
|  | // authentication document: https://help.aliyun.com/document_detail/85117.html
 | ||||||
|  | type URLSigner struct { | ||||||
|  | 	authType string | ||||||
|  | 	privKey  string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewURLSigner returns a new signer object.
 | ||||||
|  | func NewURLSigner(authType string, privKey string) *URLSigner { | ||||||
|  | 	return &URLSigner{ | ||||||
|  | 		authType: authType, | ||||||
|  | 		privKey:  privKey, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Sign returns a signed aliyuncdn url based on authentication type
 | ||||||
|  | func (s URLSigner) Sign(uri string, expires time.Time) (string, error) { | ||||||
|  | 	r, err := url.Parse(uri) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("unable to parse url: %s", uri) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch s.authType { | ||||||
|  | 	case "a": | ||||||
|  | 		return aTypeSign(r, s.privKey, expires), nil | ||||||
|  | 	case "b": | ||||||
|  | 		return bTypeSign(r, s.privKey, expires), nil | ||||||
|  | 	case "c": | ||||||
|  | 		return cTypeSign(r, s.privKey, expires), nil | ||||||
|  | 	default: | ||||||
|  | 		return "", fmt.Errorf("invalid authentication type") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // sign by A type authentication method.
 | ||||||
|  | // authentication document: https://help.aliyun.com/document_detail/85113.html
 | ||||||
|  | func aTypeSign(r *url.URL, privateKey string, expires time.Time) string { | ||||||
|  | 	//rand is a random uuid without "-"
 | ||||||
|  | 	rand := GenerateUUID().String() | ||||||
|  | 	// not use, "0" by default
 | ||||||
|  | 	uid := "0" | ||||||
|  | 	secret := fmt.Sprintf("%s-%d-%s-%s-%s", r.Path, expires.Unix(), rand, uid, privateKey) | ||||||
|  | 	hashValue := md5.Sum([]byte(secret)) | ||||||
|  | 	authKey := fmt.Sprintf("%d-%s-%s-%x", expires.Unix(), rand, uid, hashValue) | ||||||
|  | 	if r.RawQuery == "" { | ||||||
|  | 		return fmt.Sprintf("%s?auth_key=%s", r.String(), authKey) | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("%s&auth_key=%s", r.String(), authKey) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // sign by B type authentication method.
 | ||||||
|  | // authentication document: https://help.aliyun.com/document_detail/85114.html
 | ||||||
|  | func bTypeSign(r *url.URL, privateKey string, expires time.Time) string { | ||||||
|  | 	formatExp := expires.Format("200601021504") | ||||||
|  | 	secret := privateKey + formatExp + r.Path | ||||||
|  | 	hashValue := md5.Sum([]byte(secret)) | ||||||
|  | 	signURL := fmt.Sprintf("%s://%s/%s/%x%s?%s", r.Scheme, r.Host, formatExp, hashValue, r.Path, r.RawQuery) | ||||||
|  | 	return signURL | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // sign by C type authentication method.
 | ||||||
|  | // authentication document: https://help.aliyun.com/document_detail/85115.html
 | ||||||
|  | func cTypeSign(r *url.URL, privateKey string, expires time.Time) string { | ||||||
|  | 	hexExp := fmt.Sprintf("%x", expires.Unix()) | ||||||
|  | 	secret := privateKey + r.Path + hexExp | ||||||
|  | 	hashValue := md5.Sum([]byte(secret)) | ||||||
|  | 	signURL := fmt.Sprintf("%s://%s/%x/%s%s?%s", r.Scheme, r.Host, hashValue, hexExp, r.Path, r.RawQuery) | ||||||
|  | 	return signURL | ||||||
|  | } | ||||||
|  | @ -0,0 +1,53 @@ | ||||||
|  | package auth | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	testSignTime = time.Unix(1541064730, 0) | ||||||
|  | 	testPrivKey  = "12345678" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func assertEqual(t *testing.T, name string, x, y interface{}) { | ||||||
|  | 	if !reflect.DeepEqual(x, y) { | ||||||
|  | 		t.Errorf("%s: Not equal! Expected='%v', Actual='%v'\n", name, x, y) | ||||||
|  | 		t.FailNow() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestAtypeAuth(t *testing.T) { | ||||||
|  | 	r, _ := url.Parse("https://example.com/a?foo=bar") | ||||||
|  | 	url := aTypeTest(r, testPrivKey, testSignTime) | ||||||
|  | 	assertEqual(t, "testTypeA", "https://example.com/a?foo=bar&auth_key=1541064730-0-0-f9dd5ed1e274ab4b1d5f5745344bf28b", url) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestBtypeAuth(t *testing.T) { | ||||||
|  | 	signer := NewURLSigner("b", testPrivKey) | ||||||
|  | 	url, _ := signer.Sign("https://example.com/a?foo=bar", testSignTime) | ||||||
|  | 	assertEqual(t, "testTypeB", "https://example.com/201811011732/3a19d83a89ccb00a73212420791b0123/a?foo=bar", url) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestCtypeAuth(t *testing.T) { | ||||||
|  | 	signer := NewURLSigner("c", testPrivKey) | ||||||
|  | 	url, _ := signer.Sign("https://example.com/a?foo=bar", testSignTime) | ||||||
|  | 	assertEqual(t, "testTypeC", "https://example.com/7d6b308ce87beb16d9dba32d741220f6/5bdac81a/a?foo=bar", url) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func aTypeTest(r *url.URL, privateKey string, expires time.Time) string { | ||||||
|  | 	//rand equals "0" in test case
 | ||||||
|  | 	rand := "0" | ||||||
|  | 	uid := "0" | ||||||
|  | 	secret := fmt.Sprintf("%s-%d-%s-%s-%s", r.Path, expires.Unix(), rand, uid, privateKey) | ||||||
|  | 	hashValue := md5.Sum([]byte(secret)) | ||||||
|  | 	authKey := fmt.Sprintf("%d-%s-%s-%x", expires.Unix(), rand, uid, hashValue) | ||||||
|  | 	if r.RawQuery == "" { | ||||||
|  | 		return fmt.Sprintf("%s?auth_key=%s", r.String(), authKey) | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("%s&auth_key=%s", r.String(), authKey) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue