From dbc8b5316329d4a8a350d79897972dc9e1013d77 Mon Sep 17 00:00:00 2001 From: ArmanTaheriGhaleTaki Date: Fri, 10 Jan 2025 22:29:07 -0600 Subject: [PATCH] feat(): Add Docker subcommand --- config/dockerRegistry.conf | 1 + go.mod | 3 +- go.sum | 2 - internal/check/function.go | 17 ----- internal/common/function.go | 23 ++++++ internal/dns/function.go | 14 +--- internal/docker/function.go | 148 ++++++++++++++++++++++++++++-------- 7 files changed, 143 insertions(+), 65 deletions(-) create mode 100644 config/dockerRegistry.conf create mode 100644 internal/common/function.go diff --git a/config/dockerRegistry.conf b/config/dockerRegistry.conf new file mode 100644 index 0000000..9c05284 --- /dev/null +++ b/config/dockerRegistry.conf @@ -0,0 +1 @@ +docker.arvancloud.ir focker.ir registry.docker.ir docker.host:5000 docker.iranserver.com docker.haiocloud.com registry.registryhub.ir \ No newline at end of file diff --git a/go.mod b/go.mod index 161a502..b9450b6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/salehborhani/403Unlocker-cli go 1.23.1 require ( + github.com/cavaliergopher/grab/v3 v3.0.1 github.com/google/go-containerregistry v0.20.2 github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.27.5 @@ -10,7 +11,6 @@ require ( ) require ( - github.com/cavaliergopher/grab/v3 v3.0.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -21,7 +21,6 @@ require ( github.com/klauspost/compress v1.16.5 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/logrusorgru/aurora/v3 v3.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect diff --git a/go.sum b/go.sum index 731c927..00328de 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= -github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/internal/check/function.go b/internal/check/function.go index b15018f..e901f84 100644 --- a/internal/check/function.go +++ b/internal/check/function.go @@ -41,22 +41,17 @@ func CheckWithDNS(c *cli.Context) error { fmt.Println(err) return err } - url = ensureHTTPS(url) - var wg sync.WaitGroup for _, dns := range dnsList { wg.Add(1) go func(dns string) { defer wg.Done() - client := ChangeDNS(dns) - resp, err := client.Get(url) if err != nil { return } - defer resp.Body.Close() code := strings.Split(resp.Status, " ") fmt.Printf("DNS: %s %s\n", dns, code[1]) @@ -75,13 +70,7 @@ func ReadDNSFromFile(filename string) ([]string, error) { return dnsServers, nil } func DomainValidator(domain string) bool { - // Regular expression to validate domain names - // This regex ensures: - // - The domain contains only alphanumeric characters, hyphens, and dots. - // - It does not start or end with a hyphen or dot. - // - It has at least one dot. domainRegex := `^(http[s]?:\/\/)?([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}).*?$` - // Match the domain against the regex match, _ := regexp.MatchString(domainRegex, domain) if !match { return false @@ -91,7 +80,6 @@ func DomainValidator(domain string) bool { if len(domain) > 253 { return false } - // 2. Each segment between dots should be between 1 and 63 characters long. segments := strings.Split(domain, ".") for _, segment := range segments { @@ -99,7 +87,6 @@ func DomainValidator(domain string) bool { return false } } - return true } @@ -112,21 +99,17 @@ func ensureHTTPS(url string) string { return url } regexHTTP := `^(http)://` - reHTTP, err := regexp.Compile(regexHTTP) if err != nil { fmt.Println("Error compiling regex:", err) return url } - if reHTTP.MatchString(url) { url = strings.TrimPrefix(url, "http://") } - // If the URL doesn't start with http:// or https://, prepend https:// if !re.MatchString(url) { url = "https://" + url } - return url } diff --git a/internal/common/function.go b/internal/common/function.go new file mode 100644 index 0000000..539e7a0 --- /dev/null +++ b/internal/common/function.go @@ -0,0 +1,23 @@ +package common + +import "fmt" + +// FormatDataSize converts the size in bytes to a human-readable string in KB, MB, or GB. +func FormatDataSize(bytes int64) string { + const ( + kb = 1024 + mb = kb * 1024 + gb = mb * 1024 + ) + + switch { + case bytes >= gb: + return fmt.Sprintf("%.2f GB", float64(bytes)/float64(gb)) + case bytes >= mb: + return fmt.Sprintf("%.2f MB", float64(bytes)/float64(mb)) + case bytes >= kb: + return fmt.Sprintf("%.2f KB", float64(bytes)/float64(kb)) + default: + return fmt.Sprintf("%d Bytes", bytes) + } +} diff --git a/internal/dns/function.go b/internal/dns/function.go index 8c20130..ac50df6 100644 --- a/internal/dns/function.go +++ b/internal/dns/function.go @@ -9,6 +9,7 @@ import ( "github.com/cavaliergopher/grab/v3" "github.com/salehborhani/403Unlocker-cli/internal/check" + "github.com/salehborhani/403Unlocker-cli/internal/common" "github.com/urfave/cli/v2" ) @@ -28,25 +29,19 @@ func URLValidator(URL string) bool { } return true } - func CheckWithURL(c *cli.Context) error { fileToDownload := c.Args().First() timeout := c.Int("timeout") - dnsList, err := check.ReadDNSFromFile("config/dns.conf") if err != nil { fmt.Println("Error reading DNS list:", err) return err } - // Map to store the total size downloaded by each DNS dnsSizeMap := make(map[string]int64) - fmt.Println("Timeout:", timeout) fmt.Println("URL: ", fileToDownload) - tempDir := time.Now().UnixMilli() - for _, dns := range dnsList { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() @@ -54,7 +49,6 @@ func CheckWithURL(c *cli.Context) error { clientWithCustomDNS := check.ChangeDNS(dns) client := grab.NewClient() client.HTTPClient = clientWithCustomDNS - // Create a new download request req, err := grab.NewRequest(fmt.Sprintf("/tmp/%v", tempDir), fileToDownload) if err != nil { @@ -65,10 +59,8 @@ func CheckWithURL(c *cli.Context) error { resp := client.Do(req) // Update the total size downloaded by this DNS dnsSizeMap[dns] += resp.BytesComplete() // Use BytesComplete() for partial downloads - fmt.Printf("Downloaded %d KB using DNS %s\n", resp.BytesComplete()/1_000, dns) - + fmt.Printf("%v\tDNS: %s\n", common.FormatDataSize(resp.BytesComplete()), dns) } - // Determine which DNS downloaded the most data var maxDNS string var maxSize int64 @@ -79,7 +71,7 @@ func CheckWithURL(c *cli.Context) error { } } if maxDNS != "" { - fmt.Printf("%s downloaded the most data: %d KB\n", maxDNS, maxSize/1_000) + fmt.Printf("best DNS is %s and downloaded the most data: %v\n", maxDNS, common.FormatDataSize(maxSize)) } else { fmt.Println("No DNS server was able to download any data.") } diff --git a/internal/docker/function.go b/internal/docker/function.go index 888db7b..6dfe633 100644 --- a/internal/docker/function.go +++ b/internal/docker/function.go @@ -3,63 +3,145 @@ package docker import ( "context" "fmt" + "io" "log" + "net/http" + "os" + "path/filepath" "regexp" "strings" + "sync/atomic" "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/salehborhani/403Unlocker-cli/internal/check" + "github.com/salehborhani/403Unlocker-cli/internal/common" "github.com/urfave/cli/v2" ) +// DockerImageValidator validates a Docker image name using a regular expression. func DockerImageValidator(imageName string) bool { - // Regular expression to match a valid Docker image name - // This pattern allows for optional registry, namespace, and tag/digest - pattern := `^(?:[a-zA-Z0-9\-._]+(?::[0-9]+)?/)?` + // Optional registry (e.g., docker.io, localhost:5000) - `(?:[a-z0-9\-._]+/)?` + // Optional namespace (e.g., library, user) - `[a-z0-9\-._]+` + // Repository name (required) - `(?::[a-zA-Z0-9\-._]+)?` + // Optional tag (e.g., latest, v1.0) - `(?:@[a-zA-Z0-9\-._:]+)?$` // Optional digest (e.g., sha256:...) - - // Compile the regular expression + pattern := `^(?:[a-zA-Z0-9\-._]+(?::[0-9]+)?/)?` + + `(?:[a-z0-9\-._]+/)?` + + `[a-z0-9\-._]+` + + `(?::[a-zA-Z0-9\-._]+)?` + + `(?:@[a-zA-Z0-9\-._:]+)?$` regex := regexp.MustCompile(pattern) - - // Check if the image name matches the pattern return regex.MatchString(imageName) && !strings.Contains(imageName, "@@") } -// downloadDockerImage downloads a Docker image from a registry and saves it as a tarball. -func DownloadDockerImage(imageName, registry, outputPath string, timeoutSeconds int) error { - imageName = registry + "/" + imageName - outputPath = outputPath + "/" + imageName - // Parse the image reference (e.g., "ubuntu:latest") - ref, err := name.ParseReference(imageName) +// customTransport tracks the number of bytes transferred during HTTP requests. +type customTransport struct { + Transport http.RoundTripper + Bytes int64 +} + +// RoundTrip implements the http.RoundTripper interface and wraps the response body to count bytes read. +func (c *customTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := c.Transport.RoundTrip(req) if err != nil { - return fmt.Errorf("failed to parse image reference: %v", err) + return nil, err } - // Authenticate with the registry (defaults to anonymous auth) - auth := authn.DefaultKeychain - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second) - defer cancel() - img, err := remote.Image(ref, remote.WithAuthFromKeychain(auth), remote.WithContext(ctx)) + resp.Body = &countingReader{inner: resp.Body, Bytes: &c.Bytes} + return resp, nil +} + +// countingReader wraps an io.ReadCloser and counts the bytes read. +type countingReader struct { + inner io.ReadCloser + Bytes *int64 +} + +func (cr *countingReader) Read(p []byte) (int, error) { + n, err := cr.inner.Read(p) + atomic.AddInt64(cr.Bytes, int64(n)) + return n, err +} + +func (cr *countingReader) Close() error { + return cr.inner.Close() +} + +// DownloadDockerImage downloads a Docker image from a registry and tracks the bytes downloaded. +func DownloadDockerImage(ctx context.Context, imageName, registry, outputPath string) (int64, error) { + + fullImageName := registry + "/" + imageName + + // Parse the image reference. + ref, err := name.ParseReference(fullImageName) if err != nil { - if ctx.Err() == context.DeadlineExceeded { - log.Printf("Download timed out after %d seconds, saving partially downloaded image...\n", timeoutSeconds) - } else { - return fmt.Errorf("failed to fetch image: %v", err) - } + return 0, fmt.Errorf("failed to parse image reference: %v", err) } - // Save the image as a tarball - err = tarball.WriteToFile(outputPath, ref, img) + + auth := authn.DefaultKeychain + transport := &customTransport{Transport: http.DefaultTransport} + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(auth), remote.WithContext(ctx), remote.WithTransport(transport)) if err != nil { - return fmt.Errorf("failed to save image as tarball: %v", err) + return transport.Bytes, fmt.Errorf("failed to download image: %v", err) } - fmt.Printf("Image successfully downloaded and saved to %s\n", outputPath) - return nil + + // Ensure output directory exists. + if err := os.MkdirAll(outputPath, 0755); err != nil { + return transport.Bytes, fmt.Errorf("failed to create output directory: %v", err) + } + + // Save the image as a tarball. + tarballPath := filepath.Join(outputPath, filepath.Base(imageName)+".tar") + if err := tarball.WriteToFile(tarballPath, ref, img); err != nil { + return transport.Bytes, nil + } + + return transport.Bytes, nil } + +// CheckWithDockerImage downloads the image from multiple registries and reports the downloaded data size. func CheckWithDockerImage(c *cli.Context) error { + registrySizeMap := make(map[string]int64) + timeout := c.Int("timeout") + imageName := c.Args().First() + tempDir := time.Now().UnixMilli() + fmt.Println("Timeout:", timeout) + fmt.Println("Docker Image: ", imageName) + + if imageName == "" { + return fmt.Errorf("image name cannot be empty") + } + + registryList, err := check.ReadDNSFromFile("config/dockerRegistry.conf") + if err != nil { + log.Printf("Error reading registry list: %v", err) + return err + } + + for _, registry := range registryList { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + size, err := DownloadDockerImage(ctx, imageName, registry, fmt.Sprintf("/tmp/%v", tempDir)) + if err != nil { + fmt.Printf("%s: %v\n", registry, "failed") + continue + } + registrySizeMap[registry] += size + fmt.Printf("%s downloaded : %v\n", registry, common.FormatDataSize(size)) + } + // Determine which DNS downloaded the most data + var maxRegistry string + var maxSize int64 + for dns, size := range registrySizeMap { + if size > maxSize { + maxRegistry = dns + maxSize = size + } + } + if maxRegistry != "" { + fmt.Printf("best Registry is %s and downloaded the most data: %v\n", maxRegistry, common.FormatDataSize(maxSize)) + } else { + fmt.Println("No DNS server was able to download any data.") + } + os.RemoveAll(fmt.Sprintf("/tmp/%v", tempDir)) return nil }