├── .github └── workflows │ └── go-cross-build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── README_cn.md ├── auth.go ├── cmd └── crproxy │ └── main.go ├── credentials.go ├── crproxy.go ├── crproxy_blob.go ├── crproxy_control.go ├── crproxy_manifest.go ├── ctx_value.go ├── examples ├── default │ ├── .gitignore │ ├── README.md │ ├── docker-compose.yml │ ├── html │ │ └── index.html │ ├── nginx │ │ └── default.conf │ ├── registry │ │ └── config.yml │ ├── reload.sh │ ├── setup-alias.sh │ ├── setup-gateway.sh │ ├── start.sh │ └── update-tls.sh └── simple │ └── README.md ├── go.mod ├── go.sum ├── hang_manager.go ├── hang_manager_test.go ├── internal ├── acme │ └── acme.go ├── maps │ └── maps.go └── server │ └── server.go ├── logo ├── LICENSE ├── crproxy.png └── crproxy.svg ├── storage └── driver │ ├── obs │ ├── doc.go │ └── obs.go │ └── oss │ ├── doc.go │ └── oss.go ├── sync.go ├── utils.go └── utils_test.go /.github/workflows/go-cross-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.21 16 | - name: Build Cross Platform 17 | uses: wzshiming/action-go-build-cross-plantform@v1 18 | - name: Upload Release Assets 19 | uses: wzshiming/action-upload-release-assets@v1 20 | env: 21 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Log into registry 23 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 24 | - name: Upload Release Images 25 | uses: wzshiming/action-upload-release-images@v1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | WORKDIR /go/src/github.com/wzshiming/crproxy/ 3 | COPY . . 4 | ENV CGO_ENABLED=0 5 | RUN go install ./cmd/crproxy 6 | 7 | FROM alpine 8 | EXPOSE 8080 9 | COPY --from=builder /go/bin/crproxy /usr/local/bin/ 10 | ENTRYPOINT [ "/usr/local/bin/crproxy" ] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shiming Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CRProxy (Container Registry Proxy) 2 | 3 | 4 | 5 | CRProxy is a generic image proxy 6 | 7 | Add the prefix `m.daocloud.io/` to all places that need to use images 8 | 9 | - [English](https://github.com/wzshiming/crproxy/blob/master/README.md) 10 | - [简体中文](https://github.com/wzshiming/crproxy/blob/master/README_cn.md) 11 | 12 | ## m.daocloud.io 13 | 14 | you can deploy your own image proxy server if you need to. 15 | 16 | [Refer to](https://github.com/wzshiming/crproxy/tree/master/examples/default) 17 | 18 | ## crproxy + registry(pull through cache mode) 19 | 20 | - for organizations and companies which need to serve big clusters 21 | - [container-image-mirror](https://blog.geekcity.tech/articles/kubernetes/argocd/container-image-mirror/) 22 | * deploy into k8s with argocd 23 | * storage with pvc, s3(minio/oss compatible) 24 | * bind ssl with cert-manager and ingress 25 | 26 | ## On Docker 27 | 28 | Just add the prefix `m.daocloud.io/` 29 | 30 | ``` bash 31 | docker pull m.daocloud.io/docker.io/library/busybox 32 | ``` 33 | 34 | ## On Kubernetes 35 | 36 | Just add the prefix `m.daocloud.io/` 37 | 38 | ``` yaml 39 | image: m.daocloud.io/docker.io/library/busybox 40 | ``` 41 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # CRProxy (Container Registry Proxy) 2 | 3 | 4 | 5 | CRProxy 是一个通用的 Image 代理 6 | 7 | 在所有需要使用镜像的地方加上前缀 `m.daocloud.io/` 8 | 9 | - [English](https://github.com/wzshiming/crproxy/blob/master/README.md) 10 | - [简体中文](https://github.com/wzshiming/crproxy/blob/master/README_cn.md) 11 | 12 | ## m.daocloud.io 13 | 14 | 如有需要您可以部署自己的镜像代理服务器 15 | 16 | [参考](https://github.com/wzshiming/crproxy/tree/master/examples/default) 17 | 18 | ## On Docker 19 | 20 | 只需要添加前缀 `m.daocloud.io/` 21 | 22 | ``` bash 23 | docker pull m.daocloud.io/docker.io/library/busybox 24 | ``` 25 | 26 | ## On Kubernetes 27 | 28 | 只需要添加前缀 `m.daocloud.io/` 29 | 30 | ``` yaml 31 | image: m.daocloud.io/docker.io/library/busybox 32 | ``` 33 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "hash" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "time" 16 | 17 | "github.com/docker/distribution/registry/api/errcode" 18 | ) 19 | 20 | func (c *CRProxy) AuthToken(rw http.ResponseWriter, r *http.Request) { 21 | if r.Method != http.MethodGet { 22 | errcode.ServeJSON(rw, errcode.ErrorCodeUnsupported) 23 | return 24 | } 25 | if !c.simpleAuth { 26 | errcode.ServeJSON(rw, errcode.ErrorCodeUnsupported) 27 | return 28 | } 29 | query := r.URL.Query() 30 | scope := query.Get("scope") 31 | service := query.Get("service") 32 | 33 | if c.simpleAuthUserpassFunc != nil { 34 | authorization := r.Header.Get("Authorization") 35 | auth := strings.SplitN(authorization, " ", 2) 36 | if len(auth) != 2 { 37 | if c.logger != nil { 38 | c.logger.Println("Login failed", authorization) 39 | } 40 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 41 | return 42 | } 43 | switch auth[0] { 44 | case "Basic": 45 | user, pass, ok := parseBasicAuth(auth[1]) 46 | if user == "" || pass == "" { 47 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 48 | return 49 | } 50 | 51 | var u *url.Userinfo 52 | if ok { 53 | u = url.UserPassword(user, pass) 54 | } else { 55 | u = url.User(user) 56 | } 57 | if !c.simpleAuthUserpassFunc(r, u) { 58 | if c.logger != nil { 59 | c.logger.Println("Login failed user and password", u) 60 | } 61 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 62 | return 63 | } 64 | 65 | if c.logger != nil { 66 | c.logger.Println("Login succeed user", u.Username()) 67 | } 68 | default: 69 | if c.logger != nil { 70 | c.logger.Println("Unsupported authorization", authorization) 71 | } 72 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 73 | return 74 | } 75 | } 76 | 77 | rw.Header().Set("Content-Type", "application/json") 78 | 79 | now := time.Now() 80 | expiresIn := 60 81 | token := defaultTokenManager.Encode(Token{ 82 | Service: service, 83 | Scope: scope, 84 | ExpiresAt: now.Add(time.Duration(expiresIn) * time.Second), 85 | }) 86 | 87 | json.NewEncoder(rw).Encode(tokenInfo{ 88 | Token: token, 89 | ExpiresIn: int64(expiresIn), 90 | IssuedAt: now, 91 | }) 92 | } 93 | 94 | func (c *CRProxy) authenticate(rw http.ResponseWriter, r *http.Request) { 95 | tokenURL := c.tokenURL 96 | if tokenURL == "" { 97 | var scheme = "http" 98 | if c.tokenAuthForceTLS || r.TLS != nil || r.URL.Scheme == "https" { 99 | scheme = "https" 100 | } 101 | tokenURL = scheme + "://" + r.Host + "/auth/token" 102 | } 103 | header := fmt.Sprintf("Bearer realm=%q,service=%q", tokenURL, r.Host) 104 | rw.Header().Set("WWW-Authenticate", header) 105 | c.errorResponse(rw, r, errcode.ErrorCodeUnauthorized) 106 | } 107 | 108 | func (c *CRProxy) authorization(rw http.ResponseWriter, r *http.Request) bool { 109 | if c.privilegedNoAuth && c.isPrivileged(r, nil) { 110 | r.Header.Del("Authorization") 111 | return true 112 | } 113 | 114 | auth := r.Header.Get("Authorization") 115 | if auth == "" { 116 | return false 117 | } 118 | 119 | if !strings.HasPrefix(auth, "Bearer ") { 120 | return false 121 | } 122 | 123 | token, ok := defaultTokenManager.Decode(auth[7:]) 124 | if !ok { 125 | return false 126 | } 127 | 128 | if token.ExpiresAt.Before(time.Now()) { 129 | return false 130 | } 131 | 132 | r.Header.Del("Authorization") 133 | return true 134 | } 135 | 136 | type tokenInfo struct { 137 | Token string `json:"token,omitempty"` 138 | ExpiresIn int64 `json:"expires_in,omitempty"` 139 | IssuedAt time.Time `json:"issued_at,omitempty"` 140 | } 141 | 142 | var defaultTokenManager = &tokenManager{ 143 | NewHash: sha256.New, 144 | RandReader: rand.Reader, 145 | HashSize: sha256.Size, 146 | RandSize: 16, 147 | EncodeToString: base64.RawURLEncoding.EncodeToString, 148 | DecodeString: base64.RawURLEncoding.DecodeString, 149 | } 150 | 151 | type tokenManager struct { 152 | NewHash func() hash.Hash 153 | RandReader io.Reader 154 | HashSize int 155 | RandSize int 156 | EncodeToString func([]byte) string 157 | DecodeString func(string) ([]byte, error) 158 | } 159 | 160 | type Token struct { 161 | ExpiresAt time.Time `json:"expires_at,omitempty"` 162 | Scope string `json:"scope,omitempty"` 163 | Service string `json:"service,omitempty"` 164 | } 165 | 166 | func (p *tokenManager) Encode(t Token) (code string) { 167 | sum := make([]byte, p.RandSize+p.HashSize) 168 | io.ReadFull(p.RandReader, sum[:p.RandSize]) 169 | hashSum := p.NewHash() 170 | data, _ := json.Marshal(t) 171 | hashSum.Write(data) 172 | hashSum.Write(sum[:p.RandSize]) 173 | sum = hashSum.Sum(sum[:p.RandSize]) 174 | return p.EncodeToString(sum) + "." + p.EncodeToString(data) 175 | } 176 | 177 | func (p *tokenManager) Decode(code string) (t Token, b bool) { 178 | cs := strings.Split(code, ".") 179 | if len(cs) != 2 { 180 | return t, false 181 | } 182 | 183 | sum, err := p.DecodeString(cs[0]) 184 | if err != nil { 185 | return t, false 186 | } 187 | if len(sum) != p.HashSize+p.RandSize { 188 | return t, false 189 | } 190 | data, err := p.DecodeString(cs[1]) 191 | if err != nil { 192 | return t, false 193 | } 194 | hashSum := p.NewHash() 195 | hashSum.Write(data) 196 | hashSum.Write(sum[:p.RandSize]) 197 | newSum := hashSum.Sum(nil) 198 | if !bytes.Equal(sum[p.RandSize:], newSum) { 199 | return t, false 200 | } 201 | 202 | err = json.Unmarshal(data, &t) 203 | if err != nil { 204 | return t, false 205 | } 206 | 207 | return t, true 208 | } 209 | 210 | func parseBasicAuth(auth string) (username, password string, ok bool) { 211 | c, err := base64.StdEncoding.DecodeString(auth) 212 | if err != nil { 213 | return "", "", false 214 | } 215 | cs := string(c) 216 | username, password, ok = strings.Cut(cs, ":") 217 | if !ok { 218 | return "", "", false 219 | } 220 | return username, password, true 221 | } 222 | -------------------------------------------------------------------------------- /cmd/crproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "net/http/pprof" 12 | "net/url" 13 | "os" 14 | "slices" 15 | "strings" 16 | "sync/atomic" 17 | "time" 18 | 19 | "github.com/docker/distribution/registry/storage/driver/factory" 20 | "github.com/gorilla/handlers" 21 | "github.com/spf13/pflag" 22 | "github.com/wzshiming/geario" 23 | "github.com/wzshiming/hostmatcher" 24 | 25 | _ "github.com/daocloud/crproxy/storage/driver/obs" 26 | _ "github.com/daocloud/crproxy/storage/driver/oss" 27 | _ "github.com/docker/distribution/registry/storage/driver/azure" 28 | _ "github.com/docker/distribution/registry/storage/driver/gcs" 29 | _ "github.com/docker/distribution/registry/storage/driver/s3-aws" 30 | 31 | "github.com/daocloud/crproxy" 32 | "github.com/daocloud/crproxy/internal/server" 33 | ) 34 | 35 | var ( 36 | behind bool 37 | address string 38 | userpass []string 39 | disableKeepAlives []string 40 | limitDelay bool 41 | blobsSpeedLimit string 42 | ipsSpeedLimit string 43 | totalBlobsSpeedLimit string 44 | allowHostList []string 45 | allowImageListFromFile string 46 | blockImageList []string 47 | blockMessage string 48 | privilegedIPList []string 49 | privilegedImageListFromFile string 50 | privilegedNoAuth bool 51 | retry int 52 | retryInterval time.Duration 53 | storageDriver string 54 | storageParameters map[string]string 55 | linkExpires time.Duration 56 | redirectLinks string 57 | disableTagsList bool 58 | enablePprof bool 59 | defaultRegistry string 60 | overrideDefaultRegistry map[string]string 61 | simpleAuth bool 62 | simpleAuthUserpass map[string]string 63 | tokenURL string 64 | tokenAuthForceTLS bool 65 | 66 | redirectOriginBlobLinks bool 67 | 68 | acmeHosts []string 69 | acmeCacheDir string 70 | certFile string 71 | privateKeyFile string 72 | 73 | enableInternalAPI bool 74 | 75 | readmeURL string 76 | 77 | allowHeadMethod bool 78 | 79 | manifestCacheDuration time.Duration 80 | ) 81 | 82 | func init() { 83 | pflag.BoolVar(&behind, "behind", false, "Behind the reverse proxy") 84 | pflag.StringSliceVarP(&userpass, "user", "u", nil, "host and username and password -u user:pwd@host") 85 | pflag.StringVarP(&address, "address", "a", ":8080", "listen on the address") 86 | pflag.StringSliceVar(&disableKeepAlives, "disable-keep-alives", nil, "disable keep alives for the host") 87 | pflag.BoolVar(&limitDelay, "limit-delay", false, "limit with delay") 88 | pflag.StringVar(&blobsSpeedLimit, "blobs-speed-limit", "", "blobs speed limit per second (default unlimited)") 89 | pflag.StringVar(&ipsSpeedLimit, "ips-speed-limit", "", "ips speed limit per second (default unlimited)") 90 | pflag.StringVar(&totalBlobsSpeedLimit, "total-blobs-speed-limit", "", "total blobs speed limit per second (default unlimited)") 91 | pflag.StringSliceVar(&allowHostList, "allow-host-list", nil, "allow host list") 92 | pflag.StringVar(&allowImageListFromFile, "allow-image-list-from-file", "", "allow image list from file") 93 | pflag.StringSliceVar(&blockImageList, "block-image-list", nil, "block image list (deprecated)") 94 | pflag.StringVar(&blockMessage, "block-message", "", "block message") 95 | pflag.StringSliceVar(&privilegedIPList, "privileged-ip-list", nil, "privileged IP list") 96 | pflag.BoolVar(&privilegedNoAuth, "privileged-no-auth", false, "privileged no auth (deprecated)") 97 | pflag.StringVar(&privilegedImageListFromFile, "privileged-image-list-from-file", "", "privileged image list from file") 98 | pflag.IntVar(&retry, "retry", 0, "retry times") 99 | pflag.DurationVar(&retryInterval, "retry-interval", 0, "retry interval") 100 | pflag.StringVar(&storageDriver, "storage-driver", "", "storage driver") 101 | pflag.StringToStringVar(&storageParameters, "storage-parameters", nil, "storage parameters") 102 | pflag.DurationVar(&linkExpires, "link-expires", 0, "link expires") 103 | pflag.StringVar(&redirectLinks, "redirect-links", "", "redirect links") 104 | pflag.BoolVar(&disableTagsList, "disable-tags-list", false, "disable tags list") 105 | pflag.BoolVar(&enablePprof, "enable-pprof", false, "Enable pprof") 106 | pflag.StringVar(&defaultRegistry, "default-registry", "", "default registry used for non full-path docker pull, like:docker.io") 107 | pflag.StringToStringVar(&overrideDefaultRegistry, "override-default-registry", nil, "override default registry") 108 | pflag.BoolVar(&simpleAuth, "simple-auth", false, "enable simple auth") 109 | pflag.StringToStringVar(&simpleAuthUserpass, "simple-auth-user", nil, "simple auth user and password") 110 | pflag.StringVar(&tokenURL, "token-url", "", "token url (deprecated)") 111 | pflag.BoolVar(&tokenAuthForceTLS, "token-auth-force-tls", false, "token auth force TLS (deprecated)") 112 | 113 | pflag.BoolVar(&redirectOriginBlobLinks, "redirect-origin-blob-links", false, "redirect origin blob links") 114 | 115 | pflag.StringSliceVar(&acmeHosts, "acme-hosts", nil, "acme hosts") 116 | pflag.StringVar(&acmeCacheDir, "acme-cache-dir", "", "acme cache dir") 117 | pflag.StringVar(&certFile, "cert-file", "", "cert file") 118 | pflag.StringVar(&privateKeyFile, "private-key-file", "", "private key file") 119 | pflag.BoolVar(&enableInternalAPI, "enable-internal-api", false, "enable internal api") 120 | 121 | pflag.StringVar(&readmeURL, "readme-url", "", "redirect readme url when not found") 122 | pflag.BoolVar(&allowHeadMethod, "allow-head-method", false, "allow head method") 123 | 124 | pflag.DurationVar(&manifestCacheDuration, "manifest-cache-duration", 0, "manifest cache duration") 125 | pflag.Parse() 126 | } 127 | 128 | func toUserAndPass(userpass []string) (map[string]crproxy.Userpass, error) { 129 | bc := map[string]crproxy.Userpass{} 130 | for _, up := range userpass { 131 | s := strings.SplitN(up, "@", 3) 132 | if len(s) != 2 { 133 | return nil, fmt.Errorf("invalid userpass %q", up) 134 | } 135 | 136 | u := strings.SplitN(s[0], ":", 3) 137 | if len(s) != 2 { 138 | return nil, fmt.Errorf("invalid userpass %q", up) 139 | } 140 | host := s[1] 141 | user := u[0] 142 | pwd := u[1] 143 | bc[host] = crproxy.Userpass{ 144 | Username: user, 145 | Password: pwd, 146 | } 147 | } 148 | return bc, nil 149 | } 150 | 151 | func main() { 152 | ctx := context.Background() 153 | logger := log.New(os.Stderr, "[cr proxy] ", log.LstdFlags) 154 | 155 | mux := http.NewServeMux() 156 | cli := &http.Client{ 157 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 158 | if len(via) > 10 { 159 | return http.ErrUseLastResponse 160 | } 161 | s := make([]string, 0, len(via)+1) 162 | for _, v := range via { 163 | s = append(s, v.URL.String()) 164 | } 165 | 166 | lastRedirect := req.URL.String() 167 | s = append(s, lastRedirect) 168 | logger.Println("redirect", s) 169 | 170 | if v := crproxy.GetCtxValue(req.Context()); v != nil { 171 | v.LastRedirect = lastRedirect 172 | } 173 | return nil 174 | }, 175 | } 176 | 177 | opts := []crproxy.Option{ 178 | crproxy.WithBaseClient(cli), 179 | crproxy.WithLogger(logger), 180 | crproxy.WithMaxClientSizeForEachRegistry(16), 181 | crproxy.WithDomainAlias(map[string]string{ 182 | "docker.io": "registry-1.docker.io", 183 | "ollama.ai": "registry.ollama.ai", 184 | }), 185 | crproxy.WithPathInfoModifyFunc(func(info *crproxy.ImageInfo) *crproxy.ImageInfo { 186 | // docker.io/busybox => docker.io/library/busybox 187 | if info.Host == "docker.io" && !strings.Contains(info.Name, "/") { 188 | info.Name = "library/" + info.Name 189 | } 190 | if info.Host == "ollama.ai" && !strings.Contains(info.Name, "/") { 191 | info.Name = "library/" + info.Name 192 | } 193 | return info 194 | }), 195 | crproxy.WithDisableKeepAlives(disableKeepAlives), 196 | } 197 | 198 | if storageDriver != "" { 199 | parameters := map[string]interface{}{} 200 | for k, v := range storageParameters { 201 | parameters[k] = v 202 | } 203 | sd, err := factory.Create(storageDriver, parameters) 204 | if err != nil { 205 | logger.Println("create storage driver failed:", err) 206 | os.Exit(1) 207 | } 208 | opts = append(opts, crproxy.WithStorageDriver(sd)) 209 | if linkExpires > 0 { 210 | opts = append(opts, crproxy.WithLinkExpires(linkExpires)) 211 | } 212 | if redirectLinks != "" { 213 | u, err := url.Parse(redirectLinks) 214 | if err != nil { 215 | logger.Println("parse redirect links failed:", err) 216 | os.Exit(1) 217 | } 218 | opts = append(opts, crproxy.WithRedirectLinks(u)) 219 | } 220 | } 221 | 222 | if allowImageListFromFile != "" { 223 | f, err := os.ReadFile(allowImageListFromFile) 224 | if err != nil { 225 | logger.Println("can't read allow list file", allowImageListFromFile, ":", err) 226 | os.Exit(1) 227 | } 228 | 229 | var matcher atomic.Pointer[hostmatcher.Matcher] 230 | m, err := getListFrom(bytes.NewReader(f)) 231 | if err != nil { 232 | logger.Println("can't read allow list file", allowImageListFromFile, ":", err) 233 | os.Exit(1) 234 | } 235 | matcher.Store(&m) 236 | 237 | if enableInternalAPI { 238 | mux.HandleFunc("PUT /internal/api/allows", func(rw http.ResponseWriter, r *http.Request) { 239 | body, err := io.ReadAll(r.Body) 240 | if err != nil { 241 | logger.Println("read body failed:", err) 242 | rw.WriteHeader(http.StatusBadRequest) 243 | rw.Write([]byte(err.Error())) 244 | return 245 | } 246 | m, err := getListFrom(bytes.NewReader(body)) 247 | if err != nil { 248 | logger.Println("can't read allow list file", allowImageListFromFile, ":", err) 249 | rw.WriteHeader(http.StatusBadRequest) 250 | rw.Write([]byte(err.Error())) 251 | return 252 | } 253 | 254 | err = os.WriteFile(allowImageListFromFile, body, 0644) 255 | if err != nil { 256 | logger.Println("write file failed:", err) 257 | rw.WriteHeader(http.StatusBadRequest) 258 | rw.Write([]byte(err.Error())) 259 | return 260 | } 261 | 262 | matcher.Store(&m) 263 | }) 264 | } 265 | opts = append(opts, crproxy.WithBlockFunc(func(info *crproxy.BlockInfo) (string, bool) { 266 | if (*matcher.Load()).Match(info.Host + "/" + info.Name) { 267 | return "", false 268 | } 269 | return blockMessage, true 270 | })) 271 | } else if len(blockImageList) != 0 || len(allowHostList) != 0 { 272 | allowHostMap := map[string]struct{}{} 273 | for _, host := range allowHostList { 274 | allowHostMap[host] = struct{}{} 275 | } 276 | blockImageMap := map[string]struct{}{} 277 | for _, image := range blockImageList { 278 | blockImageMap[image] = struct{}{} 279 | } 280 | opts = append(opts, crproxy.WithBlockFunc(func(info *crproxy.BlockInfo) (string, bool) { 281 | if len(allowHostMap) != 0 { 282 | _, ok := allowHostMap[info.Host] 283 | if !ok { 284 | return blockMessage, true 285 | } 286 | } 287 | 288 | if len(blockImageMap) != 0 { 289 | image := info.Host + "/" + info.Name 290 | _, ok := blockImageMap[image] 291 | if ok { 292 | return blockMessage, true 293 | } 294 | } 295 | 296 | return "", false 297 | })) 298 | } 299 | 300 | if len(privilegedIPList) != 0 || privilegedImageListFromFile != "" { 301 | var matcher atomic.Pointer[hostmatcher.Matcher] 302 | if privilegedImageListFromFile != "" { 303 | f, err := os.ReadFile(privilegedImageListFromFile) 304 | if err != nil { 305 | logger.Println("can't read privileged list file", privilegedImageListFromFile, ":", err) 306 | os.Exit(1) 307 | } 308 | 309 | m, err := getListFrom(bytes.NewReader(f)) 310 | if err != nil { 311 | logger.Println("can't read privileged list file", privilegedImageListFromFile, ":", err) 312 | os.Exit(1) 313 | } 314 | matcher.Store(&m) 315 | 316 | if enableInternalAPI { 317 | mux.HandleFunc("PUT /internal/api/privileged", func(rw http.ResponseWriter, r *http.Request) { 318 | body, err := io.ReadAll(r.Body) 319 | if err != nil { 320 | logger.Println("read body failed:", err) 321 | rw.WriteHeader(http.StatusBadRequest) 322 | rw.Write([]byte(err.Error())) 323 | return 324 | } 325 | m, err := getListFrom(bytes.NewReader(body)) 326 | if err != nil { 327 | logger.Println("can't read allow list file", privilegedImageListFromFile, ":", err) 328 | rw.WriteHeader(http.StatusBadRequest) 329 | rw.Write([]byte(err.Error())) 330 | return 331 | } 332 | 333 | err = os.WriteFile(privilegedImageListFromFile, body, 0644) 334 | if err != nil { 335 | logger.Println("write file failed:", err) 336 | rw.WriteHeader(http.StatusBadRequest) 337 | rw.Write([]byte(err.Error())) 338 | return 339 | } 340 | 341 | matcher.Store(&m) 342 | }) 343 | } 344 | } 345 | 346 | set := map[string]struct{}{} 347 | for _, ip := range privilegedIPList { 348 | set[ip] = struct{}{} 349 | } 350 | opts = append(opts, crproxy.WithPrivilegedFunc(func(r *http.Request, info *crproxy.ImageInfo) bool { 351 | if len(set) != 0 { 352 | ip := r.RemoteAddr 353 | if _, ok := set[ip]; ok { 354 | return true 355 | } 356 | } 357 | if m := matcher.Load(); m != nil && info != nil { 358 | return (*m).Match(info.Host + "/" + info.Name) 359 | } 360 | return false 361 | })) 362 | } 363 | 364 | if privilegedNoAuth { 365 | opts = append(opts, crproxy.WithPrivilegedNoAuth(true)) 366 | } 367 | 368 | if len(userpass) != 0 { 369 | bc, err := toUserAndPass(userpass) 370 | if err != nil { 371 | logger.Println("failed to toUserAndPass:", err) 372 | os.Exit(1) 373 | } 374 | opts = append(opts, crproxy.WithUserAndPass(bc)) 375 | } 376 | 377 | if ipsSpeedLimit != "" { 378 | b, d, err := getLimit(ipsSpeedLimit) 379 | if err != nil { 380 | logger.Println("failed to getLimit:", err) 381 | os.Exit(1) 382 | } 383 | opts = append(opts, crproxy.WithIPsSpeedLimit(b, d)) 384 | } 385 | 386 | if blobsSpeedLimit != "" { 387 | b, d, err := getLimit(blobsSpeedLimit) 388 | if err != nil { 389 | logger.Println("failed to getLimit:", err) 390 | os.Exit(1) 391 | } 392 | opts = append(opts, crproxy.WithBlobsSpeedLimit(b, d)) 393 | } 394 | 395 | if totalBlobsSpeedLimit != "" { 396 | b, err := geario.FromBytesSize(totalBlobsSpeedLimit) 397 | if err != nil { 398 | logger.Println("failed to FromBytesSize:", err) 399 | os.Exit(1) 400 | } 401 | opts = append(opts, crproxy.WithTotalBlobsSpeedLimit(b)) 402 | } 403 | 404 | if disableTagsList { 405 | opts = append(opts, crproxy.WithDisableTagsList(true)) 406 | } 407 | 408 | if retry > 0 { 409 | opts = append(opts, crproxy.WithRetry(retry, retryInterval)) 410 | } 411 | if limitDelay { 412 | opts = append(opts, crproxy.WithLimitDelay(true)) 413 | } 414 | 415 | if defaultRegistry != "" { 416 | opts = append(opts, crproxy.WithDefaultRegistry(defaultRegistry)) 417 | } 418 | 419 | if len(overrideDefaultRegistry) != 0 { 420 | opts = append(opts, crproxy.WithOverrideDefaultRegistry(overrideDefaultRegistry)) 421 | } 422 | 423 | if simpleAuth { 424 | opts = append(opts, crproxy.WithSimpleAuth(true, tokenURL, tokenAuthForceTLS)) 425 | } 426 | if len(simpleAuthUserpass) != 0 { 427 | 428 | opts = append(opts, crproxy.WithSimpleAuthUserFunc(func(r *http.Request, userinfo *url.Userinfo) bool { 429 | pass, ok := simpleAuthUserpass[userinfo.Username()] 430 | if !ok { 431 | return false 432 | } 433 | upass, ok := userinfo.Password() 434 | if !ok { 435 | return false 436 | } 437 | if upass != pass { 438 | return false 439 | } 440 | return true 441 | })) 442 | } 443 | 444 | if redirectOriginBlobLinks { 445 | opts = append(opts, crproxy.WithRedirectToOriginBlobFunc(func(r *http.Request, info *crproxy.ImageInfo) bool { 446 | return true 447 | })) 448 | } 449 | 450 | if allowHeadMethod { 451 | opts = append(opts, crproxy.WithAllowHeadMethod(allowHeadMethod)) 452 | } 453 | 454 | if manifestCacheDuration != 0 { 455 | opts = append(opts, crproxy.WithManifestCacheDuration(manifestCacheDuration)) 456 | } 457 | 458 | crp, err := crproxy.NewCRProxy(opts...) 459 | if err != nil { 460 | logger.Println("failed to NewCRProxy:", err) 461 | os.Exit(1) 462 | } 463 | 464 | mux.Handle("/v2/", crp) 465 | mux.HandleFunc("/auth/token", crp.AuthToken) 466 | 467 | mux.HandleFunc("/internal/api/image/sync", crp.Sync) 468 | 469 | if enablePprof { 470 | mux.HandleFunc("/debug/pprof/", pprof.Index) 471 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 472 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 473 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 474 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 475 | } 476 | if readmeURL != "" { 477 | mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { 478 | http.Redirect(rw, r, readmeURL, http.StatusFound) 479 | }) 480 | } 481 | 482 | var handler http.Handler = mux 483 | handler = handlers.LoggingHandler(os.Stderr, handler) 484 | if behind { 485 | handler = handlers.ProxyHeaders(handler) 486 | } 487 | 488 | err = server.Run(ctx, address, handler, acmeHosts, acmeCacheDir, certFile, privateKeyFile) 489 | if err != nil { 490 | logger.Println("failed to ListenAndServe:", err) 491 | os.Exit(1) 492 | } 493 | } 494 | 495 | func getLimit(s string) (geario.B, time.Duration, error) { 496 | i := strings.Index(s, "/") 497 | if i == -1 { 498 | b, err := geario.FromBytesSize(s) 499 | if err != nil { 500 | return 0, 0, err 501 | } 502 | return b, time.Second, nil 503 | } 504 | 505 | b, err := geario.FromBytesSize(s[:i]) 506 | if err != nil { 507 | return 0, 0, err 508 | } 509 | 510 | dur := s[i+1:] 511 | if dur[0] < '0' || dur[0] > '9' { 512 | dur = "1" + dur 513 | } 514 | 515 | d, err := time.ParseDuration(dur) 516 | if err != nil { 517 | return 0, 0, err 518 | } 519 | 520 | return b, d, nil 521 | } 522 | 523 | func getListFrom(r io.Reader) (hostmatcher.Matcher, error) { 524 | lines := bufio.NewReader(r) 525 | hosts := []string{} 526 | for { 527 | line, _, err := lines.ReadLine() 528 | if err == io.EOF { 529 | break 530 | } 531 | h := strings.TrimSpace(string(line)) 532 | if len(h) == 0 { 533 | continue 534 | } 535 | hosts = append(hosts, h) 536 | } 537 | if len(hosts) == 0 { 538 | return nil, fmt.Errorf("no hosts found") 539 | } 540 | if !slices.IsSorted(hosts) { 541 | return nil, fmt.Errorf("hosts not sorted: %v", hosts) 542 | } 543 | return hostmatcher.NewMatcher(hosts), nil 544 | } 545 | -------------------------------------------------------------------------------- /credentials.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/docker/distribution/registry/client/auth/challenge" 9 | ) 10 | 11 | type Userpass struct { 12 | Username string 13 | Password string 14 | } 15 | 16 | type basicCredentials struct { 17 | credentials map[string]Userpass 18 | } 19 | 20 | func newBasicCredentials(cred map[string]Userpass, domainAlias func(string) string, hostScheme func(string) string) (*basicCredentials, error) { 21 | bc := &basicCredentials{ 22 | credentials: map[string]Userpass{}, 23 | } 24 | for domain, c := range cred { 25 | urls, err := getAuthURLs(hostScheme(domain)+"://"+domain, domainAlias) 26 | if err != nil { 27 | return nil, err 28 | } 29 | for _, u := range urls { 30 | bc.credentials[u] = c 31 | } 32 | } 33 | return bc, nil 34 | } 35 | 36 | func (c *basicCredentials) Basic(u *url.URL) (string, string) { 37 | up := c.credentials[u.String()] 38 | 39 | return up.Username, up.Password 40 | } 41 | 42 | func (c *basicCredentials) RefreshToken(u *url.URL, service string) string { 43 | return "" 44 | } 45 | 46 | func (c *basicCredentials) SetRefreshToken(u *url.URL, service, token string) { 47 | } 48 | 49 | func getAuthURLs(remoteURL string, domainAlias func(string) string) ([]string, error) { 50 | authURLs := []string{} 51 | 52 | u, err := url.Parse(remoteURL) 53 | if err != nil { 54 | return nil, err 55 | } 56 | if domainAlias != nil { 57 | u.Host = domainAlias(u.Host) 58 | } 59 | remoteURL = u.String() 60 | 61 | resp, err := http.Get(remoteURL + "/v2/") 62 | if err != nil { 63 | return nil, err 64 | } 65 | defer resp.Body.Close() 66 | 67 | for _, c := range challenge.ResponseChallenges(resp) { 68 | if strings.EqualFold(c.Scheme, "bearer") { 69 | authURLs = append(authURLs, c.Parameters["realm"]) 70 | } 71 | } 72 | 73 | return authURLs, nil 74 | } 75 | -------------------------------------------------------------------------------- /crproxy.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "net/textproto" 11 | "net/url" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/daocloud/crproxy/internal/maps" 17 | "github.com/docker/distribution/registry/api/errcode" 18 | "github.com/docker/distribution/registry/client/auth" 19 | "github.com/docker/distribution/registry/client/auth/challenge" 20 | "github.com/docker/distribution/registry/client/transport" 21 | storagedriver "github.com/docker/distribution/registry/storage/driver" 22 | "github.com/wzshiming/geario" 23 | "github.com/wzshiming/hostmatcher" 24 | "github.com/wzshiming/httpseek" 25 | "github.com/wzshiming/lru" 26 | ) 27 | 28 | var ( 29 | prefix = "/v2/" 30 | catalog = prefix + "_catalog" 31 | ) 32 | 33 | type Logger interface { 34 | Println(v ...interface{}) 35 | } 36 | 37 | type ImageInfo struct { 38 | Host string 39 | Name string 40 | } 41 | 42 | type BlockInfo struct { 43 | IP string 44 | Host string 45 | Name string 46 | } 47 | 48 | type CRProxy struct { 49 | baseClient *http.Client 50 | challengeManager challenge.Manager 51 | clientset maps.SyncMap[string, *lru.LRU[string, *http.Client]] 52 | clientSize int 53 | modify func(info *ImageInfo) *ImageInfo 54 | insecureDomain map[string]struct{} 55 | domainDisableKeepAlives map[string]struct{} 56 | domainAlias map[string]string 57 | userAndPass map[string]Userpass 58 | basicCredentials *basicCredentials 59 | mutClientset sync.Mutex 60 | bytesPool sync.Pool 61 | logger Logger 62 | totalBlobsSpeedLimit *geario.Gear 63 | speedLimitRecord maps.SyncMap[string, *geario.BPS] 64 | blobsSpeedLimit *geario.B 65 | blobsSpeedLimitDuration time.Duration 66 | ipsSpeedLimit *geario.B 67 | ipsSpeedLimitDuration time.Duration 68 | blockFunc []func(*BlockInfo) (string, bool) 69 | retry int 70 | retryInterval time.Duration 71 | storageDriver storagedriver.StorageDriver 72 | linkExpires time.Duration 73 | mutCache sync.Map 74 | redirectLinks *url.URL 75 | limitDelay bool 76 | privilegedNoAuth bool 77 | disableTagsList bool 78 | simpleAuth bool 79 | simpleAuthUserpassFunc func(r *http.Request, userinfo *url.Userinfo) bool 80 | tokenURL string 81 | tokenAuthForceTLS bool 82 | matcher hostmatcher.Matcher 83 | 84 | defaultRegistry string 85 | overrideDefaultRegistry map[string]string 86 | 87 | privilegedFunc func(r *http.Request, info *ImageInfo) bool 88 | redirectToOriginBlobFunc func(r *http.Request, info *ImageInfo) bool 89 | allowHeadMethod bool 90 | 91 | manifestCache maps.SyncMap[string, time.Time] 92 | manifestCacheDuration time.Duration 93 | } 94 | 95 | type Option func(c *CRProxy) 96 | 97 | func WithManifestCacheDuration(d time.Duration) Option { 98 | return func(c *CRProxy) { 99 | c.manifestCacheDuration = d 100 | } 101 | } 102 | 103 | func WithPrivilegedFunc(f func(r *http.Request, info *ImageInfo) bool) Option { 104 | return func(c *CRProxy) { 105 | c.privilegedFunc = f 106 | } 107 | } 108 | 109 | func WithRedirectToOriginBlobFunc(f func(r *http.Request, info *ImageInfo) bool) Option { 110 | return func(c *CRProxy) { 111 | c.redirectToOriginBlobFunc = f 112 | } 113 | } 114 | 115 | func WithSimpleAuth(b bool, tokenURL string, forceTLS bool) Option { 116 | return func(c *CRProxy) { 117 | c.simpleAuth = b 118 | c.tokenURL = tokenURL 119 | c.tokenAuthForceTLS = forceTLS 120 | } 121 | } 122 | 123 | func WithSimpleAuthUserFunc(f func(r *http.Request, userinfo *url.Userinfo) bool) Option { 124 | return func(c *CRProxy) { 125 | c.simpleAuthUserpassFunc = f 126 | } 127 | } 128 | 129 | func WithDefaultRegistry(target string) Option { 130 | return func(c *CRProxy) { 131 | c.defaultRegistry = target 132 | } 133 | } 134 | 135 | func WithOverrideDefaultRegistry(overrideDefaultRegistry map[string]string) Option { 136 | return func(c *CRProxy) { 137 | c.overrideDefaultRegistry = overrideDefaultRegistry 138 | } 139 | } 140 | 141 | func WithDisableTagsList(b bool) Option { 142 | return func(c *CRProxy) { 143 | c.disableTagsList = b 144 | } 145 | } 146 | 147 | func WithPrivilegedNoAuth(b bool) Option { 148 | return func(c *CRProxy) { 149 | c.privilegedNoAuth = true 150 | } 151 | } 152 | 153 | func WithLimitDelay(b bool) Option { 154 | return func(c *CRProxy) { 155 | c.limitDelay = b 156 | } 157 | } 158 | 159 | func WithLinkExpires(d time.Duration) Option { 160 | return func(c *CRProxy) { 161 | c.linkExpires = d 162 | } 163 | } 164 | 165 | func WithRedirectLinks(l *url.URL) Option { 166 | return func(c *CRProxy) { 167 | c.redirectLinks = l 168 | } 169 | } 170 | 171 | func WithStorageDriver(storageDriver storagedriver.StorageDriver) Option { 172 | return func(c *CRProxy) { 173 | c.storageDriver = storageDriver 174 | } 175 | } 176 | 177 | func WithBlobsSpeedLimit(limit geario.B, duration time.Duration) Option { 178 | return func(c *CRProxy) { 179 | c.blobsSpeedLimit = &limit 180 | c.blobsSpeedLimitDuration = duration 181 | } 182 | } 183 | 184 | func WithIPsSpeedLimit(limit geario.B, duration time.Duration) Option { 185 | return func(c *CRProxy) { 186 | c.ipsSpeedLimit = &limit 187 | c.ipsSpeedLimitDuration = duration 188 | } 189 | } 190 | 191 | func WithTotalBlobsSpeedLimit(limit geario.B) Option { 192 | return func(c *CRProxy) { 193 | c.totalBlobsSpeedLimit = geario.NewGear(time.Second, limit) 194 | } 195 | } 196 | 197 | func WithBaseClient(baseClient *http.Client) Option { 198 | return func(c *CRProxy) { 199 | c.baseClient = baseClient 200 | } 201 | } 202 | 203 | func WithLogger(logger Logger) Option { 204 | return func(c *CRProxy) { 205 | c.logger = logger 206 | } 207 | } 208 | 209 | func WithUserAndPass(userAndPass map[string]Userpass) Option { 210 | return func(c *CRProxy) { 211 | c.userAndPass = userAndPass 212 | } 213 | } 214 | 215 | func WithDomainAlias(domainAlias map[string]string) Option { 216 | return func(c *CRProxy) { 217 | c.domainAlias = domainAlias 218 | } 219 | } 220 | 221 | func WithPathInfoModifyFunc(modify func(info *ImageInfo) *ImageInfo) Option { 222 | return func(c *CRProxy) { 223 | c.modify = modify 224 | } 225 | } 226 | 227 | func WithMaxClientSizeForEachRegistry(clientSize int) Option { 228 | return func(c *CRProxy) { 229 | c.clientSize = clientSize 230 | } 231 | } 232 | 233 | func WithDisableKeepAlives(disableKeepAlives []string) Option { 234 | return func(c *CRProxy) { 235 | c.domainDisableKeepAlives = map[string]struct{}{} 236 | for _, v := range disableKeepAlives { 237 | c.domainDisableKeepAlives[v] = struct{}{} 238 | } 239 | } 240 | } 241 | 242 | func WithBlockFunc(blockFunc func(info *BlockInfo) (string, bool)) Option { 243 | return func(c *CRProxy) { 244 | c.blockFunc = append(c.blockFunc, blockFunc) 245 | } 246 | } 247 | 248 | func WithRetry(retry int, retryInterval time.Duration) Option { 249 | return func(c *CRProxy) { 250 | c.retry = retry 251 | c.retryInterval = retryInterval 252 | } 253 | } 254 | 255 | func WithAllowHeadMethod(allowHeadMethod bool) Option { 256 | return func(c *CRProxy) { 257 | c.allowHeadMethod = allowHeadMethod 258 | } 259 | } 260 | 261 | func NewCRProxy(opts ...Option) (*CRProxy, error) { 262 | c := &CRProxy{ 263 | challengeManager: challenge.NewSimpleManager(), 264 | clientSize: 10240, 265 | baseClient: http.DefaultClient, 266 | bytesPool: sync.Pool{ 267 | New: func() interface{} { 268 | return make([]byte, 32*1024) 269 | }, 270 | }, 271 | } 272 | for _, opt := range opts { 273 | opt(c) 274 | } 275 | if len(c.userAndPass) != 0 { 276 | bc, err := newBasicCredentials(c.userAndPass, c.getDomainAlias, c.getScheme) 277 | if err != nil { 278 | return nil, err 279 | } 280 | c.basicCredentials = bc 281 | } 282 | return c, nil 283 | } 284 | 285 | func (c *CRProxy) hostURL(host string) string { 286 | return c.getScheme(host) + "://" + host 287 | } 288 | 289 | func (c *CRProxy) pingURL(host string) string { 290 | return c.hostURL(host) + prefix 291 | } 292 | 293 | func (c *CRProxy) getScheme(host string) string { 294 | if c.insecureDomain != nil { 295 | _, ok := c.insecureDomain[host] 296 | if ok { 297 | return "http" 298 | } 299 | } 300 | return "https" 301 | } 302 | 303 | func (c *CRProxy) getClientset(host string, image string) *http.Client { 304 | sets, hasSets := c.clientset.Load(host) 305 | if hasSets { 306 | client, ok := sets.Get(image) 307 | if ok { 308 | return client 309 | } 310 | } 311 | 312 | c.mutClientset.Lock() 313 | defer c.mutClientset.Unlock() 314 | if sets == nil { 315 | sets = lru.NewLRU(c.clientSize, func(image string, client *http.Client) { 316 | if c.logger != nil { 317 | c.logger.Println("evicted client", host, image) 318 | } 319 | client.CloseIdleConnections() 320 | }) 321 | c.clientset.Store(host, sets) 322 | } 323 | 324 | if c.logger != nil { 325 | c.logger.Println("cache client", host, image) 326 | } 327 | var credentialStore auth.CredentialStore 328 | if c.basicCredentials != nil { 329 | credentialStore = c.basicCredentials 330 | } 331 | authHandler := auth.NewTokenHandler(nil, credentialStore, image, "pull") 332 | 333 | tr := c.baseClient.Transport 334 | 335 | if c.domainDisableKeepAlives != nil { 336 | if _, ok := c.domainDisableKeepAlives[host]; ok { 337 | tr = c.disableKeepAlives(tr) 338 | } 339 | } 340 | 341 | if c.retryInterval > 0 { 342 | if tr == nil { 343 | tr = http.DefaultTransport 344 | } 345 | tr = httpseek.NewMustReaderTransport(tr, func(request *http.Request, retry int, err error) error { 346 | if errors.Is(err, context.Canceled) || 347 | errors.Is(err, context.DeadlineExceeded) { 348 | return err 349 | } 350 | if c.retry > 0 && retry >= c.retry { 351 | return err 352 | } 353 | if c.logger != nil { 354 | c.logger.Println("Retry", request.URL, retry, err) 355 | } 356 | time.Sleep(c.retryInterval) 357 | return nil 358 | }) 359 | } 360 | 361 | tr = transport.NewTransport(tr, auth.NewAuthorizer(c.challengeManager, authHandler)) 362 | 363 | client := &http.Client{ 364 | Transport: tr, 365 | CheckRedirect: c.baseClient.CheckRedirect, 366 | Timeout: c.baseClient.Timeout, 367 | Jar: c.baseClient.Jar, 368 | } 369 | 370 | sets.Put(image, client) 371 | return client 372 | } 373 | 374 | func (c *CRProxy) disableKeepAlives(rt http.RoundTripper) http.RoundTripper { 375 | if rt == nil { 376 | tr := http.DefaultTransport.(*http.Transport).Clone() 377 | tr.DisableKeepAlives = true 378 | return tr 379 | } 380 | if tr, ok := rt.(*http.Transport); ok { 381 | if !tr.DisableKeepAlives { 382 | tr = tr.Clone() 383 | tr.DisableKeepAlives = true 384 | } 385 | return tr 386 | } 387 | if c.logger != nil { 388 | c.logger.Println("failed to disable keep alives") 389 | } 390 | return rt 391 | } 392 | 393 | func (c *CRProxy) ping(host string) error { 394 | if c.logger != nil { 395 | c.logger.Println("ping", host) 396 | } 397 | 398 | ep := c.pingURL(host) 399 | e, err := url.Parse(ep) 400 | if err != nil { 401 | return err 402 | } 403 | challenges, err := c.challengeManager.GetChallenges(*e) 404 | if err == nil && len(challenges) != 0 { 405 | return nil 406 | } 407 | 408 | resp, err := c.baseClient.Get(ep) 409 | if err != nil { 410 | return err 411 | } 412 | defer resp.Body.Close() 413 | err = c.challengeManager.AddResponse(resp) 414 | if err != nil { 415 | return err 416 | } 417 | return nil 418 | } 419 | 420 | func apiBase(w http.ResponseWriter, r *http.Request) { 421 | const emptyJSON = "{}" 422 | // Provide a simple /v2/ 200 OK response with empty json response. 423 | w.Header().Set("Content-Type", "application/json") 424 | w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) 425 | 426 | fmt.Fprint(w, emptyJSON) 427 | } 428 | 429 | func emptyTagsList(w http.ResponseWriter, r *http.Request) { 430 | const emptyTagsList = `{"name":"disable-list-tags","tags":[]}` 431 | 432 | w.Header().Set("Content-Type", "application/json") 433 | w.Header().Set("Content-Length", fmt.Sprint(len(emptyTagsList))) 434 | fmt.Fprint(w, emptyTagsList) 435 | } 436 | 437 | func (c *CRProxy) do(cli *http.Client, r *http.Request) (resp *http.Response, err error) { 438 | forHead := !c.allowHeadMethod && r.Method == http.MethodHead 439 | if forHead { 440 | r.Method = http.MethodGet 441 | } 442 | resp, err = cli.Do(r) 443 | if err != nil { 444 | return nil, err 445 | } 446 | 447 | if forHead { 448 | r.Method = http.MethodHead 449 | if resp.Body != nil { 450 | resp.Body.Close() 451 | } 452 | resp.Body = http.NoBody 453 | } 454 | return resp, err 455 | } 456 | 457 | func (c *CRProxy) doWithAuth(cli *http.Client, r *http.Request, host string) (*http.Response, error) { 458 | resp, err := c.do(cli, r) 459 | if err != nil { 460 | return nil, err 461 | } 462 | 463 | if resp.StatusCode == http.StatusUnauthorized { 464 | err = c.ping(host) 465 | if err != nil { 466 | if c.logger != nil { 467 | c.logger.Println("failed to ping", host, err) 468 | } 469 | return resp, nil 470 | } 471 | 472 | resp0, err0 := c.do(cli, r) 473 | if err0 != nil { 474 | if c.logger != nil { 475 | c.logger.Println("failed to redo", host, err) 476 | } 477 | return resp, nil 478 | } 479 | resp.Body.Close() 480 | resp = resp0 481 | } 482 | return resp, nil 483 | } 484 | 485 | func getIP(str string) string { 486 | host, _, err := net.SplitHostPort(str) 487 | if err == nil && host != "" { 488 | return host 489 | } 490 | return str 491 | } 492 | 493 | func (c *CRProxy) block(info *BlockInfo) (string, bool) { 494 | for _, blockFunc := range c.blockFunc { 495 | blockMessage, block := blockFunc(info) 496 | if block { 497 | return blockMessage, true 498 | } 499 | } 500 | return "", false 501 | } 502 | 503 | func (c *CRProxy) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 504 | if r.Method != http.MethodGet && r.Method != http.MethodHead { 505 | errcode.ServeJSON(rw, errcode.ErrorCodeUnsupported) 506 | return 507 | } 508 | 509 | r.RemoteAddr = getIP(r.RemoteAddr) 510 | 511 | if c.simpleAuth && !c.authorization(rw, r) { 512 | c.authenticate(rw, r) 513 | return 514 | } 515 | 516 | oriPath := r.URL.Path 517 | if oriPath == prefix { 518 | apiBase(rw, r) 519 | return 520 | } 521 | if !strings.HasPrefix(oriPath, prefix) { 522 | c.notFoundResponse(rw, r) 523 | return 524 | } 525 | if oriPath == catalog { 526 | errcode.ServeJSON(rw, errcode.ErrorCodeUnsupported) 527 | return 528 | } 529 | 530 | defaultRegistry := c.defaultRegistry 531 | if c.overrideDefaultRegistry != nil { 532 | r, ok := c.overrideDefaultRegistry[r.Host] 533 | if ok { 534 | defaultRegistry = r 535 | } 536 | } 537 | info, ok := ParseOriginPathInfo(oriPath, defaultRegistry) 538 | if !ok { 539 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 540 | return 541 | } 542 | 543 | if c.modify != nil { 544 | n := c.modify(&ImageInfo{ 545 | Host: info.Host, 546 | Name: info.Image, 547 | }) 548 | info.Host = n.Host 549 | info.Image = n.Name 550 | } 551 | 552 | imageInfo := &ImageInfo{ 553 | Host: info.Host, 554 | Name: info.Image, 555 | } 556 | 557 | if c.blockFunc != nil && !c.isPrivileged(r, nil) { 558 | 559 | blockMessage, block := c.block(&BlockInfo{ 560 | IP: r.RemoteAddr, 561 | Host: info.Host, 562 | Name: info.Image, 563 | }) 564 | if block { 565 | if blockMessage != "" { 566 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied.WithMessage(blockMessage)) 567 | } else { 568 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 569 | } 570 | return 571 | } 572 | } 573 | 574 | info.Host = c.getDomainAlias(info.Host) 575 | 576 | if info.TagsList && !c.isPrivileged(r, nil) && c.disableTagsList { 577 | emptyTagsList(rw, r) 578 | return 579 | } 580 | 581 | path, err := info.Path() 582 | if err != nil { 583 | if c.logger != nil { 584 | c.logger.Println("failed to get path", err) 585 | } 586 | errcode.ServeJSON(rw, errcode.ErrorCodeUnknown) 587 | return 588 | } 589 | r.RequestURI = "" 590 | r.Host = info.Host 591 | r.URL.Host = info.Host 592 | r.URL.Scheme = c.getScheme(info.Host) 593 | r.URL.Path = path 594 | r.URL.RawQuery = "" 595 | r.URL.ForceQuery = false 596 | r.Body = http.NoBody 597 | if info.Blobs != "" && c.isRedirectToOriginBlob(r, imageInfo) { 598 | c.redirectBlobResponse(rw, r, info) 599 | return 600 | } 601 | 602 | if !c.isPrivileged(r, imageInfo) { 603 | if !c.checkLimit(rw, r, info) { 604 | return 605 | } 606 | } 607 | 608 | if c.storageDriver != nil { 609 | if info.Blobs != "" { 610 | c.cacheBlobResponse(rw, r, info) 611 | return 612 | } else if info.Manifests != "" { 613 | c.cacheManifestResponse(rw, r, info) 614 | return 615 | } 616 | } 617 | c.directResponse(rw, r, info) 618 | } 619 | 620 | func (c *CRProxy) directResponse(rw http.ResponseWriter, r *http.Request, info *PathInfo) { 621 | cli := c.getClientset(info.Host, info.Image) 622 | resp, err := c.doWithAuth(cli, r, info.Host) 623 | if err != nil { 624 | if c.logger != nil { 625 | c.logger.Println("failed to request", info.Host, info.Image, err) 626 | } 627 | errcode.ServeJSON(rw, errcode.ErrorCodeUnknown) 628 | return 629 | } 630 | defer func() { 631 | resp.Body.Close() 632 | }() 633 | 634 | switch resp.StatusCode { 635 | case http.StatusUnauthorized, http.StatusForbidden: 636 | if c.logger != nil { 637 | c.logger.Println("origin direct response 40x, but hit caches", info.Host, info.Image, err, dumpResponse(resp)) 638 | } 639 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 640 | return 641 | } 642 | 643 | resp.Header.Del("Docker-Ratelimit-Source") 644 | 645 | if resp.StatusCode == http.StatusOK { 646 | oldLink := resp.Header.Get("Link") 647 | if oldLink != "" { 648 | resp.Header.Set("Link", addPrefixToImageForPagination(oldLink, info.Host)) 649 | } 650 | } 651 | 652 | header := rw.Header() 653 | for k, v := range resp.Header { 654 | key := textproto.CanonicalMIMEHeaderKey(k) 655 | header[key] = v 656 | } 657 | rw.WriteHeader(resp.StatusCode) 658 | 659 | if r.Method != http.MethodHead { 660 | buf := c.bytesPool.Get().([]byte) 661 | defer c.bytesPool.Put(buf) 662 | var body io.Reader = resp.Body 663 | 664 | if !c.isPrivileged(r, &ImageInfo{ 665 | Host: info.Host, 666 | Name: info.Image, 667 | }) { 668 | c.accumulativeLimit(r, info, resp.ContentLength) 669 | 670 | if c.totalBlobsSpeedLimit != nil && info.Blobs != "" { 671 | body = c.totalBlobsSpeedLimit.Reader(body) 672 | } 673 | 674 | if c.blobsSpeedLimit != nil && info.Blobs != "" { 675 | body = geario.NewGear(c.blobsSpeedLimitDuration, *c.blobsSpeedLimit).Reader(body) 676 | } 677 | } 678 | 679 | io.CopyBuffer(rw, body, buf) 680 | } 681 | } 682 | 683 | func (c *CRProxy) errorResponse(rw http.ResponseWriter, r *http.Request, err error) { 684 | if err != nil { 685 | e := err.Error() 686 | if c.logger != nil { 687 | c.logger.Println("error response", r.RemoteAddr, e) 688 | } 689 | } 690 | 691 | if err == nil { 692 | err = errcode.ErrorCodeUnknown 693 | } 694 | 695 | errcode.ServeJSON(rw, err) 696 | } 697 | 698 | func (c *CRProxy) notFoundResponse(rw http.ResponseWriter, r *http.Request) { 699 | http.NotFound(rw, r) 700 | } 701 | 702 | func (c *CRProxy) redirect(rw http.ResponseWriter, r *http.Request, blobPath string) error { 703 | options := map[string]interface{}{ 704 | "method": r.Method, 705 | } 706 | linkExpires := c.linkExpires 707 | if linkExpires > 0 { 708 | options["expiry"] = time.Now().Add(linkExpires) 709 | } 710 | u, err := c.storageDriver.URLFor(r.Context(), blobPath, options) 711 | if err != nil { 712 | return err 713 | } 714 | if c.logger != nil { 715 | c.logger.Println("Cache hit", blobPath, u) 716 | } 717 | if c.redirectLinks != nil { 718 | uri, err := url.Parse(u) 719 | if err == nil { 720 | uri.Scheme = c.redirectLinks.Scheme 721 | uri.Host = c.redirectLinks.Host 722 | u = uri.String() 723 | } 724 | } 725 | http.Redirect(rw, r, u, http.StatusTemporaryRedirect) 726 | return nil 727 | } 728 | 729 | func (c *CRProxy) getDomainAlias(host string) string { 730 | if c.domainAlias == nil { 731 | return host 732 | } 733 | h, ok := c.domainAlias[host] 734 | if !ok { 735 | return host 736 | } 737 | return h 738 | } 739 | -------------------------------------------------------------------------------- /crproxy_blob.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "path" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/docker/distribution/registry/api/errcode" 15 | ) 16 | 17 | func blobCachePath(blob string) string { 18 | blob = strings.TrimPrefix(blob, "sha256:") 19 | return path.Join("/docker/registry/v2/blobs/sha256", blob[:2], blob, "data") 20 | } 21 | 22 | func (c *CRProxy) cacheBlobResponse(rw http.ResponseWriter, r *http.Request, info *PathInfo) { 23 | ctx := r.Context() 24 | 25 | blobPath := blobCachePath(info.Blobs) 26 | 27 | closeValue, loaded := c.mutCache.LoadOrStore(blobPath, make(chan struct{})) 28 | closeCh := closeValue.(chan struct{}) 29 | for loaded { 30 | select { 31 | case <-ctx.Done(): 32 | err := ctx.Err().Error() 33 | if c.logger != nil { 34 | c.logger.Println(err) 35 | } 36 | http.Error(rw, err, http.StatusInternalServerError) 37 | return 38 | case <-closeCh: 39 | } 40 | closeValue, loaded = c.mutCache.LoadOrStore(blobPath, make(chan struct{})) 41 | closeCh = closeValue.(chan struct{}) 42 | } 43 | 44 | doneCache := func() { 45 | c.mutCache.Delete(blobPath) 46 | close(closeCh) 47 | } 48 | 49 | stat, err := c.storageDriver.Stat(ctx, blobPath) 50 | if err == nil { 51 | doneCache() 52 | 53 | size := stat.Size() 54 | if r.Method == http.MethodHead { 55 | rw.Header().Set("Content-Length", strconv.FormatInt(size, 10)) 56 | rw.Header().Set("Content-Type", "application/octet-stream") 57 | return 58 | } 59 | 60 | if !c.isPrivileged(r, &ImageInfo{ 61 | Host: info.Host, 62 | Name: info.Image, 63 | }) { 64 | c.accumulativeLimit(r, info, size) 65 | if !c.waitForLimit(r, info, size) { 66 | c.errorResponse(rw, r, nil) 67 | return 68 | } 69 | } 70 | 71 | err = c.redirect(rw, r, blobPath) 72 | if err == nil { 73 | return 74 | } 75 | c.errorResponse(rw, r, ctx.Err()) 76 | return 77 | } 78 | if c.logger != nil { 79 | c.logger.Println("Cache miss", blobPath) 80 | } 81 | 82 | type repo struct { 83 | err error 84 | size int64 85 | } 86 | signalCh := make(chan repo, 1) 87 | 88 | go func() { 89 | defer doneCache() 90 | size, err := c.cacheBlobContent(context.Background(), r, blobPath, info) 91 | signalCh <- repo{ 92 | err: err, 93 | size: size, 94 | } 95 | }() 96 | 97 | select { 98 | case <-ctx.Done(): 99 | c.errorResponse(rw, r, ctx.Err()) 100 | return 101 | case signal := <-signalCh: 102 | if signal.err != nil { 103 | c.errorResponse(rw, r, signal.err) 104 | return 105 | } 106 | if r.Method == http.MethodHead { 107 | rw.Header().Set("Content-Length", strconv.FormatInt(signal.size, 10)) 108 | rw.Header().Set("Content-Type", "application/octet-stream") 109 | return 110 | } 111 | 112 | if !c.isPrivileged(r, &ImageInfo{ 113 | Host: info.Host, 114 | Name: info.Image, 115 | }) { 116 | c.accumulativeLimit(r, info, signal.size) 117 | if !c.waitForLimit(r, info, signal.size) { 118 | c.errorResponse(rw, r, nil) 119 | return 120 | } 121 | } 122 | 123 | err = c.redirect(rw, r, blobPath) 124 | if err != nil { 125 | if c.logger != nil { 126 | c.logger.Println("failed to redirect", blobPath, err) 127 | } 128 | } 129 | return 130 | } 131 | } 132 | 133 | func (c *CRProxy) cacheBlobContent(ctx context.Context, r *http.Request, blobPath string, info *PathInfo) (int64, error) { 134 | cli := c.getClientset(info.Host, info.Image) 135 | resp, err := c.doWithAuth(cli, r.WithContext(ctx), info.Host) 136 | if err != nil { 137 | return 0, err 138 | } 139 | defer func() { 140 | resp.Body.Close() 141 | }() 142 | 143 | switch resp.StatusCode { 144 | case http.StatusUnauthorized, http.StatusForbidden: 145 | return 0, errcode.ErrorCodeDenied 146 | } 147 | 148 | if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 149 | return 0, errcode.ErrorCodeUnknown.WithMessage(fmt.Sprintf("Source response code %d", resp.StatusCode)) 150 | } 151 | 152 | buf := c.bytesPool.Get().([]byte) 153 | defer c.bytesPool.Put(buf) 154 | 155 | fw, err := c.storageDriver.Writer(ctx, blobPath, false) 156 | if err != nil { 157 | return 0, err 158 | } 159 | 160 | h := sha256.New() 161 | n, err := io.CopyBuffer(fw, io.TeeReader(resp.Body, h), buf) 162 | if err != nil { 163 | fw.Cancel() 164 | return 0, err 165 | } 166 | 167 | if n != resp.ContentLength { 168 | fw.Cancel() 169 | return 0, fmt.Errorf("expected %d bytes, got %d", resp.ContentLength, n) 170 | } 171 | 172 | hash := hex.EncodeToString(h.Sum(nil)[:]) 173 | if info.Blobs[7:] != hash { 174 | fw.Cancel() 175 | return 0, fmt.Errorf("expected %s hash, got %s", info.Blobs[7:], hash) 176 | } 177 | 178 | err = fw.Commit() 179 | if err != nil { 180 | return 0, err 181 | } 182 | return n, nil 183 | } 184 | 185 | func (c *CRProxy) redirectBlobResponse(rw http.ResponseWriter, r *http.Request, info *PathInfo) { 186 | r = r.WithContext(withCtxValue(r.Context())) 187 | 188 | cli := c.getClientset(info.Host, info.Image) 189 | resp, err := c.doWithAuth(cli, r, info.Host) 190 | if err != nil { 191 | if c.logger != nil { 192 | c.logger.Println("failed to request", info.Host, info.Image, err) 193 | } 194 | errcode.ServeJSON(rw, errcode.ErrorCodeUnknown) 195 | return 196 | } 197 | defer func() { 198 | resp.Body.Close() 199 | }() 200 | 201 | switch resp.StatusCode { 202 | default: 203 | if c.logger != nil { 204 | c.logger.Println("failed to redirect blob", info.Host, info.Image, resp.StatusCode) 205 | } 206 | errcode.ServeJSON(rw, errcode.ErrorCodeUnavailable) 207 | return 208 | case http.StatusUnauthorized, http.StatusForbidden: 209 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 210 | return 211 | case http.StatusTemporaryRedirect, http.StatusPermanentRedirect, http.StatusMovedPermanently, http.StatusFound: 212 | location := resp.Header.Get("Location") 213 | http.Redirect(rw, r, location, http.StatusFound) 214 | return 215 | case http.StatusOK: 216 | v := GetCtxValue(r.Context()) 217 | if v != nil && v.LastRedirect != "" { 218 | http.Redirect(rw, r, v.LastRedirect, http.StatusFound) 219 | return 220 | } 221 | errcode.ServeJSON(rw, errcode.ErrorCodeUnavailable) 222 | return 223 | } 224 | } 225 | 226 | func (c *CRProxy) isRedirectToOriginBlob(r *http.Request, info *ImageInfo) bool { 227 | if c.redirectToOriginBlobFunc == nil { 228 | return false 229 | } 230 | 231 | return c.redirectToOriginBlobFunc(r, info) 232 | } 233 | -------------------------------------------------------------------------------- /crproxy_control.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/docker/distribution/registry/api/errcode" 9 | "github.com/wzshiming/geario" 10 | ) 11 | 12 | func (c *CRProxy) isPrivileged(r *http.Request, info *ImageInfo) bool { 13 | if c.privilegedFunc == nil { 14 | return false 15 | } 16 | return c.privilegedFunc(r, info) 17 | } 18 | 19 | func (c *CRProxy) checkLimit(rw http.ResponseWriter, r *http.Request, info *PathInfo) bool { 20 | if c.ipsSpeedLimit != nil && info.Blobs != "" { 21 | bps, _ := c.speedLimitRecord.LoadOrStore(r.RemoteAddr, geario.NewBPSAver(c.ipsSpeedLimitDuration)) 22 | aver := bps.Aver() 23 | if aver > *c.ipsSpeedLimit { 24 | if c.logger != nil { 25 | c.logger.Println("exceed limit", r.RemoteAddr, aver, *c.ipsSpeedLimit) 26 | } 27 | if c.limitDelay { 28 | for bps.Aver() > *c.ipsSpeedLimit { 29 | wait := time.Second 30 | n := bps.Next() 31 | if !n.IsZero() { 32 | wait = bps.Next().Sub(time.Now()) 33 | if wait < time.Second { 34 | wait = time.Second 35 | } 36 | } 37 | select { 38 | case <-r.Context().Done(): 39 | return false 40 | case <-time.After(wait): 41 | } 42 | } 43 | } else { 44 | err := errcode.ErrorCodeTooManyRequests 45 | rw.Header().Set("X-Retry-After", strconv.FormatInt(bps.Next().Unix(), 10)) 46 | errcode.ServeJSON(rw, err) 47 | return false 48 | } 49 | } 50 | } 51 | 52 | return true 53 | } 54 | 55 | func (c *CRProxy) waitForLimit(r *http.Request, info *PathInfo, size int64) bool { 56 | if c.blobsSpeedLimit != nil && info.Blobs != "" { 57 | dur := GetSleepDuration(geario.B(size), *c.blobsSpeedLimit, c.blobsSpeedLimitDuration) 58 | if dur > 0 { 59 | if c.logger != nil { 60 | c.logger.Println("delay request", r.RemoteAddr, geario.B(size), dur) 61 | } 62 | select { 63 | case <-r.Context().Done(): 64 | return false 65 | case <-time.After(dur): 66 | } 67 | } 68 | } 69 | 70 | return true 71 | } 72 | 73 | func (c *CRProxy) accumulativeLimit(r *http.Request, info *PathInfo, size int64) { 74 | if c.ipsSpeedLimit != nil && info.Blobs != "" { 75 | bps, ok := c.speedLimitRecord.Load(r.RemoteAddr) 76 | if ok { 77 | bps.Add(geario.B(size)) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crproxy_manifest.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/textproto" 12 | "path" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/docker/distribution/registry/api/errcode" 18 | ) 19 | 20 | func manifestRevisionsCachePath(host, image, tagOrBlob string) string { 21 | return path.Join("/docker/registry/v2/repositories", host, image, "_manifests/revisions/sha256", tagOrBlob, "link") 22 | } 23 | 24 | func manifestTagCachePath(host, image, tagOrBlob string) string { 25 | return path.Join("/docker/registry/v2/repositories", host, image, "_manifests/tags", tagOrBlob, "current/link") 26 | } 27 | 28 | func (c *CRProxy) cacheManifestResponse(rw http.ResponseWriter, r *http.Request, info *PathInfo) { 29 | if c.cachedManifest(rw, r, info, true) { 30 | return 31 | } 32 | 33 | cli := c.getClientset(info.Host, info.Image) 34 | resp, err := c.doWithAuth(cli, r, info.Host) 35 | if err != nil { 36 | if c.cachedManifest(rw, r, info, false) { 37 | return 38 | } 39 | if c.logger != nil { 40 | c.logger.Println("failed to request", info.Host, info.Image, err) 41 | } 42 | errcode.ServeJSON(rw, errcode.ErrorCodeUnknown) 43 | return 44 | } 45 | defer func() { 46 | resp.Body.Close() 47 | }() 48 | 49 | switch resp.StatusCode { 50 | case http.StatusUnauthorized, http.StatusForbidden: 51 | if c.cachedManifest(rw, r, info, false) { 52 | if c.logger != nil { 53 | c.logger.Println("origin manifest response 40x, but hit caches", info.Host, info.Image, err, dumpResponse(resp)) 54 | } 55 | return 56 | } 57 | if c.logger != nil { 58 | c.logger.Println("origin manifest response 40x", info.Host, info.Image, err, dumpResponse(resp)) 59 | } 60 | errcode.ServeJSON(rw, errcode.ErrorCodeDenied) 61 | return 62 | } 63 | 64 | if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { 65 | if c.cachedManifest(rw, r, info, false) { 66 | if c.logger != nil { 67 | c.logger.Println("origin manifest response 5xx, but hit caches", info.Host, info.Image, err, dumpResponse(resp)) 68 | } 69 | return 70 | } 71 | if c.logger != nil { 72 | c.logger.Println("origin manifest response 5xx", info.Host, info.Image, err, dumpResponse(resp)) 73 | } 74 | } 75 | 76 | resp.Header.Del("Docker-Ratelimit-Source") 77 | 78 | header := rw.Header() 79 | for k, v := range resp.Header { 80 | key := textproto.CanonicalMIMEHeaderKey(k) 81 | header[key] = v 82 | } 83 | 84 | rw.WriteHeader(resp.StatusCode) 85 | 86 | if r.Method == http.MethodHead { 87 | return 88 | } 89 | 90 | if resp.StatusCode >= http.StatusOK || resp.StatusCode < http.StatusMultipleChoices { 91 | body, err := io.ReadAll(resp.Body) 92 | if err != nil { 93 | c.errorResponse(rw, r, err) 94 | return 95 | } 96 | 97 | err = c.cacheManifestContent(context.Background(), info, body) 98 | if err != nil { 99 | c.errorResponse(rw, r, err) 100 | return 101 | } 102 | rw.Write(body) 103 | } else { 104 | io.Copy(rw, resp.Body) 105 | } 106 | } 107 | 108 | func (c *CRProxy) cacheManifestContent(ctx context.Context, info *PathInfo, content []byte) error { 109 | h := sha256.New() 110 | h.Write(content) 111 | hash := hex.EncodeToString(h.Sum(nil)[:]) 112 | 113 | if strings.HasPrefix(info.Manifests, "sha256:") { 114 | if info.Manifests[7:] != hash { 115 | return fmt.Errorf("expected hash %s is not same to %s", info.Manifests[7:], hash) 116 | } 117 | } else { 118 | manifestLinkPath := manifestTagCachePath(info.Host, info.Image, info.Manifests) 119 | err := c.storageDriver.PutContent(ctx, manifestLinkPath, []byte("sha256:"+hash)) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | 125 | manifestLinkPath := manifestRevisionsCachePath(info.Host, info.Image, hash) 126 | err := c.storageDriver.PutContent(ctx, manifestLinkPath, []byte("sha256:"+hash)) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | blobCachePath := blobCachePath(hash) 132 | err = c.storageDriver.PutContent(ctx, blobCachePath, content) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | if c.manifestCacheDuration > 0 { 138 | c.manifestCache.Store(manifestLinkPath, time.Now()) 139 | } 140 | return nil 141 | } 142 | 143 | func (c *CRProxy) cachedManifest(rw http.ResponseWriter, r *http.Request, info *PathInfo, try bool) bool { 144 | if try && c.manifestCacheDuration == 0 { 145 | return false 146 | } 147 | 148 | ctx := r.Context() 149 | var manifestLinkPath string 150 | if strings.HasPrefix(info.Manifests, "sha256:") { 151 | manifestLinkPath = manifestRevisionsCachePath(info.Host, info.Image, info.Manifests[7:]) 152 | } else { 153 | manifestLinkPath = manifestTagCachePath(info.Host, info.Image, info.Manifests) 154 | } 155 | 156 | if try { 157 | last, ok := c.manifestCache.Load(manifestLinkPath) 158 | if !ok { 159 | return false 160 | } 161 | 162 | if time.Since(last) > c.manifestCacheDuration { 163 | return false 164 | } 165 | } 166 | 167 | content, err := c.storageDriver.GetContent(ctx, manifestLinkPath) 168 | if err == nil { 169 | digest := string(content) 170 | blobCachePath := blobCachePath(digest) 171 | content, err := c.storageDriver.GetContent(ctx, blobCachePath) 172 | if err == nil { 173 | 174 | mt := struct { 175 | MediaType string `json:"mediaType"` 176 | }{} 177 | err := json.Unmarshal(content, &mt) 178 | if err != nil { 179 | if c.logger != nil { 180 | c.logger.Println("Manifest blob cache err", blobCachePath, err) 181 | } 182 | return false 183 | } 184 | if c.logger != nil { 185 | c.logger.Println("Manifest blob cache hit", blobCachePath) 186 | } 187 | rw.Header().Set("docker-content-digest", digest) 188 | rw.Header().Set("Content-Type", mt.MediaType) 189 | rw.Header().Set("Content-Length", strconv.FormatInt(int64(len(content)), 10)) 190 | if r.Method != http.MethodHead { 191 | rw.Write(content) 192 | } 193 | return true 194 | } 195 | if c.logger != nil { 196 | c.logger.Println("Manifest blob cache missed", blobCachePath, err) 197 | } 198 | } else { 199 | if c.logger != nil { 200 | c.logger.Println("Manifest cache missed", manifestLinkPath, err) 201 | } 202 | } 203 | 204 | return false 205 | } 206 | -------------------------------------------------------------------------------- /ctx_value.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type infoCtxKey struct{} 8 | type InfoCtxValue struct { 9 | LastRedirect string 10 | } 11 | 12 | func withCtxValue(ctx context.Context) context.Context { 13 | return context.WithValue(ctx, infoCtxKey{}, &InfoCtxValue{}) 14 | } 15 | 16 | func GetCtxValue(ctx context.Context) *InfoCtxValue { 17 | v, ok := ctx.Value(infoCtxKey{}).(*InfoCtxValue) 18 | if !ok { 19 | return nil 20 | } 21 | return v 22 | } 23 | -------------------------------------------------------------------------------- /examples/default/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | log 3 | certbot 4 | nginx 5 | -------------------------------------------------------------------------------- /examples/default/README.md: -------------------------------------------------------------------------------- 1 | # 📢 部署须知 2 | 3 | 当前的部署形式其实只适用于小量使用的场景, 用的人多会很卡啊, 适用于大量使用的场景的代码是已经在项目里了还没文档描述(作者精力有限,欢迎大佬提交pr) 4 | 5 | 另外本文中显示的仓库地址:`kubesre.xyz`,只做本文演示使用,不保证其稳定和有效性。 6 | 7 | ## 开始部署 8 | 9 | ### 前提 10 | 11 | - 准备一台访问网络畅通的服务器 12 | 13 | > 推荐腾讯云轻量服务器,区域地域选择亚洲离中国近的地方,最近618特惠,一个月的才26元 [点击直达](https://curl.qcloud.com/RW4e7hIf) 14 | 15 | - 准备一个域名(无需备案)并做好 DNS 解析:添加两条解析记录 `@` 记录 和 `*` 记录到准备好的服务器的 IP 16 | 17 | > 域名推荐选择`xyz`结尾了,首年最低7元. 18 | > 19 | > 如果你想使用二级域名,以`cr.kubesre.xyz`举例,你可以将`cr`和`*.cr`解析到服务器ip. 20 | 21 | - 安装好 docker 和 docker-compose 参考:[菜鸟教程](https://www.runoob.com/docker/centos-docker-install.html) 22 | 23 | ### 拉取代码 24 | 25 | ``` 26 | git clone https://github.com/wzshiming/crproxy.git 27 | ``` 28 | 29 | ### 进入项目目录 30 | 31 | ``` 32 | cd crproxy/examples/default 33 | ``` 34 | 35 | ### 修改gateway域名 36 | 37 | vim start.sh 第五行 38 | 39 | ``` 40 | 原:gateway=m.daocloud.io 41 | 修改为:gateway=kubesre.xyz #改成你自己的域名 42 | ``` 43 | 44 | ### 启动服务 45 | 46 | ``` 47 | ./start.sh 48 | ``` 49 | 50 | > 如果出现了报错大概率是申请ssl证书时,ca机构检查到没有将域名解析到当前服务器导致的. 51 | > 52 | > 如果刚刚添加了域名解析,等解析生效后重新执行`./start.sh`即可 53 | 54 | 如果一切正常这时候你就可以通过添加前缀的方式拉取镜像了. 55 | 56 | 假如你的域名是:`kubesre.xyz` 57 | 58 | **使用增加前缀拉取镜像,比如:** 59 | 60 | **映射关系如下** 61 | 62 | ``` 63 | k8s.gcr.io/coredns/coredns:v1.8.6 => kubesre.xyz/k8s.gcr.io/coredns/coredns:v1.8.6 64 | ``` 65 | 66 | **拉取镜像** 67 | 68 | ``` 69 | docker pull kubesre.xyz/k8s.gcr.io/coredns/coredns:v1.8.6 70 | ``` 71 | 72 | 📢 注意:**如果你想使用前缀替换的方式拉取镜像 (务必域名做好 `*` 解析到服务器)** 73 | **映射关系如下** 74 | 75 | ``` 76 | k8s.gcr.io/coredns/coredns:v1.8.6 => k8s-gcr.kubesre.xyz/coredns/coredns:v1.8.6 77 | ``` 78 | 79 | 那你就需要执行 `setup-alias.sh` 脚本添加 `k8s-gcr` 作为 `k8s.gcr.io` 别名 80 | 81 | ### 添加别名 82 | 83 | **设置环境变量** 84 | 85 | ``` 86 | GETEWAY=kubesre.xyz ##替换成自己的域名 87 | ``` 88 | 89 | > 第一个参数前缀替换的域名 90 | > 第二个参数是源站的域名 91 | > 第三个参数是在`start.sh`脚本里配置的网关域名 92 | 93 | ``` 94 | ./setup-alias.sh k8s-gcr.${GETEWAY} k8s.gcr.io ${GETEWAY} 95 | ``` 96 | 97 | ### 为别名申请证书 98 | 99 | ``` 100 | update-tls.sh k8s-gcr.${GETEWAY} 101 | ``` 102 | 103 | **重启一下nginx服务** 104 | 105 | ``` 106 | ./reload.sh 107 | ``` 108 | 109 | 不出意外这时候你就可以使用前缀替换方式拉取镜像了 110 | 111 | **感受一下愉快的拉取镜像吧** 112 | 113 | ``` 114 | docker pull k8s-gcr.kubesre.xyz/coredns/coredns:v1.8.6 115 | ``` 116 | 117 | ## 扩展 118 | 119 | ### 常用的镜像仓库 120 | 121 | 常用的镜像仓库一般有这些: 122 | 123 | | 源站 | 别名 | 124 | | ----------------------- | ---------------------- | 125 | | cr.l5d.io | l5d.kubesre.xyz | 126 | | docker.elastic.co | elastic.kubesre.xyz | 127 | | docker.io | docker.kubesre.xyz | 128 | | gcr.io | gcr.kubesre.xyz | 129 | | ghcr.io | ghcr.kubesre.xyz | 130 | | k8s.gcr.io | k8s-gcr.kubesre.xyz | 131 | | registry.k8s.io | k8s.kubesre.xyz | 132 | | mcr.microsoft.com | mcr.kubesre.xyz | 133 | | nvcr.io | nvcr.kubesre.xyz | 134 | | quay.io | quay.kubesre.xyz | 135 | | registry.jujucharms.com | jujucharms.kubesre.xyz | 136 | 137 | ### 添加常用镜像仓库别名 138 | 139 | **设置环境变量** 140 | 141 | ```bash 142 | GETEWAY=kubesre.xyz ##替换成自己的域名 143 | ``` 144 | 145 | **添加别名** 146 | 147 | ```bash 148 | ./setup-alias.sh l5d.${GETEWAY} cr.l5d.io ${GETEWAY} 149 | ./setup-alias.sh elastic.${GETEWAY} docker.elastic.co ${GETEWAY} 150 | ./setup-alias.sh docker.${GETEWAY} docker.io ${GETEWAY} 151 | ./setup-alias.sh gcr.${GETEWAY} gcr.io ${GETEWAY} 152 | ./setup-alias.sh ghcr.${GETEWAY} ghcr.io ${GETEWAY} 153 | ./setup-alias.sh k8s-gcr.${GETEWAY} k8s.gcr.io ${GETEWAY} 154 | ./setup-alias.sh k8s.${GETEWAY} registry.k8s.io ${GETEWAY} 155 | ./setup-alias.sh mcr.${GETEWAY} mcr.microsoft.com ${GETEWAY} 156 | ./setup-alias.sh nvcr.${GETEWAY} nvcr.io ${GETEWAY} 157 | ./setup-alias.sh quay.${GETEWAY} quay.io ${GETEWAY} 158 | ./setup-alias.sh jujucharms.${GETEWAY} registry.jujucharms.com ${GETEWAY} 159 | ./setup-alias.sh rocks-canonical.${GETEWAY} rocks.canonical.com ${GETEWAY} 160 | ``` 161 | 162 | **给别名申请证书** 163 | 164 | ```bash 165 | ./update-tls.sh gcr.${GETEWAY} 166 | ./update-tls.sh ghcr.${GETEWAY} 167 | ./update-tls.sh k8s-gcr.${GETEWAY} 168 | ./update-tls.sh k8s.${GETEWAY} 169 | ./update-tls.sh k8s.${GETEWAY} 170 | ./update-tls.sh mcr.${GETEWAY} 171 | ./update-tls.sh nvcr.${GETEWAY} 172 | ./update-tls.sh quay.${GETEWAY} 173 | ./update-tls.sh jujucharms.${GETEWAY} 174 | ./update-tls.sh rocks-canonical.${GETEWAY} 175 | ``` 176 | 177 | 最后重启下就可以了 178 | 179 | ``` 180 | ./reload.sh 181 | ``` 182 | ## 采用者列表 183 | - kubesre.xyz [docker-registry-mirrors](https://github.com/kubesre/docker-registry-mirrors) 184 | - m.daocloud.io [public-image-mirror](https://github.com/DaoCloud/public-image-mirror) 185 | -------------------------------------------------------------------------------- /examples/default/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | gateway: 5 | image: ghcr.io/wzshiming/nginx-certbot:v1.21.4 6 | container_name: gateway 7 | restart: unless-stopped 8 | volumes: 9 | - ./nginx/:/etc/nginx/conf.d/ 10 | - ./certbot/conf/:/etc/letsencrypt/ 11 | - ./certbot/www/:/var/www/certbot/ 12 | - ./html:/usr/share/nginx/html 13 | - ./log/nginx/:/var/log/nginx/ 14 | - ./log/letsencrypt:/var/log/letsencrypt/ 15 | ports: 16 | - "80:80" 17 | - "443:443" 18 | 19 | # registry: 20 | # image: docker.io/library/registry:2.8.1 21 | # container_name: registry 22 | # restart: unless-stopped 23 | # command: 24 | # - registry 25 | # - serve 26 | # - /etc/docker/registry/config.yml 27 | # volumes: 28 | # - ./registry:/var/lib/registry:rw 29 | # - ./registry/config.yml:/etc/docker/registry/config.yml:ro 30 | 31 | crproxy: 32 | image: ghcr.io/daocloud/crproxy/crproxy:v0.9.1 33 | container_name: crproxy 34 | restart: unless-stopped 35 | # command: | 36 | # # add docker.io username increase pulls from 100 to 200 per 6 hour period 37 | # -u username:password@docker.io 38 | # # support ignoring prefixes, e.g. docker pull mysql 39 | # --default-registry docker.io 40 | # # cache storage example: aliyunoss 41 | # --storage-driver oss 42 | # --storage-parameters accesskeyid=xxxxx,accesskeysecret=xxxxxxx,region=oss-ap-xxxx-1,encrypt=true,bucket=xxxx 43 | # # user 44 | # --simple-auth 45 | # --simple-auth-user user1=pass1 46 | # --simple-auth-user user2=pass2 47 | 48 | ## For crproxy via a proxy 49 | 50 | # environment: 51 | # - https_proxy=http://proxy:8080 52 | # - http_proxy=http://proxy:8080 53 | 54 | # proxy: 55 | # image: ghcr.io/wzshiming/bridge/bridge:v0.8.6 56 | # container_name: proxy 57 | # restart: unless-stopped 58 | # ports: 59 | # - 8080:8080 60 | # command: -b :8080 -p - 61 | -------------------------------------------------------------------------------- /examples/default/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Container Registry Proxy 6 | 7 | 8 | 9 |
10 |

11 | Prefixes the image used with 'm.daocloud.io' 12 |

13 |

14 | Source: https://github.com/wzshiming/crproxy 15 |

16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/default/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | #access_log /var/log/nginx/host.access.log main; 6 | 7 | location / { 8 | root /usr/share/nginx/html; 9 | index index.html index.htm; 10 | } 11 | 12 | #error_page 404 /404.html; 13 | 14 | # redirect server error pages to the static page /50x.html 15 | # 16 | error_page 500 502 503 504 /50x.html; 17 | location = /50x.html { 18 | root /usr/share/nginx/html; 19 | } 20 | 21 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 22 | # 23 | #location ~ \.php$ { 24 | # proxy_pass http://127.0.0.1; 25 | #} 26 | 27 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 28 | # 29 | #location ~ \.php$ { 30 | # root html; 31 | # fastcgi_pass 127.0.0.1:9000; 32 | # fastcgi_index index.php; 33 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 34 | # include fastcgi_params; 35 | #} 36 | 37 | # deny access to .htaccess files, if Apache's document root 38 | # concurs with nginx's one 39 | # 40 | #location ~ /\.ht { 41 | # deny all; 42 | #} 43 | } -------------------------------------------------------------------------------- /examples/default/registry/config.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | accesslog: 4 | disabled: true 5 | storage: 6 | cache: 7 | blobdescriptor: inmemory 8 | filesystem: 9 | rootdirectory: "/var/lib/registry" 10 | maintenance: 11 | uploadpurging: 12 | enabled: false 13 | readonly: 14 | enabled: true 15 | http: 16 | addr: :5000 17 | headers: 18 | X-Content-Type-Options: [nosniff] 19 | 20 | health: 21 | storagedriver: 22 | enabled: false 23 | 24 | validation: 25 | disabled: true 26 | 27 | proxy: 28 | remoteurl: http://crproxy:8080 29 | -------------------------------------------------------------------------------- /examples/default/reload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose exec gateway nginx -s reload 4 | -------------------------------------------------------------------------------- /examples/default/setup-alias.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | domain=${1:-} 4 | 5 | if [[ -z "${domain}" ]]; then 6 | echo "domain is required" 7 | exit 1 8 | fi 9 | 10 | origin=${2:-} 11 | 12 | if [[ -z "${origin}" ]]; then 13 | echo "origin is required" 14 | exit 1 15 | fi 16 | 17 | gateway=${3:-} 18 | 19 | if [[ -z "${gateway}" ]]; then 20 | echo "gateway is required" 21 | exit 1 22 | fi 23 | 24 | function gen() { 25 | local domain=$1 26 | local origin=$2 27 | local gateway=$3 28 | cat <"${conf}" 53 | fi 54 | -------------------------------------------------------------------------------- /examples/default/setup-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | domain=${1:-} 4 | 5 | if [[ -z "${domain}" ]]; then 6 | echo "domain is required" 7 | exit 1 8 | fi 9 | 10 | endpoint=${2:-} 11 | 12 | if [[ -z "${endpoint}" ]]; then 13 | echo "endpoint is required" 14 | exit 1 15 | fi 16 | 17 | function gen() { 18 | local domain=$1 19 | local endpoint=$2 20 | cat <"nginx/gateway-${domain}.conf" 79 | fi 80 | -------------------------------------------------------------------------------- /examples/default/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose up -d 4 | 5 | gateway=m.daocloud.io 6 | 7 | declare -A mapping=() 8 | 9 | #./setup-gateway.sh "${gateway}" "registry:5000" 10 | ./setup-gateway.sh "${gateway}" "crproxy:8080" 11 | ./update-tls.sh "${gateway}" 12 | 13 | for key in ${!mapping[*]}; do 14 | ./setup-alias.sh "${key}" "${mapping[$key]}" "${gateway}" 15 | ./update-tls.sh "${key}" 16 | done 17 | -------------------------------------------------------------------------------- /examples/default/update-tls.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | domain=${1:-} 4 | 5 | if [[ -z "${domain}" ]]; then 6 | echo "domain is required" 7 | exit 1 8 | fi 9 | 10 | function cert_renew() { 11 | local domain=$1 12 | docker-compose exec gateway certbot --nginx -n --rsa-key-size 4096 --agree-tos --register-unsafely-without-email --domains "${domain}" 13 | } 14 | 15 | cert_renew "${domain}" 16 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # 📢 部署须知 2 | 3 | 当前的部署形式其实只适用于个人小量使用的场景 4 | 5 | ## 快速开始 6 | 7 | ### 前提 8 | - 准备一台服务器, 需要确保 80 和 443 端口打开 9 | - 准备一个域名并做好 DNS 解析到准备好的服务器的 IP 10 | - 安装好 docker 和 docker-compose 参考:[菜鸟教程](https://www.runoob.com/docker/centos-docker-install.html) 11 | 12 | ### 启动 13 | 14 | 在服务器里新建一个文件 `docker-compose.yaml` 内容如下 15 | 16 | ``` yaml 17 | version: '3' 18 | services: 19 | crproxy: 20 | image: ghcr.io/daocloud/crproxy/crproxy:v0.9.1 21 | container_name: crproxy 22 | restart: unless-stopped 23 | ports: 24 | - 80:8080 25 | - 443:8080 26 | command: | 27 | --acme-cache-dir=/tmp/acme 28 | --acme-hosts=* 29 | --default-registry=docker.io 30 | tmpfs: 31 | - /tmp/acme 32 | 33 | # 非必须, 如果这台服务器无法畅通的达到你要的镜像仓库可以尝试配置 34 | environment: 35 | - https_proxy=http://proxy:8080 36 | - http_proxy=http://proxy:8080 37 | ``` 38 | 39 | 然后执行 `docker-compose up -d` 40 | 41 | 42 | ## 然后就能愉快的拉取镜像了 43 | 44 | ``` shell 45 | docker pull 你的域名/hello-world 46 | ``` 47 | 48 | 也可以添加到 /etc/docker/daemon.json 49 | 50 | ``` json 51 | { 52 | "registry-mirrors": [ 53 | "https://你的域名" 54 | ] 55 | } 56 | ``` 57 | 58 | ``` shell 59 | docker pull hello-world 60 | ``` 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/daocloud/crproxy 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace 7 | github.com/distribution/reference v0.6.0 8 | github.com/docker/distribution v0.0.0 9 | github.com/gorilla/handlers v1.5.2 10 | github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.6+incompatible 11 | github.com/opencontainers/go-digest v1.0.0 12 | github.com/spf13/pflag v1.0.5 13 | github.com/wzshiming/cmux v0.4.2 14 | github.com/wzshiming/geario v0.0.0-20240308093553-a996e3817533 15 | github.com/wzshiming/hostmatcher v0.0.3 16 | github.com/wzshiming/httpseek v0.1.0 17 | github.com/wzshiming/lru v0.1.0 18 | golang.org/x/crypto v0.28.0 19 | ) 20 | 21 | replace github.com/docker/distribution => github.com/distribution/distribution v2.8.3+incompatible 22 | 23 | require ( 24 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 25 | github.com/Azure/azure-sdk-for-go v56.3.0+incompatible // indirect 26 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 27 | github.com/Azure/go-autorest/autorest v0.11.24 // indirect 28 | github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect 29 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 30 | github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect 31 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 32 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 33 | github.com/aws/aws-sdk-go v1.48.10 // indirect 34 | github.com/beorn7/perks v1.0.1 // indirect 35 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 36 | github.com/dnaeon/go-vcr v1.2.0 // indirect 37 | github.com/docker/go-metrics v0.0.1 // indirect 38 | github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect 39 | github.com/felixge/httpsnoop v1.0.4 // indirect 40 | github.com/gofrs/uuid v4.0.0+incompatible // indirect 41 | github.com/golang-jwt/jwt/v4 v4.5.1 // indirect 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 43 | github.com/golang/protobuf v1.5.3 // indirect 44 | github.com/google/s2a-go v0.1.4 // indirect 45 | github.com/google/uuid v1.3.1 // indirect 46 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 47 | github.com/googleapis/gax-go/v2 v2.11.0 // indirect 48 | github.com/gorilla/mux v1.8.1 // indirect 49 | github.com/jmespath/go-jmespath v0.4.0 // indirect 50 | github.com/kr/text v0.2.0 // indirect 51 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 52 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect 53 | github.com/opentracing/opentracing-go v1.2.0 // indirect 54 | github.com/prometheus/client_golang v1.17.0 // indirect 55 | github.com/prometheus/client_model v0.5.0 // indirect 56 | github.com/prometheus/common v0.44.0 // indirect 57 | github.com/prometheus/procfs v0.11.1 // indirect 58 | github.com/sirupsen/logrus v1.9.3 // indirect 59 | github.com/stretchr/testify v1.8.4 // indirect 60 | github.com/wzshiming/trie v0.3.1 // indirect 61 | go.opencensus.io v0.24.0 // indirect 62 | golang.org/x/net v0.30.0 // indirect 63 | golang.org/x/oauth2 v0.23.0 // indirect 64 | golang.org/x/sys v0.26.0 // indirect 65 | golang.org/x/text v0.19.0 // indirect 66 | google.golang.org/api v0.126.0 // indirect 67 | google.golang.org/appengine v1.6.7 // indirect 68 | google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 // indirect 69 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 70 | google.golang.org/grpc v1.59.0 // indirect 71 | google.golang.org/protobuf v1.33.0 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 4 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 5 | github.com/Azure/azure-sdk-for-go v56.3.0+incompatible h1:DmhwMrUIvpeoTDiWRDtNHqelNUd3Og8JCkrLHQK795c= 6 | github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 7 | github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= 8 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 9 | github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= 10 | github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= 11 | github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= 12 | github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= 13 | github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= 14 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 15 | github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= 16 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 17 | github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= 18 | github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= 19 | github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= 20 | github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 21 | github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= 22 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 23 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 24 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 25 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 26 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 27 | github.com/aws/aws-sdk-go v1.48.10 h1:0LIFG3wp2Dt6PsxKWCg1Y1xRrn2vZnW5/gWdgaBalKg= 28 | github.com/aws/aws-sdk-go v1.48.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= 29 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 30 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 31 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 32 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 33 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 34 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 35 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 36 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 37 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 38 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 39 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 40 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 41 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 42 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 43 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 44 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 45 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 47 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace h1:1SnCTPFh2AADpm7ti864EYaugexyiDFt55BW188+d6k= 49 | github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace/go.mod h1:TK05uvk4XXfK2kdvRwfcZ1NaxjDxmm7H3aQLko0mJxA= 50 | github.com/distribution/distribution v2.8.3+incompatible h1:RlpEXBLq/WPXYvBYMDAmBX/SnhD67qwtvW/DzKc8pAo= 51 | github.com/distribution/distribution v2.8.3+incompatible/go.mod h1:EgLm2NgWtdKgzF9NpMzUKgzmR7AMmb0VQi2B+ZzDRjc= 52 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 53 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 54 | github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= 55 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 56 | github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 57 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 58 | github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= 59 | github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= 60 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 61 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 62 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 63 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 64 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 65 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 66 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 67 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 68 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 69 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 70 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 71 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 72 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 73 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 74 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 75 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 76 | github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 77 | github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 78 | github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= 79 | github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 81 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 83 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 84 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 85 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 86 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 88 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 89 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 90 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 91 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 92 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 93 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 94 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 95 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 96 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 97 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 98 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 99 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 100 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 101 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 102 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 103 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 104 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 108 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 109 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 110 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 111 | github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= 112 | github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= 113 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 114 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 115 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 116 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= 117 | github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 118 | github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= 119 | github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= 120 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 121 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 122 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 123 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 124 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 125 | github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.6+incompatible h1:/2MdLc7zHJqzV7J2uVGaoGymVobB/OHC8wmEyWRaK68= 126 | github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.6+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= 127 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 128 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 129 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 130 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 131 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 132 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 133 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 134 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 135 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 136 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 137 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 138 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 139 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 140 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 141 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 142 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 143 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 144 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 145 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 146 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 147 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 148 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 149 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 150 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 151 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 152 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= 153 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 154 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 155 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 156 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 157 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 158 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 159 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 160 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 161 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 162 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 163 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 164 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 165 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 166 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 167 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 168 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 169 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 170 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 171 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 172 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 173 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= 174 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 175 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 176 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 177 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 178 | github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= 179 | github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= 180 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 181 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 182 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 183 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 184 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 185 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 186 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 187 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 188 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 189 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 190 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 191 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 192 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 193 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 194 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 195 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 196 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 197 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 198 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 199 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 200 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 201 | github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= 202 | github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 203 | github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= 204 | github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 205 | github.com/wzshiming/cmux v0.4.2 h1:tI73lL5ztVfiqw7R5m5BkxT1+vQ2PBo/oV6qPbNGPiA= 206 | github.com/wzshiming/cmux v0.4.2/go.mod h1:JgE61QfZAjEyNMX0iZo9zIKY6pr9bHVY132yYPwHW5U= 207 | github.com/wzshiming/geario v0.0.0-20240308093553-a996e3817533 h1:mq74wxgDCz7Q6CqZYExt0DHf7Ze28lyMW/TNsfcuk8M= 208 | github.com/wzshiming/geario v0.0.0-20240308093553-a996e3817533/go.mod h1:Fodw3HJvNUS+/MgqXCRp9iYLQfynAu/LKXGOWoX+D/Q= 209 | github.com/wzshiming/hostmatcher v0.0.3 h1:+JYAq6vUZXDEQ1Ipfdc/D7HmaIMngcc71ftonyCQVQk= 210 | github.com/wzshiming/hostmatcher v0.0.3/go.mod h1:F04RIvIWEvOIrIKOlQlMuR8vQMKAVf2YhpU6l31Wwz4= 211 | github.com/wzshiming/httpseek v0.1.0 h1:lEgL7EBELT/VV9UaTp+m3kw5Pe1KOUdY+IPnKkag6tI= 212 | github.com/wzshiming/httpseek v0.1.0/go.mod h1:YoZhlLIwNjTBDXIT8NpK5zRjOgZouRXPaBfjVXdqMMs= 213 | github.com/wzshiming/lru v0.1.0 h1:937SBBo9lDBRMivJqF46eVK2Q3omRWnN7hWpr2i3xZg= 214 | github.com/wzshiming/lru v0.1.0/go.mod h1:uaI2W/Gx3r1gHIc3FG38PKK1idvMXhgY8lToXaBsDIQ= 215 | github.com/wzshiming/trie v0.3.1 h1:YpuoqmEQFJiW0mns/mM6Qk4kdWrXc8kc28/KR1vn0m8= 216 | github.com/wzshiming/trie v0.3.1/go.mod h1:c9thxXTh4KcGkejt4sUsO4c5GUmWpxeWzOJ7AZJaI+8= 217 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 218 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 219 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 220 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 221 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 222 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 223 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 224 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 225 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 226 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 227 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 228 | golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 229 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 230 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 231 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 232 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 233 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 234 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 235 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 236 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 237 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 238 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 239 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 240 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 241 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 242 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 243 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 244 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 247 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 248 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 249 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 250 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 251 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 252 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 253 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 254 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 255 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 256 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 257 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 258 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 259 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 260 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 261 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 262 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 263 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 264 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 265 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 266 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 267 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 268 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 270 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 272 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 273 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 274 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 275 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 276 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 277 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 278 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 279 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 280 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 281 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 282 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 283 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 284 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 285 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 286 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 287 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 288 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 289 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 290 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 291 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 292 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 293 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 294 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 295 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 296 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 297 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 298 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 299 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 300 | google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= 301 | google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= 302 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 303 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 304 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 305 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 306 | google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA= 307 | google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= 308 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 309 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 310 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 311 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 312 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= 313 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= 314 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= 315 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= 316 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 317 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 318 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 319 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 320 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 321 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 322 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 323 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 324 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 325 | google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= 326 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 327 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 328 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 329 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 330 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 331 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 332 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 333 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 334 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 335 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 336 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 337 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 338 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 339 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 340 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 341 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 342 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 343 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 344 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 345 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 346 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 347 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 348 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 349 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 350 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 351 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 352 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 353 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 354 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 355 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 356 | -------------------------------------------------------------------------------- /hang_manager.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/wzshiming/geario" 7 | ) 8 | 9 | func GetSleepDuration(s geario.B, limit geario.B, r time.Duration) time.Duration { 10 | return time.Duration(s/(limit/geario.B(r)*geario.B(time.Second))) * time.Second 11 | } 12 | -------------------------------------------------------------------------------- /hang_manager_test.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/wzshiming/geario" 8 | ) 9 | 10 | func TestGetSleepDuration(t *testing.T) { 11 | type args struct { 12 | s geario.B 13 | limit geario.B 14 | r time.Duration 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want time.Duration 20 | }{ 21 | { 22 | args: args{ 23 | s: 100, 24 | limit: 100, 25 | r: time.Second, 26 | }, 27 | want: time.Second, 28 | }, 29 | { 30 | args: args{ 31 | s: 200, 32 | limit: 100, 33 | r: time.Second, 34 | }, 35 | want: 2 * time.Second, 36 | }, 37 | { 38 | args: args{ 39 | s: 100, 40 | limit: 50, 41 | r: time.Second, 42 | }, 43 | want: 2 * time.Second, 44 | }, 45 | { 46 | args: args{ 47 | s: 100, 48 | limit: 100, 49 | r: 2 * time.Second, 50 | }, 51 | want: 2 * time.Second, 52 | }, 53 | { 54 | args: args{ 55 | s: 100 * geario.MiB, 56 | limit: geario.MiB, 57 | r: time.Second, 58 | }, 59 | want: 100 * time.Second, 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | if got := GetSleepDuration(tt.args.s, tt.args.limit, tt.args.r); got != tt.want { 65 | t.Errorf("GetSleepDuration() = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/acme/acme.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | 11 | "github.com/wzshiming/hostmatcher" 12 | "golang.org/x/crypto/acme/autocert" 13 | ) 14 | 15 | func hostWhitelist(hosts ...string) autocert.HostPolicy { 16 | matcher := hostmatcher.NewMatcher(hosts) 17 | return func(_ context.Context, host string) error { 18 | if !matcher.Match(host) { 19 | return fmt.Errorf("acme/autocert: host %q not configured in HostWhitelist", host) 20 | } 21 | return nil 22 | } 23 | } 24 | 25 | func NewAcme(domains []string, dir string) *tls.Config { 26 | m := &autocert.Manager{ 27 | Prompt: autocert.AcceptTOS, 28 | } 29 | if len(domains) > 0 { 30 | m.HostPolicy = hostWhitelist(domains...) 31 | } 32 | if dir == "" { 33 | dir = cacheDir() 34 | } 35 | m.Cache = autocert.DirCache(dir) 36 | 37 | tlsConfig := m.TLSConfig() 38 | return tlsConfig 39 | } 40 | 41 | func homeDir() string { 42 | if runtime.GOOS == "windows" { 43 | return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 44 | } 45 | if h := os.Getenv("HOME"); h != "" { 46 | return h 47 | } 48 | return "/" 49 | } 50 | 51 | func cacheDir() string { 52 | const base = "autocert" 53 | switch runtime.GOOS { 54 | case "darwin": 55 | return filepath.Join(homeDir(), "Library", "Caches", base) 56 | case "windows": 57 | for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} { 58 | if v := os.Getenv(ev); v != "" { 59 | return filepath.Join(v, base) 60 | } 61 | } 62 | // Worst case: 63 | return filepath.Join(homeDir(), base) 64 | } 65 | if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { 66 | return filepath.Join(xdg, base) 67 | } 68 | return filepath.Join(homeDir(), ".cache", base) 69 | } 70 | -------------------------------------------------------------------------------- /internal/maps/maps.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // SyncMap is a wrapper around sync.Map that provides a few additional methods. 8 | type SyncMap[K comparable, V any] struct { 9 | m sync.Map 10 | } 11 | 12 | // Load returns the value stored in the map for a key, 13 | // or nil if no value is present. 14 | func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) { 15 | v, ok := m.m.Load(key) 16 | if !ok { 17 | return value, false 18 | } 19 | return v.(V), true 20 | } 21 | 22 | // Store sets the value for a key. 23 | func (m *SyncMap[K, V]) Store(key K, value V) { 24 | m.m.Store(key, value) 25 | } 26 | 27 | // Delete deletes the value for a key. 28 | func (m *SyncMap[K, V]) Delete(key K) { 29 | m.m.Delete(key) 30 | } 31 | 32 | // Range calls f sequentially for each key and value present in the map. 33 | func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) { 34 | m.m.Range(func(key, value interface{}) bool { 35 | return f(key.(K), value.(V)) 36 | }) 37 | } 38 | 39 | // LoadAndDelete deletes the value for a key, returning the previous value if any. 40 | func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { 41 | v, loaded := m.m.LoadAndDelete(key) 42 | if !loaded { 43 | return value, loaded 44 | } 45 | return v.(V), loaded 46 | } 47 | 48 | // LoadOrStore returns the existing value for the key if present. 49 | func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (V, bool) { 50 | v, loaded := m.m.LoadOrStore(key, value) 51 | if !loaded { 52 | return value, loaded 53 | } 54 | return v.(V), loaded 55 | } 56 | 57 | // Swap stores value for key and returns the previous value for that key. 58 | func (m *SyncMap[K, V]) Swap(key K, value V) (V, bool) { 59 | v, loaded := m.m.Swap(key, value) 60 | if !loaded { 61 | return value, loaded 62 | } 63 | return v.(V), loaded 64 | } 65 | 66 | // Size returns the number of items in the map. 67 | func (m *SyncMap[K, V]) Size() int { 68 | size := 0 69 | m.m.Range(func(key, value interface{}) bool { 70 | size++ 71 | return true 72 | }) 73 | return size 74 | } 75 | 76 | // Keys returns all the keys in the map. 77 | func (m *SyncMap[K, V]) Keys() []K { 78 | keys := []K{} 79 | m.m.Range(func(key, value interface{}) bool { 80 | keys = append(keys, key.(K)) 81 | return true 82 | }) 83 | return keys 84 | } 85 | 86 | // Values returns all the values in the map. 87 | func (m *SyncMap[K, V]) Values() []V { 88 | values := []V{} 89 | m.m.Range(func(key, value interface{}) bool { 90 | values = append(values, value.(V)) 91 | return true 92 | }) 93 | return values 94 | } 95 | 96 | // IsEmpty returns true if the map is empty. 97 | func (m *SyncMap[K, V]) IsEmpty() bool { 98 | empty := true 99 | m.m.Range(func(key, value interface{}) bool { 100 | empty = false 101 | return false 102 | }) 103 | return empty 104 | } 105 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/http/httptest" 9 | "time" 10 | 11 | "github.com/daocloud/crproxy/internal/acme" 12 | "github.com/wzshiming/cmux" 13 | "github.com/wzshiming/cmux/pattern" 14 | ) 15 | 16 | func Run(ctx context.Context, address string, handler http.Handler, acmeHosts []string, acmeCache string, certFile, privateKeyFile string) error { 17 | listener, err := net.Listen("tcp", address) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | muxListener := cmux.NewMuxListener(listener) 23 | tlsListener, err := muxListener.MatchPrefix(pattern.Pattern[pattern.TLS]...) 24 | if err != nil { 25 | return fmt.Errorf("match tls listener: %w", err) 26 | } 27 | unmatchedListener, err := muxListener.Unmatched() 28 | if err != nil { 29 | return fmt.Errorf("unmatched listener: %w", err) 30 | } 31 | 32 | ctx, cancel := context.WithCancel(ctx) 33 | defer cancel() 34 | 35 | errCh := make(chan error, 1) 36 | 37 | if (certFile != "" && privateKeyFile != "") || len(acmeHosts) != 0 { 38 | go func() { 39 | svc := &http.Server{ 40 | ReadHeaderTimeout: 5 * time.Second, 41 | BaseContext: func(_ net.Listener) context.Context { 42 | return ctx 43 | }, 44 | Addr: address, 45 | Handler: handler, 46 | } 47 | if len(acmeHosts) != 0 { 48 | svc.TLSConfig = acme.NewAcme(acmeHosts, acmeCache) 49 | } 50 | err = svc.ServeTLS(tlsListener, certFile, privateKeyFile) 51 | if err != nil { 52 | errCh <- fmt.Errorf("serve https: %w", err) 53 | } 54 | }() 55 | } else { 56 | svc := httptest.Server{ 57 | Listener: tlsListener, 58 | Config: &http.Server{ 59 | ReadHeaderTimeout: 5 * time.Second, 60 | BaseContext: func(_ net.Listener) context.Context { 61 | return ctx 62 | }, 63 | Addr: address, 64 | Handler: handler, 65 | }, 66 | } 67 | svc.StartTLS() 68 | } 69 | 70 | go func() { 71 | svc := &http.Server{ 72 | ReadHeaderTimeout: 5 * time.Second, 73 | BaseContext: func(_ net.Listener) context.Context { 74 | return ctx 75 | }, 76 | Addr: address, 77 | Handler: handler, 78 | } 79 | err = svc.Serve(unmatchedListener) 80 | if err != nil { 81 | errCh <- fmt.Errorf("serve http: %w", err) 82 | } 83 | }() 84 | 85 | select { 86 | case err = <-errCh: 87 | case <-ctx.Done(): 88 | err = ctx.Err() 89 | } 90 | 91 | return err 92 | } 93 | -------------------------------------------------------------------------------- /logo/LICENSE: -------------------------------------------------------------------------------- 1 | # The crproxy logo files is licensed under Apache 2.0 license. 2 | -------------------------------------------------------------------------------- /logo/crproxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaoCloud/crproxy/6e21fb998b9e6cc59fb52b41a3ec3df83884e436/logo/crproxy.png -------------------------------------------------------------------------------- /logo/crproxy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/driver/obs/doc.go: -------------------------------------------------------------------------------- 1 | // Copy from github.com/docker/distribution/registry/storage/driver/obs 2 | 3 | package obs 4 | -------------------------------------------------------------------------------- /storage/driver/obs/obs.go: -------------------------------------------------------------------------------- 1 | // Package obs provides a storagedriver.StorageDriver implementation to 2 | // store blobs in HuaweiCloud storage. 3 | // 4 | // This package leverages the huaweicloud/huaweicloud-sdk-go-obs client library 5 | // for interfacing with obs. 6 | // 7 | // Because obs is a key, value store the Stat call does not support last modification 8 | // time for directories (directories are an abstraction for key, value stores) 9 | // 10 | // Note that the contents of incomplete uploads are not accessible even though 11 | // Stat returns their length 12 | // 13 | 14 | package obs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "math" 23 | "net/http" 24 | "reflect" 25 | "sort" 26 | "strconv" 27 | "strings" 28 | "time" 29 | 30 | storagedriver "github.com/docker/distribution/registry/storage/driver" 31 | "github.com/docker/distribution/registry/storage/driver/base" 32 | "github.com/docker/distribution/registry/storage/driver/factory" 33 | "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs" 34 | ) 35 | 36 | // noStorageClass defines the value to be used if storage class is not supported by the OBS endpoint 37 | const noStorageClass obs.StorageClassType = "NONE" 38 | 39 | // OBSStorageClasses lists all compatible (instant retrieval) OBS storage classes 40 | var OBSStorageClasses = []obs.StorageClassType{ 41 | noStorageClass, 42 | obs.StorageClassStandard, 43 | obs.StorageClassWarm, 44 | obs.StorageClassCold, 45 | } 46 | 47 | // validStorageClasses contains known OBS StorageClass 48 | var validStorageClasses = map[obs.StorageClassType]struct{}{} 49 | 50 | var OBSAcls = []obs.AclType{ 51 | obs.AclPrivate, 52 | obs.AclPublicRead, 53 | obs.AclPublicReadWrite, 54 | obs.AclAuthenticatedRead, 55 | obs.AclBucketOwnerRead, 56 | obs.AclBucketOwnerFullControl, 57 | obs.AclLogDeliveryWrite, 58 | obs.AclPublicReadDelivery, 59 | obs.AclPublicReadWriteDelivery, 60 | } 61 | 62 | // validObjectACLs contains known OBS object Acls 63 | var validObjectACLs = map[obs.AclType]struct{}{} 64 | 65 | const ( 66 | driverName = "obs" 67 | 68 | minChunkSize = 5 << 20 69 | maxChunkSize = 5 << 30 70 | defaultChunkSize = 2 * minChunkSize 71 | listMax = 1000 72 | defaultMultipartCopyChunkSize = 32 << 20 73 | defaultMultipartCopyMaxConcurrency = 100 74 | defaultMultipartCopyThresholdSize = 32 << 20 75 | ) 76 | 77 | // DriverParameters A struct that encapsulates all of the driver parameters after all values have been set 78 | type DriverParameters struct { 79 | AccessKey string 80 | SecretKey string 81 | Bucket string 82 | Endpoint string 83 | Encrypt bool 84 | ChunkSize int64 85 | RootDirectory string 86 | KeyID string 87 | MultipartCopyChunkSize int64 88 | MultipartCopyMaxConcurrency int64 89 | MultipartCopyThresholdSize int64 90 | MultipartCombineSmallPart bool 91 | StorageClass obs.StorageClassType 92 | ObjectACL obs.AclType 93 | } 94 | 95 | func init() { 96 | for _, storageClass := range OBSStorageClasses { 97 | validStorageClasses[storageClass] = struct{}{} 98 | } 99 | for _, acl := range OBSAcls { 100 | validObjectACLs[acl] = struct{}{} 101 | } 102 | factory.Register(driverName, &obsDriverFactory{}) 103 | } 104 | 105 | // obsDriverFactory implements the factory.StorageDriverFactory interface 106 | type obsDriverFactory struct{} 107 | 108 | func (factory *obsDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { 109 | return FromParameters(parameters) 110 | } 111 | 112 | type driver struct { 113 | Client *obs.ObsClient 114 | Bucket string 115 | ChunkSize int64 116 | Encrypt bool 117 | RootDirectory string 118 | KeyID string 119 | MultipartCopyChunkSize int64 120 | MultipartCopyMaxConcurrency int64 121 | MultipartCopyThresholdSize int64 122 | StorageClass obs.StorageClassType 123 | ObjectACL obs.AclType 124 | } 125 | 126 | type baseEmbed struct { 127 | base.Base 128 | } 129 | 130 | // Driver is a storagedriver.StorageDriver implementation backed by OBS 131 | // Objects are stored at absolute keys in the provided bucket. 132 | type Driver struct { 133 | baseEmbed 134 | } 135 | 136 | // FromParameters constructs a new Driver with a given parameters map 137 | // Required parameters: 138 | // - accesskey 139 | // - secretkey 140 | // - region 141 | // - bucket 142 | // - encrypt 143 | func FromParameters(parameters map[string]interface{}) (*Driver, error) { 144 | // Providing no values for these is valid in case the user is authenticating 145 | accessKey, ok := parameters["accesskey"] 146 | if !ok { 147 | return nil, fmt.Errorf("No accesskeyid parameter provided") 148 | } 149 | secretKey, ok := parameters["secretkey"] 150 | if !ok { 151 | return nil, fmt.Errorf("No accesskeysecret parameter provided") 152 | } 153 | 154 | endpoint, ok := parameters["endpoint"] 155 | if !ok || fmt.Sprint(endpoint) == "" { 156 | return nil, fmt.Errorf("No region endpoint parameter provided") 157 | } 158 | 159 | bucket, ok := parameters["bucket"] 160 | if !ok || fmt.Sprint(bucket) == "" { 161 | return nil, fmt.Errorf("No bucket parameter provided") 162 | } 163 | 164 | storageClass := obs.StorageClassStandard 165 | storageClassParam, ok := parameters["storageclass"] 166 | if ok { 167 | storageClassString, ok := storageClassParam.(obs.StorageClassType) 168 | if !ok { 169 | return nil, fmt.Errorf( 170 | "the storageclass parameter must be one of %v, %v invalid", 171 | OBSStorageClasses, 172 | storageClassParam, 173 | ) 174 | } 175 | if _, ok = validStorageClasses[storageClassString]; !ok { 176 | return nil, fmt.Errorf( 177 | "the storageclass parameter must be one of %v, %v invalid", 178 | OBSStorageClasses, 179 | storageClassParam, 180 | ) 181 | } 182 | storageClass = storageClassString 183 | } 184 | 185 | objectACL := obs.AclPrivate 186 | objectACLParam, ok := parameters["objectacl"] 187 | if ok { 188 | objectACLString, ok := objectACLParam.(obs.AclType) 189 | if !ok { 190 | return nil, fmt.Errorf( 191 | "the objectacl parameter must be one of %v, %v invalid", 192 | OBSAcls, 193 | objectACLParam, 194 | ) 195 | } 196 | 197 | if _, ok = validObjectACLs[objectACLString]; !ok { 198 | return nil, fmt.Errorf( 199 | "the objectacl parameter must be one of %v, %v invalid", 200 | OBSAcls, 201 | objectACLParam, 202 | ) 203 | } 204 | objectACL = objectACLString 205 | } 206 | 207 | encryptBool := false 208 | encrypt, ok := parameters["encrypt"] 209 | if ok { 210 | switch b := encrypt.(type) { 211 | case bool: 212 | encryptBool = b 213 | case string: 214 | ib, err := strconv.ParseBool(b) 215 | if err != nil { 216 | return nil, err 217 | } 218 | encryptBool = ib 219 | default: 220 | return nil, fmt.Errorf("The encrypt parameter should be a boolean") 221 | } 222 | } 223 | 224 | keyID, ok := parameters["keyid"] 225 | if !ok { 226 | keyID = "" 227 | } 228 | 229 | chunkSize, err := getParameterAsInt64(parameters, "chunksize", defaultChunkSize, minChunkSize, maxChunkSize) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | multipartCopyChunkSize, err := getParameterAsInt64(parameters, "multipartcopychunksize", defaultMultipartCopyChunkSize, minChunkSize, maxChunkSize) 235 | if err != nil { 236 | return nil, err 237 | } 238 | 239 | multipartCopyMaxConcurrency, err := getParameterAsInt64(parameters, "multipartcopymaxconcurrency", defaultMultipartCopyMaxConcurrency, 1, math.MaxInt64) 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | multipartCopyThresholdSize, err := getParameterAsInt64(parameters, "multipartcopythresholdsize", defaultMultipartCopyThresholdSize, 0, maxChunkSize) 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | rootDirectory, ok := parameters["rootdirectory"] 250 | if !ok { 251 | rootDirectory = "" 252 | } 253 | 254 | params := DriverParameters{ 255 | AccessKey: fmt.Sprint(accessKey), 256 | SecretKey: fmt.Sprint(secretKey), 257 | Bucket: fmt.Sprint(bucket), 258 | Endpoint: fmt.Sprint(endpoint), 259 | ChunkSize: chunkSize, 260 | RootDirectory: fmt.Sprint(rootDirectory), 261 | Encrypt: encryptBool, 262 | KeyID: fmt.Sprint(keyID), 263 | MultipartCopyChunkSize: multipartCopyChunkSize, 264 | MultipartCopyMaxConcurrency: multipartCopyMaxConcurrency, 265 | MultipartCopyThresholdSize: multipartCopyThresholdSize, 266 | StorageClass: storageClass, 267 | ObjectACL: objectACL, 268 | } 269 | 270 | return New(params) 271 | } 272 | 273 | // getParameterAsInt64 converts parameters[name] to an int64 value (using 274 | // defaultt if nil), verifies it is no smaller than min, and returns it. 275 | func getParameterAsInt64(parameters map[string]interface{}, name string, defaultt int64, min int64, max int64) (int64, error) { 276 | rv := defaultt 277 | param, ok := parameters[name] 278 | if ok { 279 | switch v := param.(type) { 280 | case string: 281 | vv, err := strconv.ParseInt(v, 0, 64) 282 | if err != nil { 283 | return 0, fmt.Errorf("%s parameter must be an integer, %v invalid", name, param) 284 | } 285 | rv = vv 286 | case int64: 287 | rv = v 288 | case int, uint, int32, uint32, uint64: 289 | rv = reflect.ValueOf(v).Convert(reflect.TypeOf(rv)).Int() 290 | case nil: 291 | // do nothing 292 | default: 293 | return 0, fmt.Errorf("invalid value for %s: %#v", name, param) 294 | } 295 | } 296 | 297 | if rv < min || rv > max { 298 | return 0, fmt.Errorf("the %s %#v parameter should be a number between %d and %d (inclusive)", name, rv, min, max) 299 | } 300 | 301 | return rv, nil 302 | } 303 | 304 | // New constructs a new Driver with the given Aliyun credentials, region, encryption flag, and 305 | // bucketName 306 | func New(params DriverParameters) (*Driver, error) { 307 | client, err := obs.New(params.AccessKey, params.SecretKey, params.Endpoint) 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | d := &driver{ 313 | Client: client, 314 | Bucket: params.Bucket, 315 | ChunkSize: params.ChunkSize, 316 | Encrypt: params.Encrypt, 317 | RootDirectory: params.RootDirectory, 318 | KeyID: params.KeyID, 319 | MultipartCopyChunkSize: params.MultipartCopyChunkSize, 320 | MultipartCopyMaxConcurrency: params.MultipartCopyMaxConcurrency, 321 | MultipartCopyThresholdSize: params.MultipartCopyThresholdSize, 322 | ObjectACL: params.ObjectACL, 323 | StorageClass: params.StorageClass, 324 | } 325 | 326 | return &Driver{ 327 | baseEmbed: baseEmbed{ 328 | Base: base.Base{ 329 | StorageDriver: d, 330 | }, 331 | }, 332 | }, nil 333 | } 334 | 335 | func (d *driver) Name() string { 336 | return driverName 337 | } 338 | 339 | // GetContent retrieves the content stored at "path" as a []byte. 340 | func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { 341 | reader, err := d.Reader(ctx, path, 0) 342 | if err != nil { 343 | return nil, err 344 | } 345 | return io.ReadAll(reader) 346 | } 347 | 348 | // PutContent stores the []byte content at a location designated by "path". 349 | func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { 350 | input := &obs.PutObjectInput{} 351 | input.Bucket = d.Bucket 352 | input.Key = d.OBSPath(path) 353 | input.Body = bytes.NewReader(contents) 354 | input.ContentType = d.getContentType() 355 | input.ACL = d.getACL() 356 | input.StorageClass = d.getStorageClass() 357 | input.SseHeader = d.getEncryptionMode() 358 | _, err := d.Client.PutObject(input) 359 | return d.parseError(path, err) 360 | } 361 | 362 | // Reader retrieves an io.ReadCloser for the content stored at "path" with a 363 | // given byte offset. 364 | func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { 365 | input := obs.GetObjectInput{} 366 | input.Bucket = d.Bucket 367 | input.Key = d.OBSPath(path) 368 | input.RangeStart = offset 369 | input.RangeEnd = math.MaxInt64 370 | output, err := d.Client.GetObject(&input) 371 | if err != nil { 372 | var obsErr obs.ObsError 373 | if errors.As(err, &obsErr) { 374 | if obsErr.Code == "InvalidRange" { 375 | return io.NopCloser(bytes.NewReader(nil)), nil 376 | } 377 | } 378 | return nil, d.parseError(path, err) 379 | } 380 | return output.Body, nil 381 | } 382 | 383 | // Writer returns a FileWriter which will store the content written to it 384 | // at the location designated by "path" after the call to Commit. 385 | func (d *driver) Writer(ctx context.Context, path string, appendParam bool) (storagedriver.FileWriter, error) { 386 | key := d.OBSPath(path) 387 | if !appendParam { 388 | // TODO (brianbland): cancel other uploads at this path 389 | initInput := &obs.InitiateMultipartUploadInput{} 390 | initInput.Bucket = d.Bucket 391 | initInput.Key = key 392 | initInput.ContentType = d.getContentType() 393 | initInput.ACL = d.getACL() 394 | initInput.StorageClass = d.getStorageClass() 395 | initInput.SseHeader = d.getEncryptionMode() 396 | initOutput, err := d.Client.InitiateMultipartUpload(initInput) 397 | if err != nil { 398 | return nil, err 399 | } 400 | return d.newWriter(key, initOutput.UploadId, nil), nil 401 | } 402 | 403 | listMultipartUploadsInput := &obs.ListMultipartUploadsInput{ 404 | Bucket: d.Bucket, 405 | Prefix: key, 406 | } 407 | for { 408 | output, err := d.Client.ListMultipartUploads(listMultipartUploadsInput) 409 | if err != nil { 410 | return nil, d.parseError(path, err) 411 | } 412 | 413 | // output.Uploads can only be empty on the first call 414 | // if there were no more results to return after the first call, output.IsTruncated would have been false 415 | // and the loop would be exited without recalling ListMultipartUploads 416 | if len(output.Uploads) == 0 { 417 | break 418 | } 419 | 420 | var allParts []obs.Part 421 | for _, multi := range output.Uploads { 422 | if key != multi.Key { 423 | continue 424 | } 425 | 426 | partsList, err := d.Client.ListParts(&obs.ListPartsInput{ 427 | Bucket: d.Bucket, 428 | Key: key, 429 | UploadId: multi.UploadId, 430 | }) 431 | if err != nil { 432 | return nil, d.parseError(path, err) 433 | } 434 | allParts = append(allParts, partsList.Parts...) 435 | for partsList.IsTruncated { 436 | partsList, err = d.Client.ListParts(&obs.ListPartsInput{ 437 | Bucket: d.Bucket, 438 | Key: key, 439 | UploadId: multi.UploadId, 440 | PartNumberMarker: partsList.NextPartNumberMarker, 441 | }) 442 | if err != nil { 443 | return nil, d.parseError(path, err) 444 | } 445 | allParts = append(allParts, partsList.Parts...) 446 | } 447 | return d.newWriter(key, multi.UploadId, allParts), nil 448 | } 449 | 450 | // output.NextUploadIdMarker must have at least one element or we would have returned not found 451 | listMultipartUploadsInput.UploadIdMarker = output.NextUploadIdMarker 452 | 453 | // IsTruncated "specifies whether (true) or not (false) all of the results were returned" 454 | // if everything has been returned, break 455 | if !output.IsTruncated { 456 | break 457 | } 458 | } 459 | return nil, storagedriver.PathNotFoundError{Path: path} 460 | } 461 | 462 | // Stat retrieves the FileInfo for the given path, including the current size 463 | // in bytes and the creation time. 464 | func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { 465 | input := &obs.ListObjectsInput{} 466 | input.Bucket = d.Bucket 467 | input.Prefix = d.OBSPath(path) 468 | input.MaxKeys = 1 469 | output, err := d.Client.ListObjects(input) 470 | if err != nil { 471 | return nil, err 472 | } 473 | 474 | fi := storagedriver.FileInfoFields{ 475 | Path: path, 476 | } 477 | 478 | if len(output.Contents) == 1 { 479 | if output.Contents[0].Key != d.OBSPath(path) { 480 | fi.IsDir = true 481 | } else { 482 | fi.IsDir = false 483 | fi.Size = output.Contents[0].Size 484 | fi.ModTime = output.Contents[0].LastModified 485 | } 486 | } else if len(output.CommonPrefixes) == 1 { 487 | fi.IsDir = true 488 | } else { 489 | return nil, storagedriver.PathNotFoundError{Path: path} 490 | } 491 | 492 | return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil 493 | } 494 | 495 | // List returns a list of the objects that are direct descendants of the given path. 496 | func (d *driver) List(ctx context.Context, opath string) ([]string, error) { 497 | path := opath 498 | if path != "/" && path[len(path)-1] != '/' { 499 | path = path + "/" 500 | } 501 | 502 | // This is to cover for the cases when the rootDirectory of the driver is either "" or "/". 503 | // In those cases, there is no root prefix to replace and we must actually add a "/" to all 504 | // results in order to keep them as valid paths as recognized by storagedriver.PathRegexp 505 | prefix := "" 506 | if d.OBSPath("") == "" { 507 | prefix = "/" 508 | } 509 | input := &obs.ListObjectsInput{} 510 | input.Bucket = d.Bucket 511 | input.Prefix = d.OBSPath(path) 512 | input.MaxKeys = listMax 513 | input.Delimiter = "/" 514 | output, err := d.Client.ListObjects(input) 515 | if err != nil { 516 | return nil, d.parseError(opath, err) 517 | } 518 | 519 | files := []string{} 520 | directories := []string{} 521 | 522 | for { 523 | for _, content := range output.Contents { 524 | files = append(files, strings.Replace(content.Key, d.OBSPath(""), prefix, 1)) 525 | } 526 | 527 | for _, commonPrefix := range output.CommonPrefixes { 528 | commonPrefix := commonPrefix 529 | directories = append(directories, strings.Replace(commonPrefix[0:len(commonPrefix)-1], d.OBSPath(""), prefix, 1)) 530 | } 531 | 532 | if output.IsTruncated { 533 | input.Marker = output.NextMarker 534 | output, err = d.Client.ListObjects(input) 535 | if err != nil { 536 | return nil, err 537 | } 538 | } else { 539 | break 540 | } 541 | } 542 | 543 | // This is to cover for the cases when the first key equal to obsPath. 544 | if len(files) > 0 && files[0] == strings.Replace(d.OBSPath(path), d.OBSPath(""), prefix, 1) { 545 | files = files[1:] 546 | } 547 | 548 | if opath != "/" { 549 | if len(files) == 0 && len(directories) == 0 { 550 | // Treat empty response as missing directory, since we don't actually 551 | // have directories in OBS. 552 | return nil, storagedriver.PathNotFoundError{Path: opath} 553 | } 554 | } 555 | 556 | return append(files, directories...), nil 557 | } 558 | 559 | // Move moves an object stored at sourcePath to destPath, removing the original 560 | // object. 561 | func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { 562 | /* This is terrible, but aws doesn't have an actual move. */ 563 | if err := d.copy(ctx, sourcePath, destPath); err != nil { 564 | return err 565 | } 566 | return d.Delete(ctx, sourcePath) 567 | } 568 | 569 | // copy copies an object stored at sourcePath to destPath. 570 | func (d *driver) copy(ctx context.Context, sourcePath string, destPath string) error { 571 | // OBS can copy objects up to 5 GB in size with a single PUT Object - Copy 572 | // operation. For larger objects, the multipart upload API must be used. 573 | // 574 | // Empirically, multipart copy is fastest with 32 MB parts and is faster 575 | // than PUT Object - Copy for objects larger than 32 MB. 576 | 577 | fileInfo, err := d.Stat(ctx, sourcePath) 578 | if err != nil { 579 | return d.parseError(sourcePath, err) 580 | } 581 | 582 | if fileInfo.Size() <= d.MultipartCopyThresholdSize { 583 | input := &obs.CopyObjectInput{} 584 | input.Bucket = d.Bucket 585 | input.Key = d.OBSPath(destPath) 586 | input.ContentType = d.getContentType() 587 | input.ACL = d.getACL() 588 | input.SseHeader = d.getEncryptionMode() 589 | input.SourceSseHeader = d.getEncryptionMode() 590 | input.StorageClass = d.getStorageClass() 591 | input.CopySourceBucket = d.Bucket 592 | input.CopySourceKey = d.OBSPath(sourcePath) 593 | _, err := d.Client.CopyObject(input) 594 | if err != nil { 595 | return d.parseError(sourcePath, err) 596 | } 597 | return nil 598 | } 599 | initInput := &obs.InitiateMultipartUploadInput{} 600 | initInput.Bucket = d.Bucket 601 | initInput.Key = d.OBSPath(destPath) 602 | initInput.ContentType = d.getContentType() 603 | initInput.ACL = d.getACL() 604 | initInput.StorageClass = d.getStorageClass() 605 | initInput.SseHeader = d.getEncryptionMode() 606 | initOutput, err := d.Client.InitiateMultipartUpload(initInput) 607 | if err != nil { 608 | return err 609 | } 610 | 611 | numParts := (fileInfo.Size() + d.MultipartCopyChunkSize - 1) / d.MultipartCopyChunkSize 612 | completedParts := make([]obs.Part, numParts) 613 | errChan := make(chan error, numParts) 614 | limiter := make(chan struct{}, d.MultipartCopyMaxConcurrency) 615 | 616 | for i := range completedParts { 617 | i := int64(i) 618 | go func() { 619 | limiter <- struct{}{} 620 | firstByte := i * d.MultipartCopyChunkSize 621 | lastByte := firstByte + d.MultipartCopyChunkSize - 1 622 | if lastByte >= fileInfo.Size() { 623 | lastByte = fileInfo.Size() - 1 624 | } 625 | copyPartOutput, err := d.Client.CopyPart(&obs.CopyPartInput{ 626 | Bucket: d.Bucket, 627 | Key: d.OBSPath(destPath), 628 | CopySourceBucket: d.Bucket, 629 | CopySourceKey: d.OBSPath(sourcePath), 630 | PartNumber: int(i + 1), 631 | UploadId: initOutput.UploadId, 632 | CopySourceRangeStart: firstByte, 633 | CopySourceRangeEnd: lastByte, 634 | SseHeader: d.getEncryptionMode(), 635 | SourceSseHeader: d.getEncryptionMode(), 636 | }) 637 | if err == nil { 638 | completedParts[i] = obs.Part{ 639 | ETag: copyPartOutput.ETag, 640 | PartNumber: int(i + 1), 641 | } 642 | } 643 | errChan <- err 644 | <-limiter 645 | }() 646 | } 647 | 648 | for range completedParts { 649 | err := <-errChan 650 | if err != nil { 651 | return err 652 | } 653 | } 654 | _, err = d.Client.CompleteMultipartUpload(&obs.CompleteMultipartUploadInput{ 655 | Bucket: d.Bucket, 656 | Key: d.OBSPath(destPath), 657 | UploadId: initOutput.UploadId, 658 | Parts: completedParts, 659 | }) 660 | return err 661 | } 662 | 663 | // Delete recursively deletes all objects stored at "path" and its subpaths. 664 | // We must be careful since obs does not guarantee read after delete consistency 665 | func (d *driver) Delete(ctx context.Context, path string) error { 666 | objects := make([]obs.ObjectToDelete, 0, listMax) 667 | obsPath := d.OBSPath(path) 668 | listObjectsInput := &obs.ListObjectsInput{} 669 | listObjectsInput.Bucket = d.Bucket 670 | listObjectsInput.Prefix = obsPath 671 | 672 | for { 673 | // list all the objects 674 | output, err := d.Client.ListObjects(listObjectsInput) 675 | 676 | // output.Contents can only be empty on the first call 677 | // if there were no more results to return after the first call, resp.IsTruncated would have been false 678 | // and the loop would exit without recalling ListObjects 679 | if err != nil || len(output.Contents) == 0 { 680 | return storagedriver.PathNotFoundError{Path: path} 681 | } 682 | 683 | for _, content := range output.Contents { 684 | // Skip if we encounter a key that is not a subpath (so that deleting "/a" does not delete "/ab"). 685 | if len(content.Key) > len(obsPath) && (content.Key)[len(obsPath)] != '/' { 686 | continue 687 | } 688 | objects = append(objects, obs.ObjectToDelete{ 689 | Key: content.Key, 690 | }) 691 | } 692 | 693 | // Delete objects only if the list is not empty, otherwise obs API returns a cryptic error 694 | if len(objects) > 0 { 695 | output, err := d.Client.DeleteObjects(&obs.DeleteObjectsInput{ 696 | Bucket: d.Bucket, 697 | Objects: objects, 698 | Quiet: false, 699 | }) 700 | if err != nil { 701 | return err 702 | } 703 | 704 | if len(output.Errors) > 0 { 705 | errs := make([]error, 0, len(output.Errors)) 706 | for _, err := range output.Errors { 707 | errs = append(errs, errors.New(err.Message)) 708 | } 709 | 710 | return storagedriver.Error{ 711 | DriverName: driverName, 712 | Enclosed: errors.Join(errs...), 713 | } 714 | } 715 | } 716 | // NOTE: we don't want to reallocate 717 | // the slice so we simply "reset" it 718 | objects = objects[:0] 719 | 720 | listObjectsInput.Marker = output.NextMarker 721 | 722 | // IsTruncated "specifies whether (true) or not (false) all of the results were returned" 723 | // if everything has been returned, break 724 | if !output.IsTruncated { 725 | break 726 | } 727 | } 728 | return nil 729 | } 730 | 731 | // URLFor returns a URL which may be used to retrieve the content stored at the given path. 732 | // May return an UnsupportedMethodErr in certain StorageDriver implementations. 733 | func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { 734 | methodString := http.MethodGet 735 | method, ok := options["method"] 736 | if ok { 737 | methodString, ok = method.(string) 738 | if !ok || (methodString != http.MethodGet && methodString != http.MethodHead) { 739 | return "", storagedriver.ErrUnsupportedMethod{} 740 | } 741 | } 742 | 743 | expiresIn := 20 * time.Minute.Seconds() 744 | expires, ok := options["expiry"] 745 | if ok { 746 | et, ok := expires.(time.Time) 747 | if ok { 748 | expiresIn = time.Until(et).Seconds() 749 | } 750 | } 751 | output, err := d.Client.CreateSignedUrl(&obs.CreateSignedUrlInput{ 752 | Bucket: d.Bucket, 753 | Key: d.OBSPath(path), 754 | Method: obs.HttpMethodType(methodString), 755 | Expires: int(expiresIn), 756 | }) 757 | if err != nil { 758 | return "", err 759 | } 760 | return output.SignedUrl, nil 761 | } 762 | 763 | // Walk traverses a filesystem defined within driver, starting 764 | // from the given path, calling f on each file 765 | func (d *driver) Walk(ctx context.Context, path string, f storagedriver.WalkFn) error { 766 | return storagedriver.WalkFallback(ctx, d, path, f) 767 | } 768 | 769 | func (d *driver) OBSPath(path string) string { 770 | return strings.TrimLeft(strings.TrimRight(d.RootDirectory, "/")+path, "/") 771 | } 772 | 773 | func (d *driver) parseError(path string, err error) error { 774 | var obsErr obs.ObsError 775 | if errors.As(err, &obsErr) { 776 | if obsErr.Code == "NoSuchKey" { 777 | return storagedriver.PathNotFoundError{Path: path} 778 | } 779 | } 780 | return err 781 | } 782 | 783 | func (d *driver) getContentType() string { 784 | return "application/octet-stream" 785 | } 786 | 787 | func (d *driver) getACL() obs.AclType { 788 | return d.ObjectACL 789 | } 790 | 791 | func (d *driver) getStorageClass() obs.StorageClassType { 792 | if d.StorageClass == noStorageClass { 793 | return "" 794 | } 795 | return d.StorageClass 796 | } 797 | 798 | func (d *driver) getEncryptionMode() obs.ISseHeader { 799 | if !d.Encrypt { 800 | return nil 801 | } 802 | return obs.SseKmsHeader{Key: d.KeyID} 803 | } 804 | 805 | func (d *driver) getSSEKMSKeyID() string { 806 | return d.KeyID 807 | } 808 | 809 | // writer attempts to upload parts to OBS in a buffered fashion where the last 810 | // part is at least as large as the chunksize, so the multipart upload could be 811 | // cleanly resumed in the future. This is violated if Close is called after less 812 | // than a full chunk is written. 813 | type writer struct { 814 | driver *driver 815 | key string 816 | uploadID string 817 | parts []obs.Part 818 | size int64 819 | readyPart []byte 820 | pendingPart []byte 821 | closed bool 822 | committed bool 823 | cancelled bool 824 | } 825 | 826 | func (d *driver) newWriter(key, uploadID string, parts []obs.Part) storagedriver.FileWriter { 827 | var size int64 828 | for _, part := range parts { 829 | size += part.Size 830 | } 831 | return &writer{ 832 | driver: d, 833 | key: key, 834 | uploadID: uploadID, 835 | parts: parts, 836 | size: size, 837 | } 838 | } 839 | 840 | type completedParts []obs.Part 841 | 842 | func (a completedParts) Len() int { return len(a) } 843 | func (a completedParts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 844 | func (a completedParts) Less(i, j int) bool { return a[i].PartNumber < a[j].PartNumber } 845 | 846 | func (w *writer) Write(p []byte) (n int, err error) { 847 | if w.closed { 848 | return 0, fmt.Errorf("already closed") 849 | } else if w.committed { 850 | return 0, fmt.Errorf("already committed") 851 | } else if w.cancelled { 852 | return 0, fmt.Errorf("already cancelled") 853 | } 854 | 855 | // If the last written part is smaller than minChunkSize, we need to make a 856 | // new multipart upload :sadface: 857 | if len(w.parts) > 0 && int(w.parts[len(w.parts)-1].Size) < minChunkSize { 858 | var completedUploadedParts completedParts 859 | for _, part := range w.parts { 860 | completedUploadedParts = append(completedUploadedParts, obs.Part{ 861 | ETag: part.ETag, 862 | PartNumber: part.PartNumber, 863 | }) 864 | } 865 | 866 | sort.Sort(completedUploadedParts) 867 | 868 | _, err := w.driver.Client.CompleteMultipartUpload(&obs.CompleteMultipartUploadInput{ 869 | Bucket: w.driver.Bucket, 870 | Key: w.key, 871 | UploadId: w.uploadID, 872 | Parts: completedUploadedParts, 873 | }) 874 | if err != nil { 875 | w.driver.Client.AbortMultipartUpload(&obs.AbortMultipartUploadInput{ 876 | Bucket: w.driver.Bucket, 877 | Key: w.key, 878 | UploadId: w.uploadID, 879 | }) 880 | return 0, err 881 | } 882 | initInput := &obs.InitiateMultipartUploadInput{} 883 | initInput.Bucket = w.driver.Bucket 884 | initInput.Key = w.key 885 | initInput.ContentType = w.driver.getContentType() 886 | initInput.ACL = w.driver.getACL() 887 | initInput.StorageClass = w.driver.getStorageClass() 888 | initInput.SseHeader = w.driver.getEncryptionMode() 889 | initOutput, err := w.driver.Client.InitiateMultipartUpload(initInput) 890 | if err != nil { 891 | return 0, err 892 | } 893 | w.uploadID = initOutput.UploadId 894 | 895 | // If the entire written file is smaller than minChunkSize, we need to make 896 | // a new part from scratch :double sad face: 897 | if w.size < minChunkSize { 898 | getObjectInput := &obs.GetObjectInput{} 899 | getObjectInput.Bucket = w.driver.Bucket 900 | getObjectInput.Key = w.key 901 | getObjectOutput, err := w.driver.Client.GetObject(getObjectInput) 902 | if err != nil { 903 | return 0, err 904 | } 905 | defer getObjectOutput.Body.Close() 906 | w.parts = nil 907 | w.readyPart, err = io.ReadAll(getObjectOutput.Body) 908 | if err != nil { 909 | return 0, err 910 | } 911 | } else { 912 | // Otherwise we can use the old file as the new first part 913 | copyPartOutput, err := w.driver.Client.CopyPart(&obs.CopyPartInput{ 914 | Bucket: w.driver.Bucket, 915 | Key: w.key, 916 | CopySourceBucket: w.driver.Bucket, 917 | CopySourceKey: w.key, 918 | PartNumber: 1, 919 | UploadId: w.uploadID, 920 | SseHeader: w.driver.getEncryptionMode(), 921 | SourceSseHeader: w.driver.getEncryptionMode(), 922 | }) 923 | if err != nil { 924 | return 0, err 925 | } 926 | w.parts = []obs.Part{ 927 | { 928 | ETag: copyPartOutput.ETag, 929 | PartNumber: 1, 930 | Size: w.size, 931 | }, 932 | } 933 | } 934 | } 935 | for len(p) > 0 { 936 | // If no parts are ready to write, fill up the first part 937 | if neededBytes := int(w.driver.ChunkSize) - len(w.readyPart); neededBytes > 0 { 938 | if len(p) >= neededBytes { 939 | w.readyPart = append(w.readyPart, p[:neededBytes]...) 940 | n += neededBytes 941 | p = p[neededBytes:] 942 | } else { 943 | w.readyPart = append(w.readyPart, p...) 944 | n += len(p) 945 | p = nil 946 | } 947 | } 948 | 949 | if neededBytes := int(w.driver.ChunkSize) - len(w.pendingPart); neededBytes > 0 { 950 | if len(p) >= neededBytes { 951 | w.pendingPart = append(w.pendingPart, p[:neededBytes]...) 952 | n += neededBytes 953 | p = p[neededBytes:] 954 | err := w.flushPart() 955 | if err != nil { 956 | w.size += int64(n) 957 | return n, err 958 | } 959 | } else { 960 | w.pendingPart = append(w.pendingPart, p...) 961 | n += len(p) 962 | p = nil 963 | } 964 | } 965 | } 966 | w.size += int64(n) 967 | return n, nil 968 | } 969 | 970 | // flushPart flushes buffers to write a part to OBS. 971 | // Only called by Write (with both buffers full) and Close/Commit (always) 972 | func (w *writer) flushPart() error { 973 | if len(w.readyPart) == 0 && len(w.pendingPart) == 0 { 974 | // nothing to write 975 | return nil 976 | } 977 | if len(w.pendingPart) < int(w.driver.ChunkSize) { 978 | // closing with a small pending part 979 | // combine ready and pending to avoid writing a small part 980 | w.readyPart = append(w.readyPart, w.pendingPart...) 981 | w.pendingPart = nil 982 | } 983 | ouput, err := w.driver.Client.UploadPart(&obs.UploadPartInput{ 984 | Bucket: w.driver.Bucket, 985 | Key: w.key, 986 | PartNumber: len(w.parts) + 1, 987 | UploadId: w.uploadID, 988 | Body: bytes.NewReader(w.readyPart), 989 | SseHeader: w.driver.getEncryptionMode(), 990 | }) 991 | if err != nil { 992 | return err 993 | } 994 | w.parts = append(w.parts, obs.Part{ 995 | ETag: ouput.ETag, 996 | PartNumber: len(w.parts) + 1, 997 | Size: int64(len(w.readyPart)), 998 | }) 999 | w.readyPart = w.pendingPart 1000 | w.pendingPart = nil 1001 | return nil 1002 | } 1003 | 1004 | func (w *writer) Close() error { 1005 | if w.closed { 1006 | return fmt.Errorf("already closed") 1007 | } 1008 | w.closed = true 1009 | return w.flushPart() 1010 | } 1011 | 1012 | func (w *writer) Size() int64 { 1013 | return w.size 1014 | } 1015 | 1016 | func (w *writer) Cancel() error { 1017 | if w.closed { 1018 | return fmt.Errorf("already closed") 1019 | } else if w.committed { 1020 | return fmt.Errorf("already committed") 1021 | } 1022 | w.cancelled = true 1023 | _, err := w.driver.Client.AbortMultipartUpload(&obs.AbortMultipartUploadInput{ 1024 | Bucket: w.driver.Bucket, 1025 | Key: w.key, 1026 | UploadId: w.uploadID, 1027 | }) 1028 | return err 1029 | } 1030 | 1031 | func (w *writer) Commit() error { 1032 | if w.closed { 1033 | return fmt.Errorf("already closed") 1034 | } else if w.committed { 1035 | return fmt.Errorf("already committed") 1036 | } else if w.cancelled { 1037 | return fmt.Errorf("already cancelled") 1038 | } 1039 | err := w.flushPart() 1040 | if err != nil { 1041 | return err 1042 | } 1043 | w.committed = true 1044 | 1045 | var completedUploadedParts completedParts 1046 | for _, part := range w.parts { 1047 | completedUploadedParts = append(completedUploadedParts, obs.Part{ 1048 | ETag: part.ETag, 1049 | PartNumber: part.PartNumber, 1050 | }) 1051 | } 1052 | 1053 | sort.Sort(completedUploadedParts) 1054 | 1055 | _, err = w.driver.Client.CompleteMultipartUpload(&obs.CompleteMultipartUploadInput{ 1056 | Bucket: w.driver.Bucket, 1057 | Key: w.key, 1058 | UploadId: w.uploadID, 1059 | Parts: completedUploadedParts, 1060 | }) 1061 | if err != nil { 1062 | w.driver.Client.AbortMultipartUpload(&obs.AbortMultipartUploadInput{ 1063 | Bucket: w.driver.Bucket, 1064 | Key: w.key, 1065 | UploadId: w.uploadID, 1066 | }) 1067 | return err 1068 | } 1069 | return nil 1070 | } 1071 | -------------------------------------------------------------------------------- /storage/driver/oss/doc.go: -------------------------------------------------------------------------------- 1 | // Copy from github.com/docker/distribution/registry/storage/driver/oss 2 | 3 | package oss 4 | -------------------------------------------------------------------------------- /storage/driver/oss/oss.go: -------------------------------------------------------------------------------- 1 | // Package oss provides a storagedriver.StorageDriver implementation to 2 | // store blobs in Aliyun OSS cloud storage. 3 | // 4 | // This package leverages the denverdino/aliyungo client library for interfacing with 5 | // oss. 6 | // 7 | // Because OSS is a key, value store the Stat call does not support last modification 8 | // time for directories (directories are an abstraction for key, value stores) 9 | 10 | package oss 11 | 12 | import ( 13 | "bytes" 14 | "context" 15 | "errors" 16 | "fmt" 17 | "io" 18 | "io/ioutil" 19 | "net/http" 20 | "reflect" 21 | "strconv" 22 | "strings" 23 | "time" 24 | 25 | "github.com/denverdino/aliyungo/oss" 26 | storagedriver "github.com/docker/distribution/registry/storage/driver" 27 | "github.com/docker/distribution/registry/storage/driver/base" 28 | "github.com/docker/distribution/registry/storage/driver/factory" 29 | ) 30 | 31 | const driverName = "oss" 32 | 33 | // minChunkSize defines the minimum multipart upload chunk size 34 | // OSS API requires multipart upload chunks to be at least 5MB 35 | const minChunkSize = 5 << 20 36 | 37 | const defaultChunkSize = 2 * minChunkSize 38 | const defaultTimeout = 2 * time.Minute // 2 minute timeout per chunk 39 | 40 | // listMax is the largest amount of objects you can request from OSS in a list call 41 | const listMax = 1000 42 | 43 | // DriverParameters A struct that encapsulates all of the driver parameters after all values have been set 44 | type DriverParameters struct { 45 | AccessKeyID string 46 | AccessKeySecret string 47 | Bucket string 48 | Region oss.Region 49 | Internal bool 50 | Encrypt bool 51 | Secure bool 52 | ChunkSize int64 53 | RootDirectory string 54 | Endpoint string 55 | EncryptionKeyID string 56 | } 57 | 58 | func init() { 59 | factory.Register(driverName, &ossDriverFactory{}) 60 | } 61 | 62 | // ossDriverFactory implements the factory.StorageDriverFactory interface 63 | type ossDriverFactory struct{} 64 | 65 | func (factory *ossDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { 66 | return FromParameters(parameters) 67 | } 68 | 69 | type driver struct { 70 | Client *oss.Client 71 | Bucket *oss.Bucket 72 | ChunkSize int64 73 | Encrypt bool 74 | RootDirectory string 75 | EncryptionKeyID string 76 | } 77 | 78 | type baseEmbed struct { 79 | base.Base 80 | } 81 | 82 | // Driver is a storagedriver.StorageDriver implementation backed by Aliyun OSS 83 | // Objects are stored at absolute keys in the provided bucket. 84 | type Driver struct { 85 | baseEmbed 86 | } 87 | 88 | // FromParameters constructs a new Driver with a given parameters map 89 | // Required parameters: 90 | // - accesskey 91 | // - secretkey 92 | // - region 93 | // - bucket 94 | // - encrypt 95 | func FromParameters(parameters map[string]interface{}) (*Driver, error) { 96 | // Providing no values for these is valid in case the user is authenticating 97 | 98 | accessKey, ok := parameters["accesskeyid"] 99 | if !ok { 100 | return nil, fmt.Errorf("No accesskeyid parameter provided") 101 | } 102 | secretKey, ok := parameters["accesskeysecret"] 103 | if !ok { 104 | return nil, fmt.Errorf("No accesskeysecret parameter provided") 105 | } 106 | 107 | regionName, ok := parameters["region"] 108 | if !ok || fmt.Sprint(regionName) == "" { 109 | return nil, fmt.Errorf("No region parameter provided") 110 | } 111 | 112 | bucket, ok := parameters["bucket"] 113 | if !ok || fmt.Sprint(bucket) == "" { 114 | return nil, fmt.Errorf("No bucket parameter provided") 115 | } 116 | 117 | internalBool := false 118 | internal, ok := parameters["internal"] 119 | if ok { 120 | switch b := internal.(type) { 121 | case bool: 122 | internalBool = b 123 | case string: 124 | ib, err := strconv.ParseBool(b) 125 | if err != nil { 126 | return nil, err 127 | } 128 | internalBool = ib 129 | default: 130 | return nil, fmt.Errorf("The internal parameter should be a boolean") 131 | } 132 | } 133 | 134 | encryptBool := false 135 | encrypt, ok := parameters["encrypt"] 136 | if ok { 137 | switch b := encrypt.(type) { 138 | case bool: 139 | encryptBool = b 140 | case string: 141 | ib, err := strconv.ParseBool(b) 142 | if err != nil { 143 | return nil, err 144 | } 145 | encryptBool = ib 146 | default: 147 | return nil, fmt.Errorf("The encrypt parameter should be a boolean") 148 | } 149 | } 150 | 151 | encryptionKeyID, ok := parameters["encryptionkeyid"] 152 | if !ok { 153 | encryptionKeyID = "" 154 | } 155 | 156 | secureBool := true 157 | secure, ok := parameters["secure"] 158 | if ok { 159 | switch b := secure.(type) { 160 | case bool: 161 | secureBool = b 162 | case string: 163 | ib, err := strconv.ParseBool(b) 164 | if err != nil { 165 | return nil, err 166 | } 167 | secureBool = ib 168 | default: 169 | return nil, fmt.Errorf("The secure parameter should be a boolean") 170 | } 171 | } 172 | 173 | chunkSize := int64(defaultChunkSize) 174 | chunkSizeParam, ok := parameters["chunksize"] 175 | if ok { 176 | switch v := chunkSizeParam.(type) { 177 | case string: 178 | vv, err := strconv.ParseInt(v, 0, 64) 179 | if err != nil { 180 | return nil, fmt.Errorf("chunksize parameter must be an integer, %v invalid", chunkSizeParam) 181 | } 182 | chunkSize = vv 183 | case int64: 184 | chunkSize = v 185 | case int, uint, int32, uint32, uint64: 186 | chunkSize = reflect.ValueOf(v).Convert(reflect.TypeOf(chunkSize)).Int() 187 | default: 188 | return nil, fmt.Errorf("invalid valud for chunksize: %#v", chunkSizeParam) 189 | } 190 | 191 | if chunkSize < minChunkSize { 192 | return nil, fmt.Errorf("The chunksize %#v parameter should be a number that is larger than or equal to %d", chunkSize, minChunkSize) 193 | } 194 | } 195 | 196 | rootDirectory, ok := parameters["rootdirectory"] 197 | if !ok { 198 | rootDirectory = "" 199 | } 200 | 201 | endpoint, ok := parameters["endpoint"] 202 | if !ok { 203 | endpoint = "" 204 | } 205 | 206 | params := DriverParameters{ 207 | AccessKeyID: fmt.Sprint(accessKey), 208 | AccessKeySecret: fmt.Sprint(secretKey), 209 | Bucket: fmt.Sprint(bucket), 210 | Region: oss.Region(fmt.Sprint(regionName)), 211 | ChunkSize: chunkSize, 212 | RootDirectory: fmt.Sprint(rootDirectory), 213 | Encrypt: encryptBool, 214 | Secure: secureBool, 215 | Internal: internalBool, 216 | Endpoint: fmt.Sprint(endpoint), 217 | EncryptionKeyID: fmt.Sprint(encryptionKeyID), 218 | } 219 | 220 | return New(params) 221 | } 222 | 223 | // New constructs a new Driver with the given Aliyun credentials, region, encryption flag, and 224 | // bucketName 225 | func New(params DriverParameters) (*Driver, error) { 226 | 227 | client := oss.NewOSSClient(params.Region, params.Internal, params.AccessKeyID, params.AccessKeySecret, params.Secure) 228 | client.SetEndpoint(params.Endpoint) 229 | bucket := client.Bucket(params.Bucket) 230 | client.SetDebug(false) 231 | 232 | // Validate that the given credentials have at least read permissions in the 233 | // given bucket scope. 234 | if _, err := bucket.List(strings.TrimRight(params.RootDirectory, "/"), "", "", 1); err != nil { 235 | return nil, err 236 | } 237 | 238 | // TODO(tg123): Currently multipart uploads have no timestamps, so this would be unwise 239 | // if you initiated a new OSS client while another one is running on the same bucket. 240 | 241 | d := &driver{ 242 | Client: client, 243 | Bucket: bucket, 244 | ChunkSize: params.ChunkSize, 245 | Encrypt: params.Encrypt, 246 | RootDirectory: params.RootDirectory, 247 | EncryptionKeyID: params.EncryptionKeyID, 248 | } 249 | 250 | return &Driver{ 251 | baseEmbed: baseEmbed{ 252 | Base: base.Base{ 253 | StorageDriver: d, 254 | }, 255 | }, 256 | }, nil 257 | } 258 | 259 | // Implement the storagedriver.StorageDriver interface 260 | 261 | func (d *driver) Name() string { 262 | return driverName 263 | } 264 | 265 | // GetContent retrieves the content stored at "path" as a []byte. 266 | func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { 267 | content, err := d.Bucket.Get(d.ossPath(path)) 268 | if err != nil { 269 | return nil, parseError(path, err) 270 | } 271 | return content, nil 272 | } 273 | 274 | // PutContent stores the []byte content at a location designated by "path". 275 | func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { 276 | return parseError(path, d.Bucket.Put(d.ossPath(path), contents, d.getContentType(), getPermissions(), d.getOptions())) 277 | } 278 | 279 | // Reader retrieves an io.ReadCloser for the content stored at "path" with a 280 | // given byte offset. 281 | func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { 282 | headers := make(http.Header) 283 | headers.Add("Range", "bytes="+strconv.FormatInt(offset, 10)+"-") 284 | 285 | resp, err := d.Bucket.GetResponseWithHeaders(d.ossPath(path), headers) 286 | if err != nil { 287 | return nil, parseError(path, err) 288 | } 289 | 290 | // Due to Aliyun OSS API, status 200 and whole object will be return instead of an 291 | // InvalidRange error when range is invalid. 292 | // 293 | // OSS sever will always return http.StatusPartialContent if range is acceptable. 294 | if resp.StatusCode != http.StatusPartialContent { 295 | resp.Body.Close() 296 | return ioutil.NopCloser(bytes.NewReader(nil)), nil 297 | } 298 | 299 | return resp.Body, nil 300 | } 301 | 302 | // Writer returns a FileWriter which will store the content written to it 303 | // at the location designated by "path" after the call to Commit. 304 | func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { 305 | key := d.ossPath(path) 306 | if !append { 307 | // TODO (brianbland): cancel other uploads at this path 308 | multi, err := d.Bucket.InitMulti(key, d.getContentType(), getPermissions(), d.getOptions()) 309 | if err != nil { 310 | return nil, err 311 | } 312 | return d.newWriter(key, multi, nil), nil 313 | } 314 | multis, _, err := d.Bucket.ListMulti(key, "") 315 | if err != nil { 316 | return nil, parseError(path, err) 317 | } 318 | for _, multi := range multis { 319 | if key != multi.Key { 320 | continue 321 | } 322 | parts, err := multi.ListParts() 323 | if err != nil { 324 | return nil, parseError(path, err) 325 | } 326 | var multiSize int64 327 | for _, part := range parts { 328 | multiSize += part.Size 329 | } 330 | return d.newWriter(key, multi, parts), nil 331 | } 332 | return nil, storagedriver.PathNotFoundError{Path: path} 333 | } 334 | 335 | // Stat retrieves the FileInfo for the given path, including the current size 336 | // in bytes and the creation time. 337 | func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { 338 | listResponse, err := d.Bucket.List(d.ossPath(path), "", "", 1) 339 | if err != nil { 340 | return nil, err 341 | } 342 | 343 | fi := storagedriver.FileInfoFields{ 344 | Path: path, 345 | } 346 | 347 | if len(listResponse.Contents) == 1 { 348 | if listResponse.Contents[0].Key != d.ossPath(path) { 349 | fi.IsDir = true 350 | } else { 351 | fi.IsDir = false 352 | fi.Size = listResponse.Contents[0].Size 353 | 354 | timestamp, err := time.Parse(time.RFC3339Nano, listResponse.Contents[0].LastModified) 355 | if err != nil { 356 | return nil, err 357 | } 358 | fi.ModTime = timestamp 359 | } 360 | } else if len(listResponse.CommonPrefixes) == 1 { 361 | fi.IsDir = true 362 | } else { 363 | return nil, storagedriver.PathNotFoundError{Path: path} 364 | } 365 | 366 | return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil 367 | } 368 | 369 | // List returns a list of the objects that are direct descendants of the given path. 370 | func (d *driver) List(ctx context.Context, opath string) ([]string, error) { 371 | path := opath 372 | if path != "/" && opath[len(path)-1] != '/' { 373 | path = path + "/" 374 | } 375 | 376 | // This is to cover for the cases when the rootDirectory of the driver is either "" or "/". 377 | // In those cases, there is no root prefix to replace and we must actually add a "/" to all 378 | // results in order to keep them as valid paths as recognized by storagedriver.PathRegexp 379 | prefix := "" 380 | if d.ossPath("") == "" { 381 | prefix = "/" 382 | } 383 | 384 | ossPath := d.ossPath(path) 385 | listResponse, err := d.Bucket.List(ossPath, "/", "", listMax) 386 | if err != nil { 387 | return nil, parseError(opath, err) 388 | } 389 | 390 | files := []string{} 391 | directories := []string{} 392 | 393 | for { 394 | for _, key := range listResponse.Contents { 395 | files = append(files, strings.Replace(key.Key, d.ossPath(""), prefix, 1)) 396 | } 397 | 398 | for _, commonPrefix := range listResponse.CommonPrefixes { 399 | directories = append(directories, strings.Replace(commonPrefix[0:len(commonPrefix)-1], d.ossPath(""), prefix, 1)) 400 | } 401 | 402 | if listResponse.IsTruncated { 403 | listResponse, err = d.Bucket.List(ossPath, "/", listResponse.NextMarker, listMax) 404 | if err != nil { 405 | return nil, err 406 | } 407 | } else { 408 | break 409 | } 410 | } 411 | 412 | // This is to cover for the cases when the first key equal to ossPath. 413 | if len(files) > 0 && files[0] == strings.Replace(ossPath, d.ossPath(""), prefix, 1) { 414 | files = files[1:] 415 | } 416 | 417 | if opath != "/" { 418 | if len(files) == 0 && len(directories) == 0 { 419 | // Treat empty response as missing directory, since we don't actually 420 | // have directories in s3. 421 | return nil, storagedriver.PathNotFoundError{Path: opath} 422 | } 423 | } 424 | 425 | return append(files, directories...), nil 426 | } 427 | 428 | const maxConcurrency = 10 429 | 430 | // Move moves an object stored at sourcePath to destPath, removing the original 431 | // object. 432 | func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { 433 | err := d.Bucket.CopyLargeFileInParallel(d.ossPath(sourcePath), d.ossPath(destPath), 434 | d.getContentType(), 435 | getPermissions(), 436 | d.getOptions(), 437 | maxConcurrency) 438 | if err != nil { 439 | return parseError(sourcePath, err) 440 | } 441 | 442 | return d.Delete(ctx, sourcePath) 443 | } 444 | 445 | // Delete recursively deletes all objects stored at "path" and its subpaths. 446 | func (d *driver) Delete(ctx context.Context, path string) error { 447 | ossPath := d.ossPath(path) 448 | listResponse, err := d.Bucket.List(ossPath, "", "", listMax) 449 | if err != nil || len(listResponse.Contents) == 0 { 450 | return storagedriver.PathNotFoundError{Path: path} 451 | } 452 | 453 | ossObjects := make([]oss.Object, listMax) 454 | 455 | for len(listResponse.Contents) > 0 { 456 | numOssObjects := len(listResponse.Contents) 457 | for index, key := range listResponse.Contents { 458 | // Stop if we encounter a key that is not a subpath (so that deleting "/a" does not delete "/ab"). 459 | if len(key.Key) > len(ossPath) && (key.Key)[len(ossPath)] != '/' { 460 | numOssObjects = index 461 | break 462 | } 463 | ossObjects[index].Key = key.Key 464 | } 465 | 466 | err := d.Bucket.DelMulti(oss.Delete{Quiet: false, Objects: ossObjects[0:numOssObjects]}) 467 | if err != nil { 468 | return nil 469 | } 470 | 471 | if numOssObjects < len(listResponse.Contents) { 472 | return nil 473 | } 474 | 475 | listResponse, err = d.Bucket.List(d.ossPath(path), "", "", listMax) 476 | if err != nil { 477 | return err 478 | } 479 | } 480 | 481 | return nil 482 | } 483 | 484 | // URLFor returns a URL which may be used to retrieve the content stored at the given path. 485 | // May return an UnsupportedMethodErr in certain StorageDriver implementations. 486 | func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { 487 | methodString := "GET" 488 | method, ok := options["method"] 489 | if ok { 490 | methodString, ok = method.(string) 491 | if !ok || (methodString != "GET") { 492 | return "", storagedriver.ErrUnsupportedMethod{} 493 | } 494 | } 495 | 496 | expiresTime := time.Now().Add(20 * time.Minute) 497 | 498 | expires, ok := options["expiry"] 499 | if ok { 500 | et, ok := expires.(time.Time) 501 | if ok { 502 | expiresTime = et 503 | } 504 | } 505 | signedURL := d.Bucket.SignedURLWithMethod(methodString, d.ossPath(path), expiresTime, nil, nil) 506 | return signedURL, nil 507 | } 508 | 509 | // Walk traverses a filesystem defined within driver, starting 510 | // from the given path, calling f on each file 511 | func (d *driver) Walk(ctx context.Context, path string, f storagedriver.WalkFn) error { 512 | return storagedriver.WalkFallback(ctx, d, path, f) 513 | } 514 | 515 | func (d *driver) ossPath(path string) string { 516 | return strings.TrimLeft(strings.TrimRight(d.RootDirectory, "/")+path, "/") 517 | } 518 | 519 | func parseError(path string, err error) error { 520 | var ossErr *oss.Error 521 | if errors.As(err, &ossErr) && ossErr.StatusCode == http.StatusNotFound && (ossErr.Code == "NoSuchKey" || ossErr.Code == "") { 522 | return storagedriver.PathNotFoundError{Path: path} 523 | } 524 | 525 | return err 526 | } 527 | 528 | func (d *driver) getOptions() oss.Options { 529 | return oss.Options{ 530 | ServerSideEncryption: d.Encrypt, 531 | ServerSideEncryptionKeyID: d.EncryptionKeyID, 532 | } 533 | } 534 | 535 | func (d *driver) getCopyOptions() oss.CopyOptions { 536 | return oss.CopyOptions{ 537 | ServerSideEncryption: d.Encrypt, 538 | ServerSideEncryptionKeyID: d.EncryptionKeyID, 539 | } 540 | } 541 | 542 | func getPermissions() oss.ACL { 543 | return oss.Private 544 | } 545 | 546 | func (d *driver) getContentType() string { 547 | return "application/octet-stream" 548 | } 549 | 550 | // writer attempts to upload parts to S3 in a buffered fashion where the last 551 | // part is at least as large as the chunksize, so the multipart upload could be 552 | // cleanly resumed in the future. This is violated if Close is called after less 553 | // than a full chunk is written. 554 | type writer struct { 555 | driver *driver 556 | key string 557 | multi *oss.Multi 558 | parts []oss.Part 559 | size int64 560 | readyPart []byte 561 | pendingPart []byte 562 | closed bool 563 | committed bool 564 | cancelled bool 565 | } 566 | 567 | func (d *driver) newWriter(key string, multi *oss.Multi, parts []oss.Part) storagedriver.FileWriter { 568 | var size int64 569 | for _, part := range parts { 570 | size += part.Size 571 | } 572 | return &writer{ 573 | driver: d, 574 | key: key, 575 | multi: multi, 576 | parts: parts, 577 | size: size, 578 | } 579 | } 580 | 581 | func (w *writer) Write(p []byte) (int, error) { 582 | if w.closed { 583 | return 0, fmt.Errorf("already closed") 584 | } else if w.committed { 585 | return 0, fmt.Errorf("already committed") 586 | } else if w.cancelled { 587 | return 0, fmt.Errorf("already cancelled") 588 | } 589 | 590 | // If the last written part is smaller than minChunkSize, we need to make a 591 | // new multipart upload :sadface: 592 | if len(w.parts) > 0 && int(w.parts[len(w.parts)-1].Size) < minChunkSize { 593 | err := w.multi.Complete(w.parts) 594 | if err != nil { 595 | w.multi.Abort() 596 | return 0, err 597 | } 598 | 599 | multi, err := w.driver.Bucket.InitMulti(w.key, w.driver.getContentType(), getPermissions(), w.driver.getOptions()) 600 | if err != nil { 601 | return 0, err 602 | } 603 | w.multi = multi 604 | 605 | // If the entire written file is smaller than minChunkSize, we need to make 606 | // a new part from scratch :double sad face: 607 | if w.size < minChunkSize { 608 | contents, err := w.driver.Bucket.Get(w.key) 609 | if err != nil { 610 | return 0, err 611 | } 612 | w.parts = nil 613 | w.readyPart = contents 614 | } else { 615 | // Otherwise we can use the old file as the new first part 616 | _, part, err := multi.PutPartCopy(1, w.driver.getCopyOptions(), w.driver.Bucket.Name+"/"+w.key) 617 | if err != nil { 618 | return 0, err 619 | } 620 | w.parts = []oss.Part{part} 621 | } 622 | } 623 | 624 | var n int 625 | 626 | for len(p) > 0 { 627 | // If no parts are ready to write, fill up the first part 628 | if neededBytes := int(w.driver.ChunkSize) - len(w.readyPart); neededBytes > 0 { 629 | if len(p) >= neededBytes { 630 | w.readyPart = append(w.readyPart, p[:neededBytes]...) 631 | n += neededBytes 632 | p = p[neededBytes:] 633 | } else { 634 | w.readyPart = append(w.readyPart, p...) 635 | n += len(p) 636 | p = nil 637 | } 638 | } 639 | 640 | if neededBytes := int(w.driver.ChunkSize) - len(w.pendingPart); neededBytes > 0 { 641 | if len(p) >= neededBytes { 642 | w.pendingPart = append(w.pendingPart, p[:neededBytes]...) 643 | n += neededBytes 644 | p = p[neededBytes:] 645 | err := w.flushPart() 646 | if err != nil { 647 | w.size += int64(n) 648 | return n, err 649 | } 650 | } else { 651 | w.pendingPart = append(w.pendingPart, p...) 652 | n += len(p) 653 | p = nil 654 | } 655 | } 656 | } 657 | w.size += int64(n) 658 | return n, nil 659 | } 660 | 661 | func (w *writer) Size() int64 { 662 | return w.size 663 | } 664 | 665 | func (w *writer) Close() error { 666 | if w.closed { 667 | return fmt.Errorf("already closed") 668 | } 669 | w.closed = true 670 | return w.flushPart() 671 | } 672 | 673 | func (w *writer) Cancel() error { 674 | if w.closed { 675 | return fmt.Errorf("already closed") 676 | } else if w.committed { 677 | return fmt.Errorf("already committed") 678 | } 679 | w.cancelled = true 680 | err := w.multi.Abort() 681 | return err 682 | } 683 | 684 | func (w *writer) Commit() error { 685 | if w.closed { 686 | return fmt.Errorf("already closed") 687 | } else if w.committed { 688 | return fmt.Errorf("already committed") 689 | } else if w.cancelled { 690 | return fmt.Errorf("already cancelled") 691 | } 692 | err := w.flushPart() 693 | if err != nil { 694 | return err 695 | } 696 | w.committed = true 697 | err = w.multi.Complete(w.parts) 698 | if err != nil { 699 | w.multi.Abort() 700 | return err 701 | } 702 | return nil 703 | } 704 | 705 | // flushPart flushes buffers to write a part to S3. 706 | // Only called by Write (with both buffers full) and Close/Commit (always) 707 | func (w *writer) flushPart() error { 708 | if len(w.readyPart) == 0 && len(w.pendingPart) == 0 { 709 | // nothing to write 710 | return nil 711 | } 712 | if len(w.pendingPart) < int(w.driver.ChunkSize) { 713 | // closing with a small pending part 714 | // combine ready and pending to avoid writing a small part 715 | w.readyPart = append(w.readyPart, w.pendingPart...) 716 | w.pendingPart = nil 717 | } 718 | 719 | part, err := w.multi.PutPart(len(w.parts)+1, bytes.NewReader(w.readyPart)) 720 | if err != nil { 721 | return err 722 | } 723 | w.parts = append(w.parts, part) 724 | w.readyPart = w.pendingPart 725 | w.pendingPart = nil 726 | return nil 727 | } 728 | -------------------------------------------------------------------------------- /sync.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | 12 | "github.com/distribution/reference" 13 | "github.com/docker/distribution" 14 | "github.com/docker/distribution/manifest/manifestlist" 15 | "github.com/docker/distribution/manifest/ocischema" 16 | "github.com/docker/distribution/manifest/schema1" 17 | "github.com/docker/distribution/manifest/schema2" 18 | "github.com/docker/distribution/registry/api/errcode" 19 | "github.com/docker/distribution/registry/client" 20 | "github.com/opencontainers/go-digest" 21 | ) 22 | 23 | type namedWithoutDomain struct { 24 | reference.Reference 25 | name string 26 | } 27 | 28 | func (n namedWithoutDomain) Name() string { 29 | return n.name 30 | } 31 | 32 | func newNameWithoutDomain(named reference.Named, name string) reference.Named { 33 | return namedWithoutDomain{ 34 | Reference: named, 35 | name: name, 36 | } 37 | } 38 | 39 | func (c *CRProxy) Sync(rw http.ResponseWriter, r *http.Request) { 40 | if r.Method != http.MethodPut || c.storageDriver == nil { 41 | errcode.ServeJSON(rw, errcode.ErrorCodeUnsupported) 42 | return 43 | } 44 | 45 | query := r.URL.Query() 46 | 47 | rw.Header().Set("Content-Type", "application/json") 48 | 49 | images := query["image"] 50 | 51 | flusher, _ := rw.(http.Flusher) 52 | 53 | encoder := json.NewEncoder(rw) 54 | for _, image := range images { 55 | if image == "" { 56 | continue 57 | } 58 | err := c.SyncImageLayer(r.Context(), r.RemoteAddr, image, nil, func(sp SyncProgress) error { 59 | err := encoder.Encode(sp) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if flusher != nil { 65 | flusher.Flush() 66 | } 67 | 68 | return nil 69 | }) 70 | if err != nil { 71 | c.errorResponse(rw, r, err) 72 | return 73 | } 74 | } 75 | } 76 | 77 | type SyncProgress struct { 78 | Digest string `json:"digest,omitempty"` 79 | Size int64 `json:"size,omitempty"` 80 | Status string `json:"status,omitempty"` 81 | Platform *manifestlist.PlatformSpec `json:"platform,omitempty"` 82 | } 83 | 84 | func (c *CRProxy) SyncImageLayer(ctx context.Context, ip, image string, filter func(pf manifestlist.PlatformSpec) bool, cb func(sp SyncProgress) error) error { 85 | ref, err := reference.Parse(image) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | named, ok := ref.(reference.Named) 91 | if !ok { 92 | return fmt.Errorf("%s is not a name", ref) 93 | } 94 | 95 | host := reference.Domain(named) 96 | 97 | var name reference.Named 98 | 99 | info := &ImageInfo{ 100 | Host: host, 101 | Name: reference.Path(named), 102 | } 103 | if c.modify != nil { 104 | info = c.modify(info) 105 | name = newNameWithoutDomain(named, info.Name) 106 | } else { 107 | name = newNameWithoutDomain(named, info.Name) 108 | } 109 | 110 | if c.blockFunc != nil { 111 | blockMessage, block := c.block(&BlockInfo{ 112 | IP: ip, 113 | Host: info.Host, 114 | Name: info.Name, 115 | }) 116 | if block { 117 | if blockMessage != "" { 118 | return errcode.ErrorCodeDenied.WithMessage(blockMessage) 119 | } else { 120 | return errcode.ErrorCodeDenied 121 | } 122 | } 123 | } 124 | 125 | host = c.getDomainAlias(host) 126 | info.Host = host 127 | 128 | err = c.ping(host) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | cli := c.getClientset(host, name.Name()) 134 | 135 | repo, err := client.NewRepository(name, c.hostURL(host), cli.Transport) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | ms, err := repo.Manifests(ctx) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | bs := repo.Blobs(ctx) 146 | 147 | buf := c.bytesPool.Get().([]byte) 148 | defer c.bytesPool.Put(buf) 149 | 150 | uniq := map[digest.Digest]struct{}{} 151 | blobCallback := func(dgst digest.Digest, size int64, pf *manifestlist.PlatformSpec) error { 152 | _, ok := uniq[dgst] 153 | if ok { 154 | if cb != nil { 155 | err = cb(SyncProgress{ 156 | Digest: dgst.String(), 157 | Size: size, 158 | Status: "SKIP", 159 | Platform: pf, 160 | }) 161 | if err != nil { 162 | return err 163 | } 164 | } 165 | return nil 166 | } 167 | uniq[dgst] = struct{}{} 168 | 169 | blobPath := blobCachePath(dgst.String()) 170 | stat, err := c.storageDriver.Stat(ctx, blobPath) 171 | if err == nil { 172 | if size > 0 { 173 | gotSize := stat.Size() 174 | if size == gotSize { 175 | if c.logger != nil { 176 | c.logger.Println("skip blob", dgst) 177 | } 178 | 179 | if cb != nil { 180 | err = cb(SyncProgress{ 181 | Digest: dgst.String(), 182 | Size: size, 183 | Status: "SKIP", 184 | Platform: pf, 185 | }) 186 | if err != nil { 187 | return err 188 | } 189 | } 190 | return nil 191 | } 192 | if c.logger != nil { 193 | c.logger.Println("size is not meeting expectations", dgst, size, gotSize) 194 | } 195 | } else { 196 | if c.logger != nil { 197 | c.logger.Println("skip blob", dgst) 198 | } 199 | if cb != nil { 200 | err = cb(SyncProgress{ 201 | Digest: dgst.String(), 202 | Size: -1, 203 | Status: "SKIP", 204 | Platform: pf, 205 | }) 206 | if err != nil { 207 | return err 208 | } 209 | } 210 | return nil 211 | } 212 | } 213 | 214 | f, err := bs.Open(ctx, dgst) 215 | if err != nil { 216 | return err 217 | } 218 | defer f.Close() 219 | 220 | fw, err := c.storageDriver.Writer(ctx, blobPath, false) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | h := sha256.New() 226 | n, err := io.CopyBuffer(fw, io.TeeReader(f, h), buf) 227 | if err != nil { 228 | fw.Cancel() 229 | return err 230 | } 231 | if wantSize := fw.Size(); n != wantSize { 232 | fw.Cancel() 233 | return fmt.Errorf("expected %d bytes, got %d", wantSize, n) 234 | } 235 | 236 | hash := hex.EncodeToString(h.Sum(nil)[:]) 237 | if hex := dgst.Hex(); hex != hash { 238 | fw.Cancel() 239 | return fmt.Errorf("expected %s hash, got %s", hex, hash) 240 | } 241 | 242 | err = fw.Commit() 243 | if err != nil { 244 | return err 245 | } 246 | 247 | if c.logger != nil { 248 | c.logger.Println("sync blob", dgst) 249 | } 250 | 251 | if cb != nil { 252 | err = cb(SyncProgress{ 253 | Digest: dgst.String(), 254 | Size: n, 255 | Status: "CACHE", 256 | Platform: pf, 257 | }) 258 | if err != nil { 259 | return err 260 | } 261 | } 262 | return nil 263 | } 264 | 265 | manifestCallback := func(tagOrHash string, m distribution.Manifest) error { 266 | _, playload, err := m.Payload() 267 | if err != nil { 268 | return err 269 | } 270 | return c.cacheManifestContent(ctx, &PathInfo{ 271 | Host: info.Host, 272 | Image: info.Name, 273 | Manifests: tagOrHash, 274 | }, playload) 275 | } 276 | 277 | err = getLayerFromManifestList(ctx, ms, ref, filter, blobCallback, manifestCallback) 278 | if err != nil { 279 | return err 280 | } 281 | return nil 282 | } 283 | 284 | func getLayerFromManifestList(ctx context.Context, ms distribution.ManifestService, ref reference.Reference, filter func(pf manifestlist.PlatformSpec) bool, 285 | digestCallback func(dgst digest.Digest, size int64, pf *manifestlist.PlatformSpec) error, 286 | manifestCallback func(tagOrHash string, m distribution.Manifest) error) error { 287 | var ( 288 | m distribution.Manifest 289 | err error 290 | ) 291 | switch r := ref.(type) { 292 | case reference.Digested: 293 | m, err = ms.Get(ctx, r.Digest()) 294 | if err != nil { 295 | return err 296 | } 297 | err = manifestCallback(r.Digest().String(), m) 298 | if err != nil { 299 | return err 300 | } 301 | case reference.Tagged: 302 | tag := r.Tag() 303 | m, err = ms.Get(ctx, "", distribution.WithTag(r.Tag())) 304 | if err != nil { 305 | return err 306 | } 307 | err = manifestCallback(tag, m) 308 | if err != nil { 309 | return err 310 | } 311 | default: 312 | return fmt.Errorf("%s no reference to any source", ref) 313 | } 314 | 315 | switch m := m.(type) { 316 | case *manifestlist.DeserializedManifestList: 317 | for _, mfest := range m.ManifestList.Manifests { 318 | if filter != nil && !filter(mfest.Platform) { 319 | continue 320 | } 321 | 322 | m0, err := ms.Get(ctx, mfest.Digest) 323 | if err != nil { 324 | return err 325 | } 326 | err = manifestCallback(mfest.Digest.String(), m0) 327 | if err != nil { 328 | return err 329 | } 330 | err = getLayerFromManifest(m0, func(dgst digest.Digest, size int64) error { 331 | return digestCallback(dgst, size, &mfest.Platform) 332 | }) 333 | if err != nil { 334 | return err 335 | } 336 | } 337 | return nil 338 | default: 339 | return getLayerFromManifest(m, func(dgst digest.Digest, size int64) error { 340 | return digestCallback(dgst, size, nil) 341 | }) 342 | } 343 | } 344 | 345 | func getLayerFromManifest(m distribution.Manifest, cb func(dgst digest.Digest, size int64) error) error { 346 | switch m := m.(type) { 347 | case *ocischema.DeserializedManifest: 348 | for _, layer := range m.Layers { 349 | if layer.Size == 0 { 350 | continue 351 | } 352 | err := cb(layer.Digest, layer.Size) 353 | if err != nil { 354 | return err 355 | } 356 | } 357 | case *schema2.DeserializedManifest: 358 | for _, layer := range m.Layers { 359 | if layer.Size == 0 { 360 | continue 361 | } 362 | err := cb(layer.Digest, layer.Size) 363 | if err != nil { 364 | return err 365 | } 366 | } 367 | case *schema1.SignedManifest: 368 | for _, layer := range m.FSLayers { 369 | err := cb(layer.BlobSum, -1) 370 | if err != nil { 371 | return err 372 | } 373 | } 374 | } 375 | return nil 376 | } 377 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func addPrefixToImageForPagination(oldLink string, host string) string { 11 | linkAndRel := strings.SplitN(oldLink, ";", 2) 12 | if len(linkAndRel) != 2 { 13 | return oldLink 14 | } 15 | linkURL := strings.SplitN(strings.Trim(linkAndRel[0], "<>"), "/v2/", 2) 16 | if len(linkURL) != 2 { 17 | return oldLink 18 | } 19 | mirrorPath := prefix + host + "/" + linkURL[1] 20 | return fmt.Sprintf("<%s>;%s", mirrorPath, linkAndRel[1]) 21 | } 22 | 23 | type PathInfo struct { 24 | Host string 25 | Image string 26 | 27 | TagsList bool 28 | Manifests string 29 | Blobs string 30 | } 31 | 32 | func (p PathInfo) Path() (string, error) { 33 | if p.TagsList { 34 | return prefix + p.Image + "/tags/list", nil 35 | } 36 | if p.Manifests != "" { 37 | return prefix + p.Image + "/manifests/" + p.Manifests, nil 38 | } 39 | if p.Blobs != "" { 40 | return prefix + p.Image + "/blobs/" + p.Blobs, nil 41 | } 42 | return "", fmt.Errorf("unknow kind %#v", p) 43 | } 44 | 45 | func ParseOriginPathInfo(path string, defaultRegistry string) (*PathInfo, bool) { 46 | path = strings.TrimPrefix(path, prefix) 47 | i := strings.IndexByte(path, '/') 48 | if i <= 0 { 49 | return nil, false 50 | } 51 | host := path[:i] 52 | tail := path[i+1:] 53 | 54 | var tails = []string{} 55 | var image = "" 56 | 57 | if !isDomainName(host) || !strings.Contains(host, ".") { 58 | // disable while non default registry seted. 59 | if defaultRegistry == "" { 60 | return nil, false 61 | } 62 | // if host is not a domain name, it is a image. 63 | tails = strings.Split(tail, "/") 64 | if len(tails) < 2 { 65 | // should be more then 2 parts. like /manifests/latest 66 | return nil, false 67 | } 68 | image = strings.Join(tails[:len(tails)-2], "/") 69 | if image == "" { 70 | // the url looks like /v2/[busybox]/manifests/latest. 71 | image = host 72 | } else { 73 | // the url looks like /v2/[pytorch/pytorch/...]/[manifests/latest]. 74 | image = host + "/" + image 75 | } 76 | host = defaultRegistry 77 | } else { 78 | 79 | tails = strings.Split(tail, "/") 80 | if len(tails) < 3 { 81 | return nil, false 82 | } 83 | image = strings.Join(tails[:len(tails)-2], "/") 84 | if image == "" { 85 | return nil, false 86 | } 87 | } 88 | 89 | info := &PathInfo{ 90 | Host: host, 91 | Image: image, 92 | } 93 | switch tails[len(tails)-2] { 94 | case "tags": 95 | info.TagsList = tails[len(tails)-1] == "list" 96 | case "manifests": 97 | info.Manifests = tails[len(tails)-1] 98 | case "blobs": 99 | info.Blobs = tails[len(tails)-1] 100 | if len(info.Blobs) != 7+64 { 101 | return nil, false 102 | } 103 | } 104 | return info, true 105 | } 106 | 107 | // isDomainName checks if a string is a presentation-format domain name 108 | // (currently restricted to hostname-compatible "preferred name" LDH labels and 109 | // SRV-like "underscore labels"; see golang.org/issue/12421). 110 | func isDomainName(s string) bool { 111 | // See RFC 1035, RFC 3696. 112 | // Presentation format has dots before every label except the first, and the 113 | // terminal empty label is optional here because we assume fully-qualified 114 | // (absolute) input. We must therefore reserve space for the first and last 115 | // labels' length octets in wire format, where they are necessary and the 116 | // maximum total length is 255. 117 | // So our _effective_ maximum is 253, but 254 is not rejected if the last 118 | // character is a dot. 119 | l := len(s) 120 | if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { 121 | return false 122 | } 123 | 124 | last := byte('.') 125 | nonNumeric := false // true once we've seen a letter or hyphen 126 | partlen := 0 127 | for i := 0; i < len(s); i++ { 128 | c := s[i] 129 | switch { 130 | default: 131 | return false 132 | case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': 133 | nonNumeric = true 134 | partlen++ 135 | case '0' <= c && c <= '9': 136 | // fine 137 | partlen++ 138 | case c == '-': 139 | // Byte before dash cannot be dot. 140 | if last == '.' { 141 | return false 142 | } 143 | partlen++ 144 | nonNumeric = true 145 | case c == '.': 146 | // Byte before dot cannot be dot, dash. 147 | if last == '.' || last == '-' { 148 | return false 149 | } 150 | if partlen > 63 || partlen == 0 { 151 | return false 152 | } 153 | partlen = 0 154 | } 155 | last = c 156 | } 157 | if last == '-' || partlen > 63 { 158 | return false 159 | } 160 | 161 | return nonNumeric 162 | } 163 | 164 | func dumpResponse(resp *http.Response) string { 165 | body, _ := io.ReadAll(io.LimitReader(resp.Body, 100)) 166 | return fmt.Sprintf("%d %d %q", resp.StatusCode, resp.ContentLength, string(body)) 167 | } 168 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package crproxy 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseOriginPathInfo(t *testing.T) { 9 | 10 | testDefaultRegistry := "non_docker.io" 11 | 12 | type args struct { 13 | path string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | defaultRegistry string 19 | want *PathInfo 20 | wantOk bool 21 | }{ 22 | { 23 | args: args{ 24 | path: "/v2/busybox/manifests/1", 25 | }, 26 | defaultRegistry: testDefaultRegistry, 27 | want: &PathInfo{ 28 | Host: testDefaultRegistry, 29 | Image: "busybox", 30 | Manifests: "1", 31 | }, 32 | wantOk: true, 33 | }, 34 | { 35 | args: args{ 36 | path: "/v2/pytorch/pytorch/manifests/1", 37 | }, 38 | defaultRegistry: testDefaultRegistry, 39 | want: &PathInfo{ 40 | Host: testDefaultRegistry, 41 | Image: "pytorch/pytorch", 42 | Manifests: "1", 43 | }, 44 | wantOk: true, 45 | }, 46 | { 47 | args: args{ 48 | path: "/v2/v2/manifests/latest", 49 | }, 50 | defaultRegistry: testDefaultRegistry, 51 | want: &PathInfo{ 52 | Host: testDefaultRegistry, 53 | Image: "v2", 54 | Manifests: "latest", 55 | }, 56 | wantOk: true, 57 | }, 58 | { 59 | args: args{ 60 | path: "/v2/docker.io/busybox/manifests/1", 61 | }, 62 | want: &PathInfo{ 63 | Host: "docker.io", 64 | Image: "busybox", 65 | Manifests: "1", 66 | }, 67 | wantOk: true, 68 | }, 69 | { 70 | args: args{ 71 | path: "/v2/docker.io/library/busybox/manifests/1", 72 | }, 73 | want: &PathInfo{ 74 | Host: "docker.io", 75 | Image: "library/busybox", 76 | Manifests: "1", 77 | }, 78 | wantOk: true, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | got, gotOk := ParseOriginPathInfo(tt.args.path, tt.defaultRegistry) 84 | if !reflect.DeepEqual(got, tt.want) { 85 | t.Errorf("ParseOriginPathInfo() got = %v, want %v", got, tt.want) 86 | } 87 | if gotOk != tt.wantOk { 88 | t.Errorf("ParseOriginPathInfo() gotOk = %v, want %v", gotOk, tt.wantOk) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func Test_addPrefixToImageForPagination(t *testing.T) { 95 | type args struct { 96 | oldLink string 97 | host string 98 | } 99 | tests := []struct { 100 | name string 101 | args args 102 | want string 103 | }{ 104 | { 105 | args: args{ 106 | oldLink: "; ref=other", 107 | host: "prefix", 108 | }, 109 | want: "; ref=other", 110 | }, 111 | { 112 | args: args{ 113 | oldLink: "; ref=other", 114 | host: "prefix", 115 | }, 116 | want: "; ref=other", 117 | }, 118 | } 119 | for _, tt := range tests { 120 | t.Run(tt.name, func(t *testing.T) { 121 | if got := addPrefixToImageForPagination(tt.args.oldLink, tt.args.host); got != tt.want { 122 | t.Errorf("addPrefixToImageForPagination() = %v, want %v", got, tt.want) 123 | } 124 | }) 125 | } 126 | } 127 | --------------------------------------------------------------------------------