Skip to content

Commit

Permalink
feat(): Add Docker subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
ArmanTaheriGhaleTaki committed Jan 11, 2025
1 parent 12df65a commit dbc8b53
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 65 deletions.
1 change: 1 addition & 0 deletions config/dockerRegistry.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker.arvancloud.ir focker.ir registry.docker.ir docker.host:5000 docker.iranserver.com docker.haiocloud.com registry.registryhub.ir
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ 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
gotest.tools/v3 v3.0.3
)

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
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
17 changes: 0 additions & 17 deletions internal/check/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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
Expand All @@ -91,15 +80,13 @@ 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 {
if len(segment) < 1 || len(segment) > 63 {
return false
}
}

return true
}

Expand All @@ -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
}
23 changes: 23 additions & 0 deletions internal/common/function.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 3 additions & 11 deletions internal/dns/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -28,33 +29,26 @@ 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()
// Create a custom HTTP client with the specified DNS
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 {
Expand All @@ -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
Expand All @@ -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.")
}
Expand Down
148 changes: 115 additions & 33 deletions internal/docker/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit dbc8b53

Please sign in to comment.