├── images ├── aws │ └── api_gateway │ │ ├── map.png │ │ ├── overall_diagram.png │ │ ├── rotating_process.png │ │ └── available_ips_per_region.png ├── azure │ └── cloud_shell │ │ ├── map.png │ │ ├── overall_diagram.png │ │ └── available_ips_per_region.png ├── github │ └── github_actions │ │ ├── map.png │ │ ├── overall_diagram.png │ │ └── available_ips_per_region.png └── ipspinner │ └── overall_diagram.png ├── Makefile ├── utils ├── rand.go ├── interfaces.go ├── maps.go ├── constants.go ├── slices.go ├── url.go ├── logger.go ├── strings.go ├── structs.go ├── network.go ├── files.go ├── requests.go └── crypto.go ├── providers ├── aws │ ├── auth.go │ ├── aws.go │ └── fireprox.go ├── github │ ├── github.go │ ├── sdk.go │ ├── repository.go │ └── files.go ├── azure │ ├── files.go │ ├── azure.go │ ├── account.go │ └── cloudshell.go └── providers.go ├── Containerfile ├── config.ini ├── go.mod ├── main.go ├── proxy └── proxy.go ├── go.sum └── README.md /images/aws/api_gateway/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/aws/api_gateway/map.png -------------------------------------------------------------------------------- /images/azure/cloud_shell/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/azure/cloud_shell/map.png -------------------------------------------------------------------------------- /images/github/github_actions/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/github/github_actions/map.png -------------------------------------------------------------------------------- /images/ipspinner/overall_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/ipspinner/overall_diagram.png -------------------------------------------------------------------------------- /images/aws/api_gateway/overall_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/aws/api_gateway/overall_diagram.png -------------------------------------------------------------------------------- /images/aws/api_gateway/rotating_process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/aws/api_gateway/rotating_process.png -------------------------------------------------------------------------------- /images/azure/cloud_shell/overall_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/azure/cloud_shell/overall_diagram.png -------------------------------------------------------------------------------- /images/github/github_actions/overall_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/github/github_actions/overall_diagram.png -------------------------------------------------------------------------------- /images/aws/api_gateway/available_ips_per_region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/aws/api_gateway/available_ips_per_region.png -------------------------------------------------------------------------------- /images/azure/cloud_shell/available_ips_per_region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/azure/cloud_shell/available_ips_per_region.png -------------------------------------------------------------------------------- /images/github/github_actions/available_ips_per_region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/IPSpinner/HEAD/images/github/github_actions/available_ips_per_region.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | pre-build: 4 | go mod tidy 5 | 6 | 7 | build-all: build-linux build-windows build-collector-linux build-collector-windows 8 | 9 | 10 | build: build-linux build-windows 11 | 12 | build-linux: pre-build 13 | GOOS=linux GOARCH=amd64 go build -o ipspinner . 14 | 15 | build-windows: pre-build 16 | GOOS=windows GOARCH=amd64 go build -o ipspinner.exe . 17 | 18 | 19 | clean: 20 | rm ipspinner 21 | rm ipspinner.exe 22 | -------------------------------------------------------------------------------- /utils/rand.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | // GenerateSecureRandomInt génère un nombre entier aléatoire entre 0 et max (exclusif) en utilisant crypto/rand. 9 | func generateSecureRandomInt(max int) int { 10 | if max <= 0 { 11 | return 0 12 | } 13 | 14 | // Convertir max en un type big.Int pour l'utiliser avec crypto/rand 15 | n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) 16 | if err != nil { 17 | return 0 18 | } 19 | 20 | // Convertir en int et retourner le résultat 21 | return int(n.Int64()) 22 | } 23 | -------------------------------------------------------------------------------- /utils/interfaces.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | ) 7 | 8 | type Provider interface { 9 | GetName() string 10 | SummarizeState() string 11 | GetAvailableLaunchers() []Launcher 12 | GetLaunchers() []Launcher 13 | GetNbTotalReqSent() int 14 | IsStopped() bool 15 | Clear() bool 16 | } 17 | 18 | type Launcher interface { 19 | GetName() string 20 | GetProvider() Provider 21 | GetNbTotalReqSent() int 22 | SummarizeState() string 23 | PreloadHosts(context.Context, []*url.URL) 24 | SendRequest(context.Context, HTTPRequestData, *AllConfigs) (HTTPResponseData, string, error) 25 | IsAvailable() bool 26 | IsStopped() bool 27 | Clear() bool 28 | } 29 | -------------------------------------------------------------------------------- /utils/maps.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Returns a random key in the given map 4 | func RandomKeyOfMap[T any](m map[string]T) string { 5 | keys := make([]string, 0, len(m)) 6 | 7 | for key := range m { 8 | keys = append(keys, key) 9 | } 10 | 11 | if len(keys) == 0 { 12 | return "" 13 | } 14 | 15 | randomIndex := generateSecureRandomInt(len(keys)) 16 | 17 | return keys[randomIndex] 18 | } 19 | 20 | // Returns the value associated to the key in the given map if it exists, or the defaultValue otherwise 21 | func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V { 22 | if val, ok := m[key]; ok { 23 | return val 24 | } 25 | 26 | return defaultValue 27 | } 28 | -------------------------------------------------------------------------------- /providers/aws/auth.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "ipspinner/utils" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/config" 9 | "github.com/aws/aws-sdk-go-v2/credentials" 10 | ) 11 | 12 | const awsMaxAttempts = 30 13 | 14 | // Returns an AWS config with provided credentials and information: 15 | func GetConfig(ctx context.Context, accessKey, secretKey, sessionToken, region string) (aws.Config, error) { 16 | cfg, err := config.LoadDefaultConfig(ctx, 17 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, sessionToken)), 18 | config.WithRegion(region), 19 | config.WithRetryMaxAttempts(awsMaxAttempts), 20 | config.WithHTTPClient(utils.GetHTTPClient(nil)), 21 | ) 22 | 23 | return cfg, err 24 | } 25 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | ARG USERNAME=ipspinner 4 | ARG USER_UID=1000 5 | ARG USER_GID=$USER_UID 6 | 7 | RUN apt update 8 | RUN apt install -y build-essential curl wget 9 | 10 | # Install go 11 | RUN wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz 12 | RUN tar -C /usr/local -xzf go1.22.1.linux-amd64.tar.gz 13 | RUN rm -f go1.22.1.linux-amd64.tar.gz 14 | ENV PATH="$PATH:/usr/local/go/bin" 15 | 16 | COPY . /go/workspace/ipspinner 17 | WORKDIR /go/workspace/ipspinner 18 | 19 | RUN printf "go 1.22.1\n\nuse ./ipspinner" > /go/workspace/go.work 20 | 21 | RUN groupadd --gid $USER_GID $USERNAME \ 22 | && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME 23 | 24 | RUN chown -R $USERNAME /go/workspace/ipspinner 25 | 26 | USER $USERNAME 27 | WORKDIR /go/workspace/ipspinner 28 | 29 | RUN go mod tidy 30 | RUN make build-linux 31 | 32 | ENTRYPOINT ["/go/workspace/ipspinner/ipspinner"] 33 | -------------------------------------------------------------------------------- /utils/constants.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | LogFile = "ipspinner.log" 5 | SummarizeStateInterval = 300 6 | MaxResourcePerFireProxInstance = 300 7 | IPSpinnerResponseHeaderPrefix = "X-IPSpinner-" 8 | ) 9 | 10 | // RandomWords are used for generating random sentences (to be stealthier when a string input is required => like for naming something) 11 | // And also because its funnier to have a "real" sentence rather than fjeqrjagfdfdqs for a repository name :) 12 | // Why are you still reading this? 13 | // Thanks ChatGPT for generating a such nice list :) 14 | var RandomWords = []string{ 15 | "apple", "banana", "cat", "dog", "elephant", "fish", "gorilla", "hat", "icecream", 16 | "jacket", "kangaroo", "lemon", "monkey", "ninja", "orange", "penguin", "queen", 17 | "rabbit", "snake", "tiger", "umbrella", "vampire", "whale", "xylophone", "yak", "zebra", 18 | } 19 | -------------------------------------------------------------------------------- /utils/slices.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Removes the given element in the slice (removes it each time it appears) 4 | func DeleteElementFromSlice[T comparable](slice []T, element T) []T { 5 | newSlice := []T{} 6 | 7 | for _, e := range slice { 8 | if e != element { 9 | newSlice = append(newSlice, e) 10 | } 11 | } 12 | 13 | return newSlice 14 | } 15 | 16 | // Returns a random element from the slice 17 | func RandomElementInSlice[T any](slice []T) T { 18 | randomIndex := generateSecureRandomInt(len(slice)) 19 | 20 | return slice[randomIndex] 21 | } 22 | 23 | // Takes as input a list and a maxLength and subdivises the input list into a list of sublists with a maximym length of maxLength 24 | func SubdiviseSlice[T any](slice []T, maxSubSliceLength int) [][]T { 25 | allSubSlices := [][]T{} 26 | currentSubSlice := []T{} 27 | 28 | for _, element := range slice { 29 | currentSubSlice = append(currentSubSlice, element) 30 | 31 | if len(currentSubSlice) >= maxSubSliceLength { 32 | allSubSlices = append(allSubSlices, currentSubSlice) 33 | 34 | currentSubSlice = []T{} 35 | } 36 | } 37 | 38 | if len(currentSubSlice) > 0 { 39 | allSubSlices = append(allSubSlices, currentSubSlice) 40 | } 41 | 42 | return allSubSlices 43 | } 44 | -------------------------------------------------------------------------------- /utils/url.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // A base URL is the [SCHEME]://[DOMAIN]:[PORT] 10 | func GetBaseURL(urlObj *url.URL) string { 11 | scheme := strings.ToLower(urlObj.Scheme) 12 | host := strings.ToLower(urlObj.Host) 13 | 14 | if scheme == "" { 15 | scheme = "https" 16 | } 17 | 18 | baseURL := fmt.Sprintf("%s://%s", scheme, host) 19 | 20 | port := urlObj.Port() 21 | 22 | if port == "" { 23 | if scheme == "https" { 24 | baseURL += ":443" 25 | } else { 26 | baseURL += ":80" 27 | } 28 | } 29 | 30 | return baseURL 31 | } 32 | 33 | // A path corresponds to the part after the host part 34 | // Ex: the path for "https://google.com/search/test" is "/search/test" 35 | func GetPathFromURL(urlObj *url.URL) string { 36 | schemeLength := 0 37 | 38 | if len(urlObj.Scheme) > 0 { 39 | schemeLength = len(urlObj.Scheme) + 3 40 | } 41 | 42 | prefixLength := schemeLength + len(urlObj.Host) 43 | 44 | minIndex := min(len(urlObj.String()), prefixLength) 45 | 46 | return urlObj.String()[minIndex:] 47 | } 48 | 49 | // Generates a random string with lowercase letters 50 | func GenerateRandomPrefix(size int) string { 51 | const lowercaseLetters = "abcdefghijklmnopqrstuvwxyz" 52 | 53 | prefix := make([]byte, size) 54 | 55 | for i := 0; i < size; i++ { 56 | prefix[i] = lowercaseLetters[generateSecureRandomInt(len(lowercaseLetters))] 57 | } 58 | 59 | return string(prefix) 60 | } 61 | 62 | func CompareBaseURLs(url1, url2 *url.URL) bool { 63 | return GetBaseURL(url1) == GetBaseURL(url2) 64 | } 65 | 66 | func DoesURLListContainsBaseURL(urls []*url.URL, url1 *url.URL) bool { 67 | for _, url2 := range urls { 68 | if CompareBaseURLs(url1, url2) { 69 | return true 70 | } 71 | } 72 | 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "time" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | // https://stackoverflow.com/questions/76858037/how-to-use-zerolog-to-filter-info-logs-to-stdout-and-error-logs-to-stderr 12 | type SpecificLevelWriter struct { 13 | io.Writer 14 | Levels []zerolog.Level 15 | DebugVerbose *bool 16 | TraceVerbose *bool 17 | } 18 | 19 | var Logger zerolog.Logger 20 | var runLogFile *os.File 21 | 22 | // Closes the log file 23 | func CloseLogFile() error { 24 | return runLogFile.Close() 25 | } 26 | 27 | // Initialises the logger (specific rules and handlers) 28 | func InitializeLogger(verbose1, verbose2 bool) { 29 | runLogFile, _ := os.OpenFile( 30 | LogFile, 31 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, 32 | fileMode, 33 | ) 34 | 35 | consolePartsToExclude := []string{zerolog.CallerFieldName} 36 | 37 | trueVar := true 38 | falseVar := false 39 | 40 | multi := zerolog.MultiLevelWriter( 41 | SpecificLevelWriter{ 42 | Writer: runLogFile, 43 | DebugVerbose: &trueVar, 44 | TraceVerbose: &falseVar, 45 | }, 46 | SpecificLevelWriter{ 47 | Writer: zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC822Z, PartsExclude: consolePartsToExclude}, 48 | DebugVerbose: &verbose1, 49 | TraceVerbose: &verbose2, 50 | }, 51 | ) 52 | 53 | Logger = zerolog.New(multi).With().Caller().Timestamp().Logger() 54 | } 55 | 56 | // Handler for the log level 57 | // Does log or not according to the verbose mode 58 | func (w SpecificLevelWriter) WriteLevel(level zerolog.Level, content []byte) (int, error) { 59 | if level == zerolog.TraceLevel && !*w.TraceVerbose { 60 | return len(content), nil 61 | } 62 | 63 | if level == zerolog.DebugLevel && !*w.DebugVerbose { 64 | return len(content), nil 65 | } 66 | 67 | return w.Write(content) 68 | } 69 | -------------------------------------------------------------------------------- /utils/strings.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Generates a random password (at least one character of each class) (min size = 4) 11 | func GenerateRandomPassword(size int) string { 12 | const ( 13 | lowerChars = "abcdefghijklmnopqrstuvwxyz" 14 | upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 15 | numberChars = "0123456789" 16 | specialChars = "!.*" 17 | ) 18 | 19 | const allChars = lowerChars + upperChars + numberChars + specialChars 20 | 21 | password := randomChar(lowerChars) + randomChar(upperChars) + randomChar(numberChars) + randomChar(specialChars) 22 | 23 | for i := 0; i < (size - 4); i++ { //nolint:gomnd 24 | password += randomChar(allChars) 25 | } 26 | 27 | return shuffle(password) 28 | } 29 | 30 | // GenerateRandomSentence generates a random English sentence with X words 31 | func GenerateRandomSentence(size int) string { 32 | var sentence string 33 | 34 | for i := 0; i < size; i++ { 35 | if i > 0 { 36 | sentence += " " 37 | } 38 | 39 | wordIndex := generateSecureRandomInt(len(RandomWords)) 40 | sentence += RandomWords[wordIndex] 41 | } 42 | 43 | // Capitalize the first letter of the sentence (it's more beautiful, isn't it?) 44 | sentence = string(sentence[0]-32) + sentence[1:] //nolint:gomnd 45 | 46 | return sentence 47 | } 48 | 49 | func GenerateUUIDv4() string { 50 | return uuid.New().String() 51 | } 52 | 53 | func randomChar(str string) string { 54 | return string(str[generateSecureRandomInt(len(str))]) 55 | } 56 | 57 | func shuffle(str string) string { 58 | inRune := []rune(str) 59 | 60 | rand.Shuffle(len(inRune), func(i, j int) { 61 | inRune[i], inRune[j] = inRune[j], inRune[i] 62 | }) 63 | 64 | return string(inRune) 65 | } 66 | 67 | func PrepareBearerHeader(token string) string { 68 | return fmt.Sprintf("Bearer %s", token) 69 | } 70 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [proxy] 2 | #preload_hosts_file = /tmp/preloadHosts.txt 3 | #whitelist_hosts_file = /tmp/whitelist.txt 4 | #blacklist_hosts_file = /tmp/blacklist.txt # the blacklist is not taken into account if the whitelist is specified 5 | #ca_cert_file = 6 | #ca_cert_key_file = 7 | #user_agents_file=agents.txt 8 | debug_response_headers = true 9 | #wait_for_launcher_available_timeout=60 10 | 11 | [aws] 12 | ## AWS ## 13 | regions = eu-west-1,eu-west-2 14 | #profile = default # if enabled, it overrides access_key, secret_key and session_token parameters 15 | access_key = TO_FILL_IF_NEEDED 16 | secret_key = TO_FILL_IF_NEEDED 17 | #session_token = 18 | 19 | ## AWS API GATEWAY ## 20 | ag_enabled = false 21 | #ag_max_instances = 5 22 | #ag_rotate_nb_requests = 5000 23 | #ag_forwarded_for_range = 35.180.0.0/16 # AWS eu-west-3 from https://ip-ranges.amazonaws.com/ip-ranges.json 24 | #ag_instance_title_prefix = "fpr" # random three letter prefix by default 25 | #ag_instance_deployment_description = "IPSpinner FireProx Prod" # random sentence of english words by default 26 | #ag_instance_deployment_stage_description = "IPSpinner FireProx Prod Stage" # random sentence of english words by default 27 | #ag_instance_deployment_stage_name = "fireprox" # random sentence of english words by default 28 | 29 | [github] 30 | ## GITHUB ## 31 | username = TO_FILL_IF_NEEDED 32 | token = TO_FILL_IF_NEEDED 33 | 34 | ## GITHUB ACTIONS ## 35 | ga_enabled = false 36 | 37 | [azure] 38 | ## AZURE ## 39 | admin_email = TO_FILL_IF_NEEDED 40 | admin_password = TO_FILL_IF_NEEDED 41 | tenant_id = TO_FILL_IF_NEEDED 42 | subscription_id = TO_FILL_IF_NEEDED 43 | #accounts_file = azure_accounts.txt # can contain multiple accounts, one info per line : email, password (overrides admin_email and admin_password) 44 | 45 | ## AZURE CLOUD SHELL ## 46 | cs_enabled = false 47 | cs_preferred_locations = westus,westeurope #westus,southcentralus,eastus,northeurope,westeurope,centralindia,southeastasia## TO AVOID, not stable: westcentralus,eastus2euap,centraluseuap 48 | cs_nb_instances = 5 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ipspinner 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 7 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 8 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 9 | github.com/aws/aws-sdk-go-v2 v1.26.1 10 | github.com/aws/aws-sdk-go-v2/config v1.27.10 11 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10 12 | github.com/aws/aws-sdk-go-v2/service/apigateway v1.23.6 13 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 14 | github.com/google/uuid v1.6.0 15 | github.com/gorilla/websocket v1.5.1 16 | github.com/jefflinse/githubsecret v1.0.2 17 | github.com/rs/zerolog v1.32.0 18 | gopkg.in/ini.v1 v1.67.0 19 | ) 20 | 21 | require ( 22 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect 23 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect 33 | github.com/aws/smithy-go v1.20.2 // indirect 34 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 35 | github.com/kylelemons/godebug v1.1.0 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.19 // indirect 38 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 39 | golang.org/x/crypto v0.26.0 // indirect 40 | golang.org/x/net v0.28.0 // indirect 41 | golang.org/x/sys v0.23.0 // indirect 42 | golang.org/x/text v0.17.0 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /utils/structs.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "net/url" 6 | ) 7 | 8 | type HTTPRequestData struct { 9 | URL *url.URL 10 | Method string 11 | Headers map[string]any 12 | Body *bytes.Buffer 13 | FollowRedirections bool 14 | } 15 | 16 | type HTTPResponseData struct { 17 | StatusCode int 18 | Headers map[string]any 19 | Body []byte 20 | } 21 | 22 | type HTTPRequestJSONData struct { 23 | URL *url.URL 24 | Method string 25 | Headers map[string]any 26 | Body map[string]any 27 | FollowRedirections bool 28 | } 29 | 30 | type HTTPResponseJSONData struct { 31 | StatusCode int 32 | Headers map[string]any 33 | Body map[string]any 34 | } 35 | 36 | type ProvidersConfig struct { 37 | AWSRegions []string 38 | AWSProfile string 39 | AWSAccessKey string 40 | AWSSecretKey string 41 | AWSSessionToken string 42 | AWSAGEnabled bool 43 | AWSAGMaxInstances int 44 | AWSAGRotateNbRequests int 45 | AWSAGForwardedForRange string 46 | AWSAGInstanceTitlePrefix string 47 | AWSAGInstanceDeploymentDescription string 48 | AWSAGInstanceDeploymentStageDescription string 49 | AWSAGInstanceDeploymentStageName string 50 | GitHubGAEnabled bool 51 | GitHubUsername string 52 | GitHubToken string 53 | AzureAdminEmail string 54 | AzureAdminPassword string 55 | AzureTenantID string 56 | AzureSubscriptionID string 57 | AzureAccountsFile string 58 | AzureCSEnabled bool 59 | AzureCSPreferredLocations []string 60 | AzureCSNbInstances int 61 | } 62 | 63 | type ProxyConfig struct { 64 | PreloadHostsFile string 65 | WhitelistHostsFile string 66 | BlacklistHostsFile string 67 | CaCertFile string 68 | CaCertKeyFile string 69 | DebugResponseHeaders bool 70 | UserAgentsFile string 71 | WaitForLauncherAvailableTimeout int 72 | } 73 | 74 | type CommandParameters struct { 75 | Host string 76 | Port int 77 | Verbose1 bool 78 | Verbose2 bool 79 | Verbose3 bool 80 | ExportCaCert bool 81 | ConfigINIPath string 82 | } 83 | 84 | type AllConfigs struct { 85 | ProvidersConfig ProvidersConfig 86 | ProxyConfig ProxyConfig 87 | CommandParameters CommandParameters 88 | } 89 | -------------------------------------------------------------------------------- /utils/network.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "math/big" 6 | "math/rand" 7 | "net" 8 | "time" 9 | ) 10 | 11 | const ExpectedBitSize = 128 12 | const PaddingSize = 16 13 | 14 | // Returns a random IP from the CIDR 15 | func RandomIPFromCIDR(cidr string) (net.IP, error) { 16 | _, ipNet, err := net.ParseCIDR(cidr) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | if ipNet.IP.To4() != nil { 22 | return randomIPv4FromCIDR(cidr) 23 | } 24 | 25 | return randomIPv6FromCIDR(cidr) 26 | } 27 | 28 | // randomIPv6FromCIDR generates a random IPv6 address from the given CIDR. 29 | func randomIPv6FromCIDR(cidr string) (net.IP, error) { 30 | _, ipv6Net, err := net.ParseCIDR(cidr) 31 | if err != nil { 32 | return nil, errors.New("invalid CIDR") 33 | } 34 | 35 | // Get the network start IP 36 | startIP := ipv6Net.IP 37 | startBytes := startIP.To16() 38 | 39 | if startBytes == nil { 40 | return nil, errors.New("invalid start IP in CIDR") 41 | } 42 | 43 | startInt := new(big.Int).SetBytes(startBytes) 44 | 45 | // Calculate the network size 46 | maskSize, totalBits := ipv6Net.Mask.Size() 47 | if totalBits != ExpectedBitSize { 48 | return nil, errors.New("invalid IPv6 network mask") 49 | } 50 | 51 | networkSize := ExpectedBitSize - maskSize 52 | 53 | // Calculate the maximum IP in the range (end IP) 54 | maxAdd := new(big.Int).Lsh(big.NewInt(1), uint(networkSize)) 55 | endInt := new(big.Int).Add(startInt, maxAdd) 56 | endInt.Sub(endInt, big.NewInt(1)) 57 | 58 | // Generate a random number in the range [startInt, endInt] 59 | diff := new(big.Int).Sub(endInt, startInt) 60 | randInt := new(big.Int).Rand(rand.New(rand.NewSource(time.Now().UnixNano())), new(big.Int).Add(diff, big.NewInt(1))) //nolint:gosec 61 | randInt.Add(randInt, startInt) 62 | 63 | // Convert back to IP 64 | randBytes := randInt.Bytes() 65 | if len(randBytes) < PaddingSize { 66 | padded := make([]byte, PaddingSize) 67 | copy(padded[PaddingSize-len(randBytes):], randBytes) 68 | randBytes = padded 69 | } 70 | 71 | randomIP := net.IP(randBytes) 72 | 73 | return randomIP, nil 74 | } 75 | 76 | // Returns a random IPv4 from the CIDR 77 | func randomIPv4FromCIDR(cidr string) (net.IP, error) { 78 | _, ipNet, err := net.ParseCIDR(cidr) 79 | 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | // Convert network IP to 32-bit integer 85 | start := ipNet.IP.To4() 86 | startInt := int(start[0])<<24 | int(start[1])<<16 | int(start[2])<<8 | int(start[3]) //nolint:revive 87 | 88 | // Calculate the number of possible addresses in the CIDR 89 | ones, bits := ipNet.Mask.Size() 90 | numAddresses := 1 << uint(bits-ones) 91 | 92 | // Generate a random offset within the range of possible addresses 93 | offset := generateSecureRandomInt(numAddresses) 94 | 95 | // Calculate the final IP address by adding the offset to the starting IP 96 | resultInt := startInt + offset 97 | result := make(net.IP, 4) //nolint:gomnd 98 | result[0] = byte(resultInt >> 24) //nolint:gomnd 99 | result[1] = byte(resultInt >> 16) //nolint:gomnd 100 | result[2] = byte(resultInt >> 8) //nolint:gomnd 101 | result[3] = byte(resultInt) 102 | 103 | return result, nil 104 | } 105 | -------------------------------------------------------------------------------- /utils/files.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "os/user" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | const fileMode = 0600 //nolint:gocritic 13 | 14 | // Reads the content of the given file 15 | func ReadFileContent(filePath string) ([]byte, error) { 16 | content, err := os.ReadFile(filePath) 17 | 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return content, nil 23 | } 24 | 25 | // Write the given content into the given file 26 | func WriteFileContent(filePath string, content []byte) error { 27 | err := os.WriteFile(filePath, content, fileMode) 28 | 29 | return err 30 | } 31 | 32 | // ReadFileLines reads the content of the given file and returns each line as a string slice 33 | func ReadFileLines(filePath string) ([]string, error) { 34 | content, err := ReadFileContent(filePath) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | lines := strings.Split(string(content), "\n") 41 | 42 | // Remove empty lines if any 43 | var cleanLines []string 44 | 45 | for _, line := range lines { 46 | if line != "" { 47 | cleanLines = append(cleanLines, line) 48 | } 49 | } 50 | 51 | return cleanLines, nil 52 | } 53 | 54 | // Parses a host file (a text file where each line contains a domain name or a URL) 55 | func ParseHostsFile(path string) []*url.URL { 56 | if path == "" { 57 | return []*url.URL{} 58 | } 59 | 60 | hostsStr, hostsStrErr := ReadFileLines(path) 61 | 62 | if hostsStrErr != nil { 63 | Logger.Warn().Err(hostsStrErr).Msg("Can not read preload hosts file.") 64 | 65 | return []*url.URL{} 66 | } 67 | 68 | hostLst := []*url.URL{} 69 | 70 | for _, hostStr := range hostsStr { 71 | // If this is only a domain name => it loads http and https endpoints 72 | if !strings.HasPrefix(hostStr, "http://") && !strings.HasPrefix(hostStr, "https://") { 73 | for _, prefix := range []string{"http://", "https://"} { 74 | newHostStr := prefix + hostStr 75 | host, hostErr := url.Parse(newHostStr) 76 | 77 | if hostErr != nil { 78 | Logger.Warn().Err(hostsStrErr).Str("host", newHostStr).Msg("Host can not be parsed as a valid URL.") 79 | 80 | continue 81 | } 82 | 83 | hostLst = append(hostLst, host) 84 | } 85 | } else { 86 | host, hostErr := url.Parse(hostStr) 87 | 88 | if hostErr != nil { 89 | Logger.Warn().Err(hostsStrErr).Str("host", hostStr).Msg("Host can not be parsed as a valid URL.") 90 | 91 | continue 92 | } 93 | 94 | hostLst = append(hostLst, host) 95 | } 96 | } 97 | 98 | return hostLst 99 | } 100 | 101 | func GetHomeDirectory() (string, error) { 102 | // Essayez d'abord d'utiliser os/user pour obtenir le répertoire home 103 | usr, err := user.Current() 104 | if err == nil { 105 | return usr.HomeDir, nil 106 | } 107 | 108 | // Si os/user échoue, utilisez les variables d'environnement 109 | if runtime.GOOS == "windows" { 110 | home := os.Getenv("USERPROFILE") 111 | 112 | if home != "" { 113 | return home, nil 114 | } 115 | } else { 116 | home := os.Getenv("HOME") 117 | 118 | if home != "" { 119 | return home, nil 120 | } 121 | } 122 | 123 | return "", fmt.Errorf("cannot retrieve the user home directory") 124 | } 125 | 126 | func FileExists(path string) bool { 127 | _, err := os.Stat(path) 128 | 129 | return !os.IsNotExist(err) 130 | } 131 | -------------------------------------------------------------------------------- /utils/requests.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | // Sends a HTTP request with the provided request json data and receives a json-body response 13 | func SendJSONRequest(reqJSONData HTTPRequestJSONData) (HTTPResponseJSONData, error) { 14 | body := &bytes.Buffer{} 15 | 16 | if reqJSONData.Body != nil { 17 | reqBodyErr := json.NewEncoder(body).Encode(reqJSONData.Body) 18 | 19 | if reqBodyErr != nil { 20 | return HTTPResponseJSONData{}, reqBodyErr 21 | } 22 | } else { 23 | body = nil 24 | } 25 | 26 | if _, ok := reqJSONData.Headers["Content-Type"]; !ok { 27 | reqJSONData.Headers["Content-Type"] = "application/json" 28 | } 29 | 30 | reqData := HTTPRequestData{ 31 | URL: reqJSONData.URL, 32 | Method: reqJSONData.Method, 33 | Headers: reqJSONData.Headers, 34 | Body: body, 35 | FollowRedirections: reqJSONData.FollowRedirections, 36 | } 37 | 38 | resp, respErr := SendRequest(reqData) 39 | 40 | if respErr != nil { 41 | return HTTPResponseJSONData{}, respErr 42 | } 43 | 44 | var bodyJSON map[string]any 45 | 46 | if len(resp.Body) > 0 { 47 | respBodyErr := json.Unmarshal(resp.Body, &bodyJSON) 48 | 49 | if respBodyErr != nil { 50 | return HTTPResponseJSONData{}, respBodyErr 51 | } 52 | } 53 | 54 | return HTTPResponseJSONData{ 55 | StatusCode: resp.StatusCode, 56 | Headers: resp.Headers, 57 | Body: bodyJSON, 58 | }, nil 59 | } 60 | 61 | // Sends a HTTP request with the provided request data 62 | func SendRequest(reqData HTTPRequestData) (HTTPResponseData, error) { 63 | body := reqData.Body 64 | 65 | if body == nil { 66 | body = &bytes.Buffer{} 67 | } 68 | 69 | req, reqErr := http.NewRequest(reqData.Method, reqData.URL.String(), body) 70 | 71 | if reqErr != nil { 72 | return HTTPResponseData{}, reqErr 73 | } 74 | 75 | // Sets the request headers 76 | for key, value := range reqData.Headers { 77 | req.Header.Set(key, fmt.Sprintf("%v", value)) 78 | } 79 | 80 | checkRedirect := func(req *http.Request, via []*http.Request) error { 81 | return http.ErrUseLastResponse 82 | } 83 | 84 | if reqData.FollowRedirections { 85 | checkRedirect = nil 86 | } 87 | 88 | // Sends the request 89 | client := GetHTTPClient(checkRedirect) 90 | 91 | resp, respErr := client.Do(req) 92 | 93 | if respErr != nil { 94 | return HTTPResponseData{}, respErr 95 | } 96 | 97 | defer resp.Body.Close() 98 | 99 | // Reads the response body 100 | respBody, respBodyErr := io.ReadAll(resp.Body) 101 | 102 | if respBodyErr != nil { 103 | return HTTPResponseData{}, respBodyErr 104 | } 105 | 106 | // Parses the response headers 107 | responseHeaders := make(map[string]any) 108 | 109 | for key, value := range resp.Header { 110 | if len(value) > 0 { 111 | responseHeaders[key] = value[0] 112 | } 113 | } 114 | 115 | return HTTPResponseData{ 116 | StatusCode: resp.StatusCode, 117 | Headers: responseHeaders, 118 | Body: respBody, 119 | }, nil 120 | } 121 | 122 | func GetHTTPClient(checkRedirect func(req *http.Request, via []*http.Request) error) *http.Client { 123 | return &http.Client{ 124 | CheckRedirect: checkRedirect, 125 | Transport: &http.Transport{ 126 | Proxy: http.ProxyFromEnvironment, 127 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec 128 | }, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /providers/github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "ipspinner/utils" 8 | ) 9 | 10 | type Infos struct { 11 | Accept string 12 | APIVersion string 13 | Token string 14 | Username string 15 | } 16 | 17 | // Provider object 18 | type GitHub struct { 19 | repositories []*Repository 20 | infos Infos 21 | stopped bool 22 | } 23 | 24 | //nolint:revive 25 | func (instance GitHub) GetName() string { 26 | return "GitHub" 27 | } 28 | 29 | func (instance GitHub) GetInfos() Infos { 30 | return instance.infos 31 | } 32 | 33 | func (instance GitHub) SummarizeState() string { 34 | if !instance.IsStopped() { 35 | return fmt.Sprintf("Provider %s is running with %d launcher(s).", instance.GetName(), len(instance.GetLaunchers())) 36 | } 37 | 38 | return fmt.Sprintf("Provider %s is stopped.", instance.GetName()) 39 | } 40 | 41 | func (instance *GitHub) GetAvailableLaunchers() []utils.Launcher { 42 | launchers := make([]utils.Launcher, 0) 43 | 44 | for _, launcher := range instance.GetLaunchers() { 45 | if launcher.IsAvailable() { 46 | launchers = append(launchers, launcher) 47 | } 48 | } 49 | 50 | return launchers 51 | } 52 | 53 | func (instance *GitHub) GetLaunchers() []utils.Launcher { 54 | launchers := make([]utils.Launcher, 0) 55 | 56 | for _, repository := range instance.repositories { 57 | launchers = append(launchers, repository) 58 | } 59 | 60 | return launchers 61 | } 62 | 63 | func (instance GitHub) GetNbTotalReqSent() int { 64 | count := 0 65 | 66 | for _, launcher := range instance.GetLaunchers() { 67 | count += launcher.GetNbTotalReqSent() 68 | } 69 | 70 | return count 71 | } 72 | 73 | func (instance GitHub) IsStopped() bool { 74 | return instance.stopped 75 | } 76 | 77 | func (instance *GitHub) Clear() bool { 78 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("Clearing provider.") 79 | 80 | instance.stopped = true 81 | 82 | fullyCleared := true 83 | 84 | for _, launcher := range instance.GetLaunchers() { 85 | cleared := launcher.Clear() 86 | 87 | if !cleared { 88 | fullyCleared = false 89 | } 90 | } 91 | 92 | return fullyCleared 93 | } 94 | 95 | // Creates and initializes a new instance of the GitHub provider object 96 | // revive:disable:unused-parameter 97 | func Initialize(ctx context.Context, allConfigs *utils.AllConfigs) (GitHub, error) { 98 | instance := GitHub{stopped: false} 99 | 100 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("Configuring provider.") 101 | 102 | instance.infos = Infos{ 103 | Accept: "application/vnd.github+json", 104 | APIVersion: "2022-11-28", 105 | Token: allConfigs.ProvidersConfig.GitHubToken, 106 | Username: allConfigs.ProvidersConfig.GitHubUsername, 107 | } 108 | 109 | if allConfigs.ProvidersConfig.GitHubGAEnabled { 110 | instance.loadRepositoryLaunchers() 111 | } 112 | 113 | if len(instance.GetLaunchers()) == 0 { 114 | return instance, errors.New("no launchers could have been created") 115 | } 116 | 117 | return instance, nil 118 | } 119 | 120 | // revive:enable:unused-parameter 121 | 122 | func (instance *GitHub) loadRepositoryLaunchers() { 123 | launcher, launcherErr := CreateRepositoryLauncher(instance) 124 | 125 | if launcherErr != nil { 126 | utils.Logger.Error().Err(launcherErr).Msg("Cannot create repository workers launcher.") 127 | } else { 128 | instance.repositories = append(instance.repositories, launcher) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /providers/azure/files.go: -------------------------------------------------------------------------------- 1 | package azure 2 | 3 | const SCRIPT_PY_FILE_BASE64 string = "aW1wb3J0IHJlcXVlc3RzCmZyb20gdXJsbGliMy5leGNlcHRpb25zIGltcG9ydCBJbnNlY3VyZVJlcXVlc3RXYXJuaW5nCmltcG9ydCBiYXNlNjQKaW1wb3J0IHN5cwppbXBvcnQgdHlwaW5nCgpyZXF1ZXN0cy5wYWNrYWdlcy51cmxsaWIzLmRpc2FibGVfd2FybmluZ3MoKSAKCmlmIGxlbihzeXMuYXJndikgPCAzOgogICAgcmFpc2UgRXhjZXB0aW9uKCJVc2FnZTogcHl0aG9uMyBzY3JpcHRzLnB5IE1FVEhPRF9FTkMgVVJMX0VOQyBbSEVBREVSU19FTkNdIFtCT0RZX0VOQ10iKQoKQ0hVTktfU0laRSA9IDUwMDAKCm1ldGhvZEVuYyA9IHN5cy5hcmd2WzFdCnVybEVuYyA9IHN5cy5hcmd2WzJdCmhlYWRlcnNFbmMgPSBzeXMuYXJndlszXSBpZiBsZW4oc3lzLmFyZ3YpID49IDQgZWxzZSAiIgpib2R5RW5jID0gc3lzLmFyZ3ZbNF0gaWYgbGVuKHN5cy5hcmd2KSA+PSA1IGVsc2UgIiIKCmRlZiBwYXJzZUlucHV0cyhtZXRob2RFbmM6IHN0ciwgdXJsRW5jOiBzdHIsIGhlYWRlcnNFbmM6IHN0ciwgYm9keUVuYzogc3RyKSAtPiB0eXBpbmcuVHVwbGVbYnl0ZXMsIGJ5dGVzLCBkaWN0LCBieXRlc106CiAgICBtZXRob2QgPSBiYXNlNjQuYjY0ZGVjb2RlKG1ldGhvZEVuYykKICAgIHVybCA9IGJhc2U2NC5iNjRkZWNvZGUodXJsRW5jKQogICAgCiAgICBoZWFkZXJzID0ge30KICAgIGlmIGxlbihoZWFkZXJzRW5jKSA+IDA6CiAgICAgICAgaGVhZGVyc1JhdyA9IGJhc2U2NC5iNjRkZWNvZGUoaGVhZGVyc0VuYykuZGVjb2RlKCkKICAgICAgICBoZWFkZXJzTGluZXMgPSBoZWFkZXJzUmF3LnNwbGl0bGluZXMoKQogICAgICAgIGlmIGxlbihoZWFkZXJzTGluZXMpJTIgIT0gMDoKICAgICAgICAgICAgcmFpc2UgRXhjZXB0aW9uKCJDYW4gbm90IHBhcnNlIHRoZSByZXF1ZXN0IGhlYWRlcnMuIikKCiAgICAgICAgZm9yIGkgaW4gcmFuZ2UoMCwgbGVuKGhlYWRlcnNMaW5lcyksIDIpOgogICAgICAgICAgICBoZWFkZXJzW2hlYWRlcnNMaW5lc1tpXV0gPSBoZWFkZXJzTGluZXNbaSsxXQoKICAgIGJvZHkgPSBOb25lCiAgICBpZiBsZW4oYm9keUVuYykgPiAwOgogICAgICAgIGJvZHkgPSBiYXNlNjQuYjY0ZGVjb2RlKGJvZHlFbmMpCgogICAgcmV0dXJuIG1ldGhvZCwgdXJsLCBoZWFkZXJzLCBib2R5CgpkZWYgZW5jb2RlT3V0cHV0cyhzdGF0dXM6IGludCwgaGVhZGVyczogZGljdCwgYm9keTogYnl0ZXMpIC0+IHR5cGluZy5UdXBsZVtzdHIsIHN0ciwgc3RyXToKICAgIHN0YXR1c0VuYyA9ICIiCiAgICBpZiBzdGF0dXMgIT0gTm9uZToKICAgICAgICBzdGF0dXNFbmMgPSBiYXNlNjQuYjY0ZW5jb2RlKHN0cihzdGF0dXMpLmVuY29kZSgpKS5kZWNvZGUoKQoKICAgIGhlYWRlcnNTdHIgPSAiIgogICAgaWYgaGVhZGVycyAhPSBOb25lOgogICAgICAgIGZvciBrZXkgaW4gaGVhZGVyczoKICAgICAgICAgICAgaGVhZGVyc1N0ciArPSBrZXkgKyAiXG4iICsgaGVhZGVyc1trZXldICsgIlxuIgogICAgICAgIGlmIGxlbihoZWFkZXJzU3RyKSA+IDA6CiAgICAgICAgICAgIGhlYWRlcnNTdHIgPSBoZWFkZXJzU3RyWzotMV0KICAgIGhlYWRlcnNFbmMgPSBiYXNlNjQuYjY0ZW5jb2RlKGhlYWRlcnNTdHIuZW5jb2RlKCkpLmRlY29kZSgpCgogICAgYm9keUVuYyA9ICIiCiAgICBpZiBib2R5ICE9IE5vbmU6CiAgICAgICAgYm9keUVuYyA9IGJhc2U2NC5iNjRlbmNvZGUoYm9keSkuZGVjb2RlKCkKCiAgICByZXR1cm4gc3RhdHVzRW5jLCBoZWFkZXJzRW5jLCBib2R5RW5jCgpkZWYgbWFrZVJlcXVlc3QobWV0aG9kOiBieXRlcywgdXJsOiBieXRlcywgaGVhZGVyczogZGljdD1Ob25lLCBib2R5OiBieXRlcz1Ob25lKToKICAgIHRyeToKICAgICAgICByZXNwb25zZSA9IHJlcXVlc3RzLnJlcXVlc3QobWV0aG9kLCB1cmwsIGhlYWRlcnM9aGVhZGVycywgZGF0YT1ib2R5LCB2ZXJpZnk9RmFsc2UsIGFsbG93X3JlZGlyZWN0cz1GYWxzZSwgc3RyZWFtPVRydWUpCiAgICAKICAgICAgICBzdGF0dXMgPSByZXNwb25zZS5zdGF0dXNfY29kZQogICAgICAgIGhlYWRlcnMgPSByZXNwb25zZS5oZWFkZXJzCiAgICAgICAgYm9keSA9IHJlc3BvbnNlLnJhdy5yZWFkKCkKCiAgICAgICAgcmV0dXJuIHN0YXR1cywgaGVhZGVycywgYm9keSwgTm9uZQogICAgZXhjZXB0IHJlcXVlc3RzLlJlcXVlc3RFeGNlcHRpb24gYXMgZToKICAgICAgICByZXR1cm4gTm9uZSwgTm9uZSwgTm9uZSwgc3RyKGUpCgoKcmVxTWV0aG9kLCByZXFVcmwsIHJlcUhlYWRlcnMsIHJlcUJvZHkgPSBwYXJzZUlucHV0cyhtZXRob2RFbmMsIHVybEVuYywgaGVhZGVyc0VuYywgYm9keUVuYykKCnJlc3BTdGF0dXMsIHJlc3BIZWFkZXJzLCByZXNwQm9keSwgcmVzcEVyciA9IG1ha2VSZXF1ZXN0KHJlcU1ldGhvZCwgcmVxVXJsLCByZXFIZWFkZXJzLCByZXFCb2R5KQoKcHJpbnQoIlJFU1BfU1RBUlQiKQoKaWYgcmVzcEVyciAhPSBOb25lOgogICAgcHJpbnQoIlJFU1BfRVJSIiwgcmVzcEVycikKZWxzZToKICAgIHJlc3BTdGF0dXNFbmMsIHJlc3BIZWFkZXJzRW5jLCByZXNwQm9keUVuYyA9IGVuY29kZU91dHB1dHMocmVzcFN0YXR1cywgcmVzcEhlYWRlcnMsIHJlc3BCb2R5KQoKICAgIHByaW50KCJSRVNQX1NUQVRVU19FTkMiLCByZXNwU3RhdHVzRW5jKQogICAgcHJpbnQoIlJFU1BfSEVBREVSU19FTkMiLCByZXNwSGVhZGVyc0VuYykKICAgICMgV2UgY2h1bmsgdGhlIHJlc3BvbnNlIGJvZHkgaW4gdGhlIGNhc2UgdGhlIGJvZHkgaXMgdG9vIGxhcmdlCiAgICBmb3IgaSBpbiByYW5nZSgwLCBsZW4ocmVzcEJvZHlFbmMpLCBDSFVOS19TSVpFKToKICAgICAgICBwcmludCgiUkVTUF9CT0RZX0VOQyIsIHJlc3BCb2R5RW5jW2k6aSArIENIVU5LX1NJWkVdKQogICAgcHJpbnQoIlJFU1BfRU5EIik=" //nolint:revive,stylecheck 4 | -------------------------------------------------------------------------------- /utils/crypto.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/hex" 11 | "encoding/pem" 12 | "io" 13 | "math/big" 14 | "time" 15 | ) 16 | 17 | const rsaKeyBitsSize = 2048 18 | const aesKeyBytesSize = 32 19 | 20 | // Generates and returns a RSA CA certificate and key 21 | func GenerateRSACACertificate() (cert, key []byte, err error) { 22 | // Generates a RSA key 23 | privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitsSize) 24 | if err != nil { 25 | return []byte{}, []byte{}, err 26 | } 27 | 28 | // Creates a self-signed certificate 29 | template := x509.Certificate{ 30 | SerialNumber: big.NewInt(1), 31 | Subject: pkix.Name{Organization: []string{"IPSpinner Tool"}}, 32 | NotBefore: time.Now(), 33 | NotAfter: time.Now().Add(365 * 24 * time.Hour), 34 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 35 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 36 | BasicConstraintsValid: true, 37 | IsCA: true, 38 | } 39 | 40 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 41 | if err != nil { 42 | return []byte{}, []byte{}, err 43 | } 44 | 45 | // Converts the certificate in a PEM format 46 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 47 | 48 | // Converts the key in a PEM format 49 | privBytes := x509.MarshalPKCS1PrivateKey(privateKey) 50 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) 51 | 52 | return certPEM, keyPEM, nil 53 | } 54 | 55 | func Aes256GenerateKey() (string, error) { 56 | bytes := make([]byte, aesKeyBytesSize) // generate a random 32 byte key for AES-256 57 | 58 | if _, err := rand.Read(bytes); err != nil { 59 | return "", err 60 | } 61 | 62 | return hex.EncodeToString(bytes), nil 63 | } 64 | 65 | func Aes256Encrypt(dataStr, keyHexStr string) (string, error) { 66 | key, _ := hex.DecodeString(keyHexStr) 67 | 68 | plaintext := []byte(dataStr) 69 | 70 | // Create a new Cipher Block from the key 71 | block, err := aes.NewCipher(key) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | // Create a new GCM - https://en.wikipedia.org/wiki/Galois/Counter_Mode 77 | // https://golang.org/pkg/crypto/cipher/#NewGCM 78 | aesGCM, err := cipher.NewGCM(block) 79 | if err != nil { 80 | return "", err 81 | } 82 | 83 | // Create a nonce. Nonce should be from GCM 84 | nonce := make([]byte, aesGCM.NonceSize()) 85 | if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 86 | return "", err 87 | } 88 | 89 | // Encrypt the data using aesGCM.Seal 90 | // Since we don't want to save the nonce somewhere else in this case, we add it as a prefix to the encrypted data. The first nonce argument in Seal is the prefix. 91 | ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) 92 | 93 | return hex.EncodeToString(ciphertext), nil 94 | } 95 | 96 | func Aes256Decrypt(encryptedDataHexStr, keyHexStr string) (string, error) { 97 | key, _ := hex.DecodeString(keyHexStr) 98 | enc, _ := hex.DecodeString(encryptedDataHexStr) 99 | 100 | // Create a new Cipher Block from the key 101 | block, err := aes.NewCipher(key) 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | // Create a new GCM 107 | aesGCM, err := cipher.NewGCM(block) 108 | if err != nil { 109 | return "", err 110 | } 111 | 112 | // Get the nonce size 113 | nonceSize := aesGCM.NonceSize() 114 | 115 | // Extract the nonce from the encrypted data 116 | nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] 117 | 118 | // Decrypt the data 119 | plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) 120 | if err != nil { 121 | return "", err 122 | } 123 | 124 | return string(plaintext), nil 125 | } 126 | -------------------------------------------------------------------------------- /providers/providers.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "ipspinner/providers/aws" 6 | "ipspinner/providers/azure" 7 | "ipspinner/providers/github" 8 | "ipspinner/utils" 9 | "net/url" 10 | "strings" 11 | ) 12 | 13 | // Initializes and returns all available providers 14 | func LoadProviders(ctx context.Context, allConfigs *utils.AllConfigs) []utils.Provider { 15 | utils.Logger.Info().Msg("Loading providers, please do not press ctrl+c.") 16 | 17 | providers := []utils.Provider{} 18 | 19 | // For AWS FireProx 20 | if allConfigs.ProvidersConfig.AWSAGEnabled { 21 | provider, providerErr := aws.Initialize(ctx, allConfigs) 22 | 23 | if providerErr != nil { 24 | utils.Logger.Error().Err(providerErr).Str("provider", provider.GetName()).Msg("Can not load the provider.") 25 | 26 | provider.Clear() 27 | } else { 28 | providers = append(providers, provider) 29 | } 30 | } 31 | 32 | // For Github Actions 33 | if allConfigs.ProvidersConfig.GitHubGAEnabled { 34 | if strings.EqualFold(allConfigs.ProvidersConfig.GitHubUsername, "synacktiv") { 35 | utils.Logger.Error().Msg("Are you crazy ? Do not run IPSpinner on the Github Synacktiv account, we may be banned...") 36 | } else { 37 | provider, providerErr := github.Initialize(ctx, allConfigs) 38 | 39 | if providerErr != nil { 40 | utils.Logger.Error().Err(providerErr).Str("provider", provider.GetName()).Msg("Can not load the provider.") 41 | 42 | provider.Clear() 43 | } else { 44 | providers = append(providers, &provider) 45 | } 46 | } 47 | } 48 | 49 | // For Azure Cloudshell 50 | if allConfigs.ProvidersConfig.AzureCSEnabled { 51 | provider, providerErr := azure.Initialize(ctx, allConfigs) 52 | 53 | if providerErr != nil { 54 | utils.Logger.Error().Err(providerErr).Str("provider", provider.GetName()).Msg("Can not load the provider.") 55 | 56 | provider.Clear() 57 | } else { 58 | providers = append(providers, provider) 59 | } 60 | } 61 | 62 | preloadHosts := utils.ParseHostsFile(allConfigs.ProxyConfig.PreloadHostsFile) 63 | whitelistHosts := utils.ParseHostsFile(allConfigs.ProxyConfig.WhitelistHostsFile) 64 | blacklistHosts := utils.ParseHostsFile(allConfigs.ProxyConfig.BlacklistHostsFile) 65 | 66 | newPreloadHosts := []*url.URL{} 67 | 68 | // If a whitelist or a blacklist is provided, it checks if URLs in preload hosts list are suitable 69 | // The whitelist is prioritary on the blacklist 70 | if len(whitelistHosts) > 0 || len(blacklistHosts) > 0 { 71 | for _, preloadHost := range preloadHosts { 72 | if len(whitelistHosts) > 0 { // Whitelist is used 73 | if utils.DoesURLListContainsBaseURL(whitelistHosts, preloadHost) { 74 | newPreloadHosts = append(newPreloadHosts, preloadHost) 75 | } else { 76 | utils.Logger.Warn().Str("host", preloadHost.String()).Msg("The host has been removed from the preloading hosts because it is not mentioned in the whitelist.") 77 | } 78 | } else if len(blacklistHosts) > 0 { // if the whitelist is not used and the blacklist is used 79 | if utils.DoesURLListContainsBaseURL(blacklistHosts, preloadHost) { 80 | utils.Logger.Warn().Str("host", preloadHost.String()).Msg("The host has been removed from the preloading hosts because it is mentioned in the blacklist.") 81 | } else { 82 | newPreloadHosts = append(newPreloadHosts, preloadHost) 83 | } 84 | } 85 | } 86 | } else { 87 | newPreloadHosts = preloadHosts 88 | } 89 | 90 | if len(newPreloadHosts) > 0 { 91 | for _, provider := range providers { 92 | for _, launcher := range provider.GetLaunchers() { 93 | launcher.PreloadHosts(ctx, newPreloadHosts) 94 | } 95 | } 96 | } 97 | 98 | return providers 99 | } 100 | 101 | // Clears all configured providers 102 | func ClearProviders(providers []utils.Provider) { 103 | result := true 104 | 105 | for _, provider := range providers { 106 | result = result && provider.Clear() 107 | } 108 | 109 | if result { 110 | utils.Logger.Info().Msg("All providers have been cleared.") 111 | } else { 112 | utils.Logger.Error().Msg("Some providers have not been cleared.") 113 | } 114 | } 115 | 116 | func GetAllLaunchers(providers []utils.Provider) []*utils.Launcher { 117 | launchers := make([]*utils.Launcher, 0) 118 | 119 | for _, provider := range providers { 120 | for _, launcher := range provider.GetLaunchers() { 121 | launcherCopy := launcher 122 | 123 | launchers = append(launchers, &launcherCopy) 124 | } 125 | } 126 | 127 | return launchers 128 | } 129 | 130 | func GetAllAvailableLaunchers(providers []utils.Provider) []*utils.Launcher { 131 | launchers := make([]*utils.Launcher, 0) 132 | 133 | for _, provider := range providers { 134 | for _, launcher := range provider.GetAvailableLaunchers() { 135 | launcherCopy := launcher 136 | 137 | launchers = append(launchers, &launcherCopy) 138 | } 139 | } 140 | 141 | return launchers 142 | } 143 | -------------------------------------------------------------------------------- /providers/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "ipspinner/utils" 8 | "path" 9 | 10 | awssdk "github.com/aws/aws-sdk-go-v2/aws" 11 | "gopkg.in/ini.v1" 12 | ) 13 | 14 | type AWS struct { 15 | awsConfigs map[string]*awssdk.Config 16 | fireProxInstances []*FireProx 17 | stopped bool 18 | } 19 | 20 | //nolint:revive 21 | func (instance AWS) GetName() string { 22 | return "AWS" 23 | } 24 | 25 | func (instance AWS) SummarizeState() string { 26 | if !instance.IsStopped() { 27 | return fmt.Sprintf("Provider %s is running with %d launcher(s).", instance.GetName(), len(instance.GetLaunchers())) 28 | } 29 | 30 | return fmt.Sprintf("Provider %s is stopped.", instance.GetName()) 31 | } 32 | 33 | func (instance *AWS) GetAvailableLaunchers() []utils.Launcher { 34 | launchers := make([]utils.Launcher, 0) 35 | 36 | for _, launcher := range instance.GetLaunchers() { 37 | if launcher.IsAvailable() { 38 | launchers = append(launchers, launcher) 39 | } 40 | } 41 | 42 | return launchers 43 | } 44 | 45 | func (instance *AWS) GetLaunchers() []utils.Launcher { 46 | launchers := make([]utils.Launcher, 0) 47 | 48 | for _, fireProx := range instance.fireProxInstances { 49 | launchers = append(launchers, fireProx) 50 | } 51 | 52 | return launchers 53 | } 54 | 55 | func (instance AWS) GetNbTotalReqSent() int { 56 | count := 0 57 | 58 | for _, launcher := range instance.GetLaunchers() { 59 | count += launcher.GetNbTotalReqSent() 60 | } 61 | 62 | return count 63 | } 64 | 65 | func (instance AWS) IsStopped() bool { 66 | return instance.stopped 67 | } 68 | 69 | func (instance *AWS) Clear() bool { 70 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("Clearing provider.") 71 | 72 | instance.stopped = true 73 | 74 | fullyCleared := true 75 | 76 | for _, launcher := range instance.GetLaunchers() { 77 | cleared := launcher.Clear() 78 | 79 | if !cleared { 80 | fullyCleared = false 81 | } 82 | } 83 | 84 | return fullyCleared 85 | } 86 | 87 | // Creates and initializes a new instance of the AWS provider object 88 | func Initialize(ctx context.Context, allConfigs *utils.AllConfigs) (*AWS, error) { 89 | instance := AWS{stopped: false} 90 | 91 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("Configuring provider.") 92 | 93 | awsConfigs, awsConfigsErr := loadAWSConfigs(ctx, allConfigs) 94 | 95 | if awsConfigsErr != nil { 96 | return &instance, awsConfigsErr 97 | } 98 | 99 | // If no awssdk.Config has been successfully instantiated => can not go further 100 | if len(awsConfigs) == 0 { 101 | return &instance, errors.New("no valid AWS configurations have been set up (please check the provided credentials and regions)") 102 | } 103 | 104 | instance.awsConfigs = awsConfigs 105 | 106 | if allConfigs.ProvidersConfig.AWSAGEnabled { 107 | instance.loadFireProxLaunchers(allConfigs) 108 | } 109 | 110 | if len(instance.GetLaunchers()) == 0 { 111 | return &instance, errors.New("no launchers could have been created") 112 | } 113 | 114 | return &instance, nil 115 | } 116 | 117 | func (instance *AWS) loadFireProxLaunchers(allConfigs *utils.AllConfigs) { 118 | maxFireProxInstances := allConfigs.ProvidersConfig.AWSAGMaxInstances 119 | fireProxTitlePrefix := allConfigs.ProvidersConfig.AWSAGInstanceTitlePrefix 120 | fireProxDeploymentDescription := allConfigs.ProvidersConfig.AWSAGInstanceDeploymentDescription 121 | fireProxDeploymentStageDescription := allConfigs.ProvidersConfig.AWSAGInstanceDeploymentStageDescription 122 | fireProxDeploymentStageName := allConfigs.ProvidersConfig.AWSAGInstanceDeploymentStageName 123 | rotateAPIGateway := allConfigs.ProvidersConfig.AWSAGRotateNbRequests 124 | 125 | fireProx, fireProxErr := CreateFireProx(instance, maxFireProxInstances, fireProxTitlePrefix, fireProxDeploymentDescription, fireProxDeploymentStageDescription, fireProxDeploymentStageName, rotateAPIGateway) 126 | 127 | if fireProxErr != nil { 128 | utils.Logger.Error().Err(fireProxErr).Msg("Cannot create FireProx launcher.") 129 | 130 | return 131 | } 132 | 133 | instance.fireProxInstances = append(instance.fireProxInstances, fireProx) 134 | } 135 | 136 | func loadAWSConfigs(ctx context.Context, allConfigs *utils.AllConfigs) (map[string]*awssdk.Config, error) { 137 | awsConfigs := make(map[string]*awssdk.Config, 0) 138 | 139 | accessKey := allConfigs.ProvidersConfig.AWSAccessKey 140 | secretKey := allConfigs.ProvidersConfig.AWSSecretKey 141 | sessionToken := allConfigs.ProvidersConfig.AWSSessionToken 142 | 143 | if len(allConfigs.ProvidersConfig.AWSProfile) > 0 { 144 | homeDirectory, homeDirectoryErr := utils.GetHomeDirectory() 145 | 146 | if homeDirectoryErr != nil { 147 | return awsConfigs, homeDirectoryErr 148 | } 149 | 150 | awsCredentialsFilePath := path.Join(homeDirectory, ".aws", "credentials") 151 | 152 | if !utils.FileExists(awsCredentialsFilePath) { 153 | return awsConfigs, fmt.Errorf("the aws credentials file does not exist or is not at its default location (%s)", awsCredentialsFilePath) 154 | } 155 | 156 | cfg, err := ini.Load(awsCredentialsFilePath) 157 | 158 | if err != nil { 159 | return awsConfigs, errors.New("cannot parse the aws credentials file") 160 | } 161 | 162 | if !cfg.HasSection(allConfigs.ProvidersConfig.AWSProfile) { 163 | return awsConfigs, fmt.Errorf("the user %s cannot be found in the aws credentials file", allConfigs.ProvidersConfig.AWSProfile) 164 | } 165 | 166 | accessKey = cfg.Section(allConfigs.ProvidersConfig.AWSProfile).Key("aws_access_key_id").String() 167 | secretKey = cfg.Section(allConfigs.ProvidersConfig.AWSProfile).Key("aws_secret_access_key").String() 168 | sessionToken = cfg.Section(allConfigs.ProvidersConfig.AWSProfile).Key("aws_session_token").String() 169 | } 170 | 171 | // Instantiates the awssdk.Config for each region 172 | for _, awsRegion := range allConfigs.ProvidersConfig.AWSRegions { 173 | utils.Logger.Info().Str("region", awsRegion).Msg("Creating AWS configuration.") 174 | 175 | awsConfig, err := GetConfig(ctx, accessKey, secretKey, sessionToken, awsRegion) 176 | 177 | if err != nil { 178 | utils.Logger.Warn().Err(err).Str("region", awsRegion).Msg("Can not authenticate to AWS with provided credentials on the region.") 179 | } else { 180 | awsConfigs[awsRegion] = &awsConfig 181 | } 182 | } 183 | 184 | return awsConfigs, nil 185 | } 186 | -------------------------------------------------------------------------------- /providers/azure/azure.go: -------------------------------------------------------------------------------- 1 | package azure 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "ipspinner/utils" 8 | "time" 9 | 10 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" 11 | ) 12 | 13 | // Provider object 14 | type Azure struct { 15 | precreatedAccounts []*Account 16 | cloudShells []*CloudShell 17 | adminAccount *Account 18 | stopped bool 19 | } 20 | 21 | //nolint:revive 22 | func (instance Azure) GetName() string { 23 | return "Azure" 24 | } 25 | 26 | func (instance Azure) IsStopped() bool { 27 | return instance.stopped 28 | } 29 | 30 | func (instance Azure) SummarizeState() string { 31 | if !instance.IsStopped() { 32 | return fmt.Sprintf("Provider %s is running with %d launcher(s).", instance.GetName(), len(instance.GetLaunchers())) 33 | } 34 | 35 | return fmt.Sprintf("Provider %s is stopped.", instance.GetName()) 36 | } 37 | 38 | // Creates and initializes a new instance of the Azure provider object 39 | func Initialize(ctx context.Context, allConfigs *utils.AllConfigs) (*Azure, error) { //nolint:revive 40 | instance := Azure{stopped: false} 41 | 42 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("Configuring provider.") 43 | 44 | if allConfigs.ProvidersConfig.AzureAccountsFile == "" { 45 | adminAccount, adminAccountErr := ConnectAccount(allConfigs.ProvidersConfig.AzureAdminEmail, allConfigs.ProvidersConfig.AzureAdminPassword, allConfigs.ProvidersConfig.AzureTenantID, false, "", "") 46 | 47 | if adminAccountErr != nil { 48 | return &instance, adminAccountErr 49 | } 50 | 51 | instance.adminAccount = &adminAccount 52 | } else { 53 | loadAccountsErr := instance.loadAzurePrecreatedAccounts(allConfigs) 54 | 55 | if loadAccountsErr != nil { 56 | return &instance, loadAccountsErr 57 | } 58 | } 59 | 60 | if allConfigs.ProvidersConfig.AzureCSEnabled { 61 | instance.loadCloudShellLaunchers(allConfigs.ProvidersConfig.AzureCSNbInstances, allConfigs.ProvidersConfig.AzureSubscriptionID, allConfigs.ProvidersConfig.AzureCSPreferredLocations) 62 | } 63 | 64 | if len(instance.GetLaunchers()) == 0 { 65 | return &instance, errors.New("no launchers could have been created") 66 | } 67 | 68 | return &instance, nil 69 | } 70 | 71 | func (instance *Azure) loadAzurePrecreatedAccounts(allConfigs *utils.AllConfigs) error { 72 | utils.Logger.Debug().Str("provider", instance.GetName()).Msg("Loading precreated accounts.") 73 | 74 | tenantID := allConfigs.ProvidersConfig.AzureTenantID 75 | 76 | accountsFileLines, accountsFileLinesErr := utils.ReadFileLines(allConfigs.ProvidersConfig.AzureAccountsFile) 77 | 78 | if accountsFileLinesErr != nil { 79 | return accountsFileLinesErr 80 | } 81 | 82 | if len(accountsFileLines)%2 != 0 { 83 | return fmt.Errorf("the Azure accounts file does not respect the expected format: 2 lines per account (email, password)") 84 | } 85 | 86 | for i := 0; i < len(accountsFileLines)/2; i++ { 87 | email := accountsFileLines[i*2] 88 | password := accountsFileLines[i*2+1] 89 | 90 | account, accountErr := ConnectAccount(email, password, tenantID, false, "", "") 91 | 92 | if accountErr != nil { 93 | utils.Logger.Warn().Str("provider", instance.GetName()).Str("email", email).Msg("Cannot connect to this user.") 94 | 95 | continue 96 | } 97 | 98 | instance.precreatedAccounts = append(instance.precreatedAccounts, &account) 99 | } 100 | 101 | if len(instance.precreatedAccounts) == 0 { 102 | return fmt.Errorf("ipspinner was not able to connect to any of the precreated accounts") 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (instance *Azure) loadCloudShellLaunchers(nbLaunchers int, subscriptionID string, preferredLocations []string) { 109 | if len(preferredLocations) == 0 { 110 | preferredLocations = append(preferredLocations, "westeurope") 111 | } 112 | 113 | for i := 0; i < nbLaunchers; i++ { 114 | var account *Account 115 | 116 | if len(instance.precreatedAccounts) > 0 { 117 | if i >= len(instance.precreatedAccounts) { 118 | utils.Logger.Warn().Msg("No more precreated account available for creating a new CloudShell launcher.") 119 | 120 | continue 121 | } 122 | 123 | account = instance.precreatedAccounts[i] 124 | } else { 125 | accountCreated, accountCreatedErr := CreateCloudShellAccount(instance, subscriptionID) 126 | 127 | if accountCreatedErr != nil { 128 | utils.Logger.Error().Err(accountCreatedErr).Msg("Cannot create an account for this CloudShell launcher.") 129 | 130 | continue 131 | } 132 | 133 | account = &accountCreated 134 | } 135 | 136 | cloudShell, cloudShellErr := CreateCloudShell(instance, subscriptionID, preferredLocations[i%len(preferredLocations)], *account) 137 | 138 | if cloudShellErr != nil { 139 | utils.Logger.Error().Err(cloudShellErr).Msg("Cannot create cloud shell launcher.") 140 | 141 | // Waits for the account to be propagated 142 | time.Sleep(2 * time.Second) 143 | 144 | cloudShell.Clear() 145 | 146 | continue 147 | } 148 | 149 | instance.cloudShells = append(instance.cloudShells, cloudShell) 150 | } 151 | } 152 | 153 | func (instance *Azure) GetAvailableLaunchers() []utils.Launcher { 154 | launchers := make([]utils.Launcher, 0) 155 | 156 | for _, launcher := range instance.GetLaunchers() { 157 | if launcher.IsAvailable() { 158 | launchers = append(launchers, launcher) 159 | } 160 | } 161 | 162 | return launchers 163 | } 164 | 165 | func (instance *Azure) GetLaunchers() []utils.Launcher { 166 | launchers := make([]utils.Launcher, 0) 167 | 168 | for _, cloudShell := range instance.cloudShells { 169 | launchers = append(launchers, cloudShell) 170 | } 171 | 172 | return launchers 173 | } 174 | 175 | func (instance *Azure) GetAdminAccount() *Account { 176 | return instance.adminAccount 177 | } 178 | 179 | func (instance *Azure) Clear() bool { 180 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("Clearing provider.") 181 | 182 | instance.stopped = true 183 | 184 | fullyCleared := true 185 | 186 | for _, launcher := range instance.GetLaunchers() { 187 | cleared := launcher.Clear() 188 | 189 | if !cleared { 190 | fullyCleared = false 191 | } 192 | } 193 | 194 | return fullyCleared 195 | } 196 | 197 | func (instance Azure) GetNbTotalReqSent() int { 198 | count := 0 199 | 200 | for _, launcher := range instance.GetLaunchers() { 201 | count += launcher.GetNbTotalReqSent() 202 | } 203 | 204 | return count 205 | } 206 | 207 | //nolint:revive //tmp 208 | func (instance Azure) CreateResourceGroup() { 209 | rgClient, rgClientErr := armresources.NewResourceGroupsClient("f89fac99-3ac9-4650-942f-acd69f471c20", *instance.adminAccount.tokenCredential, nil) 210 | 211 | fmt.Println(rgClientErr) 212 | 213 | location := "francecentral" 214 | 215 | createResp, createErr := rgClient.CreateOrUpdate(context.Background(), "rgtest", armresources.ResourceGroup{ 216 | Location: &location, 217 | }, &armresources.ResourceGroupsClientCreateOrUpdateOptions{}) 218 | 219 | fmt.Printf("%+v \n", createResp) 220 | 221 | fmt.Println(createErr) 222 | } 223 | 224 | //nolint:revive //tmp 225 | func (instance Azure) DeleteResourceGroup() { 226 | rgClient, rgClientErr := armresources.NewResourceGroupsClient("f89fac99-3ac9-4650-942f-acd69f471c20", *instance.adminAccount.tokenCredential, nil) 227 | 228 | fmt.Println(rgClientErr) 229 | 230 | deleteResp, deleteErr := rgClient.BeginDelete(context.Background(), "rgtest", &armresources.ResourceGroupsClientBeginDeleteOptions{}) 231 | 232 | fmt.Printf("%+v \n", deleteResp) 233 | 234 | fmt.Println(deleteErr) 235 | } 236 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "ipspinner/providers" 9 | "ipspinner/proxy" 10 | "ipspinner/utils" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | "time" 16 | 17 | "gopkg.in/ini.v1" 18 | ) 19 | 20 | func main() { 21 | //////////////////////////// //nolint:revive 22 | // INITIALISATION SECTION // 23 | //////////////////////////// //nolint:revive 24 | commandParameters := parseFlags() 25 | 26 | utils.InitializeLogger(commandParameters.Verbose1, commandParameters.Verbose2) 27 | 28 | defer utils.CloseLogFile() //nolint:errcheck 29 | 30 | proxyConfig, providersConfig, parseConfigErr := parseConfig(commandParameters.ConfigINIPath) 31 | 32 | if parseConfigErr != nil { 33 | utils.Logger.Error().Err(parseConfigErr).Msg("Can not parse the configuration file.") 34 | 35 | return 36 | } 37 | 38 | allConfigs := utils.AllConfigs{ 39 | ProvidersConfig: providersConfig, 40 | ProxyConfig: proxyConfig, 41 | CommandParameters: commandParameters, 42 | } 43 | 44 | ////////////////// //nolint:revive 45 | // MAIN SECTION // 46 | ////////////////// //nolint:revive 47 | // Impede ctrl+c 48 | sigtermChan := make(chan os.Signal, 1) 49 | 50 | signal.Notify(sigtermChan, os.Interrupt, syscall.SIGTERM) 51 | 52 | go func() { 53 | <-sigtermChan 54 | }() 55 | 56 | providersLst := providers.LoadProviders(context.Background(), &allConfigs) 57 | 58 | if len(providers.GetAllLaunchers(providersLst)) == 0 { 59 | utils.Logger.Error().Msg("No launcher is available.") 60 | 61 | return 62 | } 63 | 64 | defer providers.ClearProviders(providersLst) 65 | 66 | summarizeStateRunning := launchSummarizeStateTask(&allConfigs, providersLst) 67 | 68 | srv, srvListenAddress, srvErr := launchProxy(context.Background(), &allConfigs, providersLst) 69 | 70 | if srvErr != nil { 71 | utils.Logger.Error().Err(srvErr).Str("listenAddress", srvListenAddress).Msg("Can not create and launch the proxy.") 72 | 73 | return 74 | } 75 | 76 | ////////////////// //nolint:revive 77 | // STOP SECTION // 78 | ////////////////// //nolint:revive 79 | // Waits for Ctrl+c 80 | sigChan := make(chan os.Signal, 1) 81 | 82 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 83 | 84 | <-sigChan 85 | 86 | *summarizeStateRunning = false 87 | 88 | utils.Logger.Info().Msg("Stopping proxy ...") 89 | 90 | srvCloseErr := srv.Close() 91 | 92 | if srvCloseErr != nil { 93 | utils.Logger.Error().Err(srvCloseErr).Msg("An error happened while stopping the proxy.") 94 | } 95 | } 96 | 97 | // Parses the configuration file 98 | // 99 | //nolint:revive 100 | func parseConfig(configFilePath string) (utils.ProxyConfig, utils.ProvidersConfig, error) { 101 | cfg, err := ini.Load(configFilePath) 102 | 103 | if err != nil { 104 | return utils.ProxyConfig{}, utils.ProvidersConfig{}, errors.New("can not retrieve or parse the configuration file") 105 | } 106 | 107 | return utils.ProxyConfig{ 108 | PreloadHostsFile: cfg.Section("proxy").Key("preload_hosts_file").String(), 109 | WhitelistHostsFile: cfg.Section("proxy").Key("whitelist_hosts_file").String(), 110 | BlacklistHostsFile: cfg.Section("proxy").Key("blacklist_hosts_file").String(), 111 | CaCertFile: cfg.Section("proxy").Key("ca_cert_file").String(), 112 | CaCertKeyFile: cfg.Section("proxy").Key("ca_cert_key_file").String(), 113 | UserAgentsFile: cfg.Section("proxy").Key("user_agents_file").String(), 114 | DebugResponseHeaders: cfg.Section("proxy").Key("debug_response_headers").MustBool(true), 115 | WaitForLauncherAvailableTimeout: cfg.Section("proxy").Key("wait_for_launcher_available_timeout").MustInt(60), //nolint:gomnd 116 | }, utils.ProvidersConfig{ 117 | AWSRegions: cfg.Section("aws").Key("regions").Strings(","), 118 | AWSProfile: cfg.Section("aws").Key("profile").String(), 119 | AWSAccessKey: cfg.Section("aws").Key("access_key").String(), 120 | AWSSecretKey: cfg.Section("aws").Key("secret_key").String(), 121 | AWSSessionToken: cfg.Section("aws").Key("session_token").String(), 122 | AWSAGEnabled: cfg.Section("aws").Key("ag_enabled").MustBool(false), 123 | AWSAGMaxInstances: cfg.Section("aws").Key("ag_max_instances").MustInt(5), //nolint:gomnd 124 | AWSAGRotateNbRequests: cfg.Section("aws").Key("ag_rotate_nb_requests").MustInt(5000), //nolint:gomnd 125 | AWSAGForwardedForRange: cfg.Section("aws").Key("ag_forwarded_for_range").MustString("35.180.0.0/16"), 126 | AWSAGInstanceTitlePrefix: cfg.Section("aws").Key("ag_instance_title_prefix").MustString(utils.GenerateRandomSentence(1)), 127 | AWSAGInstanceDeploymentDescription: cfg.Section("aws").Key("ag_instance_deployment_description").MustString(utils.GenerateRandomSentence(3)), 128 | AWSAGInstanceDeploymentStageDescription: cfg.Section("aws").Key("ag_instance_deployment_stage_description").MustString(utils.GenerateRandomSentence(3)), 129 | AWSAGInstanceDeploymentStageName: cfg.Section("aws").Key("ag_instance_deployment_stage_name").MustString(utils.GenerateRandomPrefix(10)), //nolint:gomnd 130 | GitHubUsername: cfg.Section("github").Key("username").String(), 131 | GitHubToken: cfg.Section("github").Key("token").String(), 132 | GitHubGAEnabled: cfg.Section("github").Key("ga_enabled").MustBool(false), 133 | AzureAdminEmail: cfg.Section("azure").Key("admin_email").String(), 134 | AzureAdminPassword: cfg.Section("azure").Key("admin_password").String(), 135 | AzureTenantID: cfg.Section("azure").Key("tenant_id").String(), 136 | AzureSubscriptionID: cfg.Section("azure").Key("subscription_id").String(), 137 | AzureAccountsFile: cfg.Section("azure").Key("accounts_file").String(), 138 | AzureCSEnabled: cfg.Section("azure").Key("cs_enabled").MustBool(false), 139 | AzureCSPreferredLocations: cfg.Section("azure").Key("cs_preferred_locations").Strings(","), 140 | AzureCSNbInstances: cfg.Section("azure").Key("cs_nb_instances").MustInt(5), //nolint:gomnd 141 | }, nil 142 | } 143 | 144 | // Parses the command-line arguments 145 | func parseFlags() utils.CommandParameters { 146 | var ( 147 | host string 148 | port int 149 | verbose1 bool 150 | verbose2 bool 151 | verbose3 bool 152 | exportCaCert bool 153 | configINIPath string 154 | ) 155 | 156 | flag.StringVar(&host, "host", "127.0.0.1", "Proxy host") 157 | flag.IntVar(&port, "port", 8080, "Proxy port") //nolint:revive,gomnd 158 | flag.BoolVar(&verbose1, "v", false, "Verbose mode") 159 | flag.BoolVar(&verbose2, "vv", false, "Very verbose mode") 160 | flag.BoolVar(&verbose3, "vvv", false, "Very very verbose mode") 161 | flag.BoolVar(&exportCaCert, "export-ca-cert", false, "Export CA cert and key") 162 | flag.StringVar(&configINIPath, "config", "config.ini", "Config INI file path (check zad for file format)") 163 | 164 | flag.Parse() 165 | 166 | return utils.CommandParameters{ 167 | Host: host, 168 | Port: port, 169 | Verbose1: verbose1 || verbose2 || verbose3, 170 | Verbose2: verbose2 || verbose3, 171 | Verbose3: verbose3, 172 | ExportCaCert: exportCaCert, 173 | ConfigINIPath: configINIPath, 174 | } 175 | } 176 | 177 | // Launches the summarize task and returns a bool pointer to stop it 178 | func launchSummarizeStateTask(allConfigs *utils.AllConfigs, providersLst []utils.Provider) *bool { 179 | var running bool 180 | 181 | if allConfigs.CommandParameters.Verbose1 || allConfigs.CommandParameters.Verbose2 || allConfigs.CommandParameters.Verbose3 { 182 | running = true 183 | 184 | go func() { 185 | for running { 186 | time.Sleep(utils.SummarizeStateInterval * time.Second) 187 | 188 | if running { // If it has been stopped during the time.sleep 189 | for _, provider := range providersLst { 190 | utils.Logger.Debug().Str("provider", provider.GetName()).Msg(provider.SummarizeState()) 191 | 192 | for _, launcher := range provider.GetLaunchers() { 193 | utils.Logger.Debug().Str("provider", provider.GetName()).Str("launcher", launcher.GetName()).Msg(" - " + launcher.SummarizeState()) 194 | } 195 | } 196 | } 197 | } 198 | }() 199 | } 200 | 201 | return &running 202 | } 203 | 204 | // Launches the proxy with the given configuration and providers 205 | func launchProxy(ctx context.Context, allConfigs *utils.AllConfigs, providersLst []utils.Provider) (*http.Server, string, error) { 206 | prxy, proxyErr := proxy.CreateProxy(ctx, allConfigs, providersLst) 207 | 208 | host := allConfigs.CommandParameters.Host 209 | 210 | port := allConfigs.CommandParameters.Port 211 | 212 | if port == 0 { 213 | port = 8080 //nolint:revive 214 | } 215 | 216 | listenAddress := fmt.Sprintf("%s:%d", host, port) 217 | 218 | if proxyErr != nil { 219 | return nil, listenAddress, proxyErr 220 | } 221 | 222 | srv := http.Server{Addr: listenAddress, Handler: prxy} //nolint:gosec 223 | 224 | // Starts the Proxy server 225 | go func() { 226 | utils.Logger.Info().Str("listenAddress", listenAddress).Msg("Proxy is running.") 227 | 228 | err := srv.ListenAndServe() 229 | 230 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 231 | utils.Logger.Error().Str("listenAddress", listenAddress).Err(err).Msg("An error happened while launching the proxy.") 232 | 233 | providers.ClearProviders(providersLst) 234 | 235 | os.Exit(1) //nolint:revive 236 | } 237 | }() 238 | 239 | return &srv, listenAddress, nil 240 | } 241 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "io" 10 | "ipspinner/providers" 11 | "ipspinner/utils" 12 | "net/http" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/elazarl/goproxy" 17 | ) 18 | 19 | // Setups the proxy certificate authority 20 | func setupProxyCA(caCert, caKey []byte) error { 21 | goproxyCa, err := tls.X509KeyPair(caCert, caKey) 22 | 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if goproxyCa.Leaf, err = x509.ParseCertificate(goproxyCa.Certificate[0]); err != nil { 28 | return err 29 | } 30 | 31 | goproxy.GoproxyCa = goproxyCa 32 | goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 33 | goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 34 | goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 35 | goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 36 | 37 | return nil 38 | } 39 | 40 | // Retrieves or generates CA certificates for the proxy 41 | func prepareProxyCertificates(allConfigs *utils.AllConfigs) error { 42 | var caCert []byte 43 | 44 | var caKey []byte 45 | 46 | // If no CA certificate and key have been given 47 | if allConfigs.ProxyConfig.CaCertFile == "" || allConfigs.ProxyConfig.CaCertKeyFile == "" { 48 | var caErr error 49 | 50 | // It generates a random self-signed CA certificate and its key 51 | utils.Logger.Info().Msg("Generating a new CA certificate.") 52 | 53 | caCert, caKey, caErr = utils.GenerateRSACACertificate() 54 | 55 | if caErr != nil { 56 | return caErr 57 | } 58 | } else { // Otherwise, it tries to read the provided certicate and key 59 | var caCertErr error 60 | var caKeyErr error 61 | 62 | utils.Logger.Info().Str("caCertPath", allConfigs.ProxyConfig.CaCertFile).Str("caCertKeyPath", allConfigs.ProxyConfig.CaCertKeyFile).Msg("Retrieving the provided CA certificate.") 63 | 64 | caCert, caCertErr = utils.ReadFileContent(allConfigs.ProxyConfig.CaCertFile) 65 | 66 | if caCertErr != nil { 67 | return caCertErr 68 | } 69 | 70 | caKey, caKeyErr = utils.ReadFileContent(allConfigs.ProxyConfig.CaCertKeyFile) 71 | 72 | if caKeyErr != nil { 73 | return caKeyErr 74 | } 75 | } 76 | 77 | // It setups the certificate and key on the proxy 78 | err := setupProxyCA(caCert, caKey) 79 | 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // If the user has asked for exporting the certificate and the key 85 | if allConfigs.CommandParameters.ExportCaCert { 86 | exportedCertName := "ipspinner-ca-cert.pem" 87 | exportedCertKeyName := "ipspinner-ca-cert-key.pem" 88 | 89 | utils.Logger.Info().Str("exportedCaCertPath", exportedCertName).Str("exportedCaCertKeyPath", exportedCertKeyName).Msg("Exporting the CA certificate.") 90 | 91 | caCertErr := utils.WriteFileContent(exportedCertName, caCert) 92 | 93 | if caCertErr != nil { 94 | return caCertErr 95 | } 96 | 97 | caCertKeyErr := utils.WriteFileContent(exportedCertKeyName, caKey) 98 | 99 | if caCertKeyErr != nil { 100 | return caCertKeyErr 101 | } 102 | } 103 | 104 | return nil 105 | } 106 | 107 | // Returns the OnRequest handler 108 | func generateOnRequestHandler(ctx context.Context, providersLst []utils.Provider, allConfigs *utils.AllConfigs) func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 109 | requestCount := 0 110 | 111 | whitelistHosts := utils.ParseHostsFile(allConfigs.ProxyConfig.WhitelistHostsFile) 112 | blacklistHosts := utils.ParseHostsFile(allConfigs.ProxyConfig.BlacklistHostsFile) 113 | 114 | if len(whitelistHosts) > 0 { 115 | utils.Logger.Info().Int("nbHostsInWhitelist", len(whitelistHosts)).Msg("Hosts mentioned in the whitelist have been loaded.") 116 | 117 | if len(blacklistHosts) > 0 { 118 | utils.Logger.Warn().Msg("The blacklist has been ignored because a whitelist has been given.") 119 | } 120 | } else if len(blacklistHosts) > 0 { 121 | utils.Logger.Info().Int("nbHostsInBlacklist", len(blacklistHosts)).Msg("Hosts mentioned in the blacklist have been loaded.") 122 | } 123 | 124 | userAgents := []string{} 125 | 126 | if len(allConfigs.ProxyConfig.UserAgentsFile) > 0 { 127 | userAgentsLines, userAgentsErr := utils.ReadFileLines(allConfigs.ProxyConfig.UserAgentsFile) 128 | 129 | if userAgentsErr != nil { 130 | utils.Logger.Warn().Err(userAgentsErr).Msg("Can not read user agents file.") 131 | } 132 | 133 | userAgents = userAgentsLines 134 | } 135 | 136 | return func(req *http.Request, proxyCtx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 137 | if req == nil || req.URL == nil { 138 | return req, nil 139 | } 140 | 141 | // If the whitelist is set 142 | if len(whitelistHosts) > 0 { 143 | if !utils.DoesURLListContainsBaseURL(whitelistHosts, req.URL) { // Checks if the request URL is not in the whitelist 144 | utils.Logger.Warn().Str("host", req.URL.String()).Msg("Can not send a request to this host because it is not mentioned in the whitelist.") 145 | 146 | return nil, goproxy.NewResponse(req, 147 | goproxy.ContentTypeText, 148 | http.StatusBadGateway, 149 | "PROXY ERROR: Can not send a request to this host because it is not mentioned in the whitelist.") 150 | } 151 | } else if len(blacklistHosts) > 0 { // if the blacklist is set 152 | if utils.DoesURLListContainsBaseURL(blacklistHosts, req.URL) { // Checks if the request URL is in the blacklist 153 | utils.Logger.Warn().Str("host", req.URL.String()).Msg("Can not send a request to this host because it is mentioned in the blacklist.") 154 | 155 | return nil, goproxy.NewResponse(req, 156 | goproxy.ContentTypeText, 157 | http.StatusBadGateway, 158 | "PROXY ERROR: Can not send a request to this host because it is mentioned in the blacklist.") 159 | } 160 | } 161 | 162 | // Waits for an available launcher 163 | var maxRep = 1000 / 100 * allConfigs.ProxyConfig.WaitForLauncherAvailableTimeout // max WaitForLauncherAvailable seconds 164 | 165 | nbRep := 0 166 | 167 | for len(providers.GetAllAvailableLaunchers(providersLst)) == 0 { 168 | nbRep++ 169 | 170 | if nbRep >= maxRep { 171 | return nil, goproxy.NewResponse(req, 172 | goproxy.ContentTypeText, 173 | http.StatusBadGateway, 174 | "PROXY ERROR: Timeout - no launcher seem to be available.") 175 | } 176 | 177 | time.Sleep(100 * time.Millisecond) //nolint:gomnd 178 | } 179 | 180 | // It takes a random launcher among all available launchers 181 | launcher := utils.RandomElementInSlice(providers.GetAllAvailableLaunchers(providersLst)) 182 | 183 | headers := map[string]any{} 184 | 185 | for name, headersLst := range req.Header { 186 | for _, h := range headersLst { 187 | headers[name] = h 188 | } 189 | } 190 | 191 | if len(userAgents) > 0 { 192 | headers["User-Agent"] = utils.RandomElementInSlice(userAgents) 193 | } 194 | 195 | body, bodyErr := io.ReadAll(req.Body) 196 | 197 | if bodyErr != nil { 198 | utils.Logger.Error().Err(bodyErr).Msg("Error reading request body.") 199 | 200 | return nil, goproxy.NewResponse(req, 201 | goproxy.ContentTypeText, 202 | http.StatusBadGateway, 203 | "PROXY ERROR: Error reading request body: "+bodyErr.Error()) 204 | } 205 | 206 | respData, respHeaderCustom, respDataErr := (*launcher).SendRequest(ctx, utils.HTTPRequestData{ 207 | URL: req.URL, 208 | Method: req.Method, 209 | Headers: headers, 210 | Body: bytes.NewBuffer(body), 211 | }, allConfigs) 212 | 213 | if respDataErr != nil { 214 | utils.Logger.Error().Err(respDataErr).Msg("Error while processing request.") 215 | 216 | return nil, goproxy.NewResponse(req, 217 | goproxy.ContentTypeText, 218 | http.StatusBadGateway, 219 | "PROXY ERROR: Error while processing request: "+respDataErr.Error()) 220 | } 221 | 222 | resp := goproxy.NewResponse(req, 223 | utils.GetOrDefault(respData.Headers, "Content-Type", goproxy.ContentTypeText).(string), 224 | respData.StatusCode, 225 | string(respData.Body)) 226 | 227 | for headerName, headerVal := range respData.Headers { 228 | resp.Header.Set(headerName, headerVal.(string)) 229 | } 230 | 231 | if allConfigs.ProxyConfig.DebugResponseHeaders { 232 | resp.Header.Set(utils.IPSpinnerResponseHeaderPrefix+"Provider", (*launcher).GetProvider().GetName()) 233 | resp.Header.Set(utils.IPSpinnerResponseHeaderPrefix+"Launcher", (*launcher).GetName()) 234 | resp.Header.Set(utils.IPSpinnerResponseHeaderPrefix+"Provider-NbTotalReqSent", strconv.Itoa((*launcher).GetProvider().GetNbTotalReqSent())) 235 | resp.Header.Set(utils.IPSpinnerResponseHeaderPrefix+"Launcher-NbTotalReqSent", strconv.Itoa((*launcher).GetNbTotalReqSent())) 236 | 237 | if respHeaderCustom != "" { 238 | resp.Header.Set(utils.IPSpinnerResponseHeaderPrefix+"Launcher-Custom", respHeaderCustom) 239 | } 240 | } 241 | 242 | requestCount++ 243 | 244 | utils.Logger.Trace().Str("from", req.URL.String()).Str("to-provider", (*launcher).GetProvider().GetName()).Str("to-launcher", (*launcher).GetName()).Str("method", req.Method).Msg(fmt.Sprintf("Redirecting request #%d.", requestCount)) 245 | 246 | return req, resp 247 | } 248 | } 249 | 250 | // Creates a proxy instance 251 | func CreateProxy(ctx context.Context, allConfigs *utils.AllConfigs, providersLst []utils.Provider) (*goproxy.ProxyHttpServer, error) { 252 | prepareErr := prepareProxyCertificates(allConfigs) 253 | 254 | if prepareErr != nil { 255 | return nil, prepareErr 256 | } 257 | 258 | // It instantiates the goproxy instance 259 | proxy := goproxy.NewProxyHttpServer() 260 | 261 | proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) 262 | 263 | proxy.OnRequest().DoFunc(generateOnRequestHandler(ctx, providersLst, allConfigs)) //nolint:bodyclose 264 | 265 | proxy.Verbose = allConfigs.CommandParameters.Verbose3 266 | 267 | return proxy, nil 268 | } 269 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= 3 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= 4 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= 5 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= 6 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= 7 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= 8 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= 9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= 10 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= 11 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= 12 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= 13 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= 14 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 15 | github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= 16 | github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= 17 | github.com/aws/aws-sdk-go-v2/config v1.27.10 h1:PS+65jThT0T/snC5WjyfHHyUgG+eBoupSDV+f838cro= 18 | github.com/aws/aws-sdk-go-v2/config v1.27.10/go.mod h1:BePM7Vo4OBpHreKRUMuDXX+/+JWP38FLkzl5m27/Jjs= 19 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10 h1:qDZ3EA2lv1KangvQB6y258OssCHD0xvaGiEDkG4X/10= 20 | github.com/aws/aws-sdk-go-v2/credentials v1.17.10/go.mod h1:6t3sucOaYDwDssHQa0ojH1RpmVmF5/jArkye1b2FKMI= 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= 22 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= 24 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= 25 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 28 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 29 | github.com/aws/aws-sdk-go-v2/service/apigateway v1.23.6 h1:YZ4tYuH59Xd5q3bYmDqKXt8fQVJ19WPoq4lKzW1iLMg= 30 | github.com/aws/aws-sdk-go-v2/service/apigateway v1.23.6/go.mod h1:3h9BDpayKgNNrpHZBvL7gCIeikqiE7oBxGGcrzmtLAM= 31 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= 32 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= 33 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= 34 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= 35 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 h1:WzFol5Cd+yDxPAdnzTA5LmpHYSWinhmSj4rQChV0ee8= 36 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.4/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= 37 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE= 38 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= 39 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU= 40 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= 41 | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= 42 | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 43 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= 49 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 50 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= 51 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 52 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 53 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 54 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 55 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 56 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 57 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 58 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 59 | github.com/jefflinse/githubsecret v1.0.2 h1:eq9HVjLvljN7PdpdkttU+M7gtsehzW8pChHpcP3kP3E= 60 | github.com/jefflinse/githubsecret v1.0.2/go.mod h1:+5nqeYtLIZ6tdFwP1LD38A3JnvyHHcVPVUYvHVRZr9k= 61 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 62 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 63 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 64 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 65 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 66 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 67 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 68 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 69 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 70 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 71 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 72 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 73 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 74 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 75 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 78 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 79 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= 80 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 81 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 84 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 85 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 86 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 87 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 88 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 89 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 90 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 91 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 98 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 99 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 100 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 101 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 102 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 103 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 104 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 106 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 107 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 108 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 109 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 110 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 111 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 112 | -------------------------------------------------------------------------------- /providers/github/sdk.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "ipspinner/utils" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/jefflinse/githubsecret" 12 | ) 13 | 14 | type WorkflowRun struct { 15 | Name string 16 | Status string 17 | ID int64 18 | } 19 | 20 | type WorkflowJob struct { 21 | Name string 22 | WorkflowName string 23 | Status string 24 | ID int64 25 | RunID int64 26 | Steps []WorkflowJobStep 27 | } 28 | 29 | type WorkflowJobStep struct { 30 | Name string 31 | Status string 32 | } 33 | 34 | func (infos Infos) getHeaders() map[string]any { 35 | return map[string]any{ 36 | "Accept": infos.Accept, 37 | "X-GitHub-Api-Version": infos.APIVersion, 38 | "Authorization": fmt.Sprintf("Bearer %s", infos.Token), 39 | } 40 | } 41 | 42 | // Creates a repository with the provided information 43 | func CreateRepository(infos Infos, repositoryName string) error { 44 | reqBody := map[string]any{ 45 | "name": repositoryName, 46 | } 47 | 48 | githubURL, githubURLErr := url.Parse("https://api.github.com/user/repos") 49 | 50 | if githubURLErr != nil { 51 | return githubURLErr 52 | } 53 | 54 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 55 | URL: githubURL, 56 | Method: "POST", 57 | Headers: infos.getHeaders(), 58 | Body: reqBody, 59 | }) 60 | 61 | if respErr != nil { 62 | return respErr 63 | } 64 | 65 | if resp.StatusCode != http.StatusCreated { 66 | return errors.New(utils.GetOrDefault(resp.Body, "message", "Can not create the repository.").(string)) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // Adds a file to the repository (file content encoded in base64) 73 | func AddFileToRepository(infos Infos, repositoryName, path, fileContentB64, commitMessage string) error { 74 | reqBody := map[string]any{ 75 | "message": commitMessage, 76 | "content": fileContentB64, 77 | "private": true, 78 | } 79 | 80 | path = strings.TrimPrefix(path, "/") 81 | 82 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", infos.Username, repositoryName, path)) 83 | 84 | if githubURLErr != nil { 85 | return githubURLErr 86 | } 87 | 88 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 89 | URL: githubURL, 90 | Method: "PUT", 91 | Headers: infos.getHeaders(), 92 | Body: reqBody, 93 | }) 94 | 95 | if respErr != nil { 96 | return respErr 97 | } 98 | 99 | if resp.StatusCode != http.StatusCreated { 100 | return errors.New(utils.GetOrDefault(resp.Body, "message", "Can not add the file to the directory.").(string)) 101 | } 102 | 103 | return nil 104 | } 105 | 106 | // Dispatches the provided workflow 107 | func DispatchWorkflow(infos Infos, repositoryName string, inputs map[string]any) (string, error) { 108 | runIdentifier := utils.GenerateRandomPrefix(10) //nolint:revive,gomnd // used to retrieve the workflow ID later as it is launched asynchronously : https://stackoverflow.com/questions/69479400/get-run-id-after-triggering-a-github-workflow-dispatch-event 109 | 110 | inputs["runIdentifier"] = runIdentifier 111 | 112 | reqBody := map[string]any{ 113 | "ref": "main", 114 | "inputs": inputs, 115 | } 116 | 117 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/workflows/sprayer.yml/dispatches", infos.Username, repositoryName)) 118 | 119 | if githubURLErr != nil { 120 | return runIdentifier, githubURLErr 121 | } 122 | 123 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 124 | URL: githubURL, 125 | Method: "POST", 126 | Headers: infos.getHeaders(), 127 | Body: reqBody, 128 | }) 129 | 130 | if respErr != nil { 131 | return runIdentifier, respErr 132 | } 133 | 134 | if resp.StatusCode != http.StatusNoContent { 135 | return runIdentifier, errors.New(utils.GetOrDefault(resp.Body, "message", "Can not dispatch the workflow.").(string)) 136 | } 137 | 138 | return runIdentifier, nil 139 | } 140 | 141 | // Retrieves all workflow runs in the provided repository 142 | func GetWorkflowRuns(infos Infos, repositoryName string) ([]WorkflowRun, error) { 143 | allWorkflowRuns := []WorkflowRun{} 144 | 145 | perPage := 100 //nolint:revive 146 | page := 1 147 | maxPage := 1 148 | 149 | for page <= maxPage { 150 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/runs?page=%d&per_page=%d", infos.Username, repositoryName, page, perPage)) 151 | 152 | if githubURLErr != nil { 153 | return allWorkflowRuns, githubURLErr 154 | } 155 | 156 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 157 | URL: githubURL, 158 | Method: "GET", 159 | Headers: infos.getHeaders(), 160 | Body: nil, 161 | }) 162 | 163 | if respErr != nil { 164 | return allWorkflowRuns, respErr 165 | } 166 | 167 | if resp.StatusCode != http.StatusOK { 168 | return allWorkflowRuns, errors.New(utils.GetOrDefault(resp.Body, "message", "Can not list workflow runs.").(string)) 169 | } 170 | 171 | for _, rawWorkflowRun := range resp.Body["workflow_runs"].([]any) { 172 | allWorkflowRuns = append(allWorkflowRuns, WorkflowRun{ 173 | ID: int64(rawWorkflowRun.(map[string]any)["id"].(float64)), 174 | Name: rawWorkflowRun.(map[string]any)["name"].(string), 175 | Status: rawWorkflowRun.(map[string]any)["status"].(string), 176 | }) 177 | } 178 | 179 | maxPage = int(utils.GetOrDefault(resp.Body, "total_count", 1).(float64))/perPage + 1 180 | 181 | page++ 182 | } 183 | 184 | return allWorkflowRuns, nil 185 | } 186 | 187 | // Retrieves all workflow jobs in the provided repository / workflow 188 | func GetWorkflowJobs(infos Infos, repositoryName string, workflowID int64) ([]WorkflowJob, error) { 189 | allWorkflowJobs := []WorkflowJob{} 190 | 191 | perPage := 100 //nolint:revive 192 | page := 1 193 | maxPage := 1 194 | 195 | for page <= maxPage { 196 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/runs/%d/jobs?page=%d&per_page=%d", infos.Username, repositoryName, workflowID, page, perPage)) 197 | 198 | if githubURLErr != nil { 199 | return allWorkflowJobs, githubURLErr 200 | } 201 | 202 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 203 | URL: githubURL, 204 | Method: "GET", 205 | Headers: infos.getHeaders(), 206 | Body: nil, 207 | }) 208 | 209 | if respErr != nil { 210 | return allWorkflowJobs, respErr 211 | } 212 | 213 | if resp.StatusCode != http.StatusOK { 214 | return allWorkflowJobs, errors.New(utils.GetOrDefault(resp.Body, "message", "Can not list workflow jobs.").(string)) 215 | } 216 | 217 | for _, rawWorkflowJob := range resp.Body["jobs"].([]any) { 218 | allWorkflowJobSteps := []WorkflowJobStep{} 219 | 220 | for _, rawWorkflowJobStep := range rawWorkflowJob.(map[string]any)["steps"].([]any) { 221 | allWorkflowJobSteps = append(allWorkflowJobSteps, WorkflowJobStep{ 222 | Name: rawWorkflowJobStep.(map[string]any)["name"].(string), 223 | Status: rawWorkflowJobStep.(map[string]any)["status"].(string), 224 | }) 225 | } 226 | 227 | allWorkflowJobs = append(allWorkflowJobs, WorkflowJob{ 228 | ID: int64(rawWorkflowJob.(map[string]any)["id"].(float64)), 229 | RunID: int64(rawWorkflowJob.(map[string]any)["run_id"].(float64)), 230 | Name: rawWorkflowJob.(map[string]any)["name"].(string), 231 | Status: rawWorkflowJob.(map[string]any)["status"].(string), 232 | WorkflowName: rawWorkflowJob.(map[string]any)["name"].(string), 233 | Steps: allWorkflowJobSteps, 234 | }) 235 | } 236 | 237 | maxPage = int(utils.GetOrDefault(resp.Body, "total_count", 1).(float64))/perPage + 1 238 | 239 | page++ 240 | } 241 | 242 | return allWorkflowJobs, nil 243 | } 244 | 245 | // Retrieves the logs of the provided workflow job 246 | func GetWorkflowJobLogs(infos Infos, repositoryName string, jobID int64) ([]string, error) { 247 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/jobs/%d/logs", infos.Username, repositoryName, jobID)) 248 | 249 | if githubURLErr != nil { 250 | return []string{}, githubURLErr 251 | } 252 | 253 | resp, respErr := utils.SendRequest(utils.HTTPRequestData{ 254 | URL: githubURL, 255 | Method: "GET", 256 | Headers: infos.getHeaders(), 257 | FollowRedirections: true, 258 | Body: nil, 259 | }) 260 | 261 | if respErr != nil { 262 | return []string{}, respErr 263 | } 264 | 265 | if resp.StatusCode != http.StatusOK { 266 | return []string{}, errors.New(string(resp.Body)) 267 | } 268 | 269 | return strings.Split(string(resp.Body), "\n"), nil 270 | } 271 | 272 | // Deletes the provided workflow run 273 | func DeleteWorkflowRun(infos Infos, repositoryName string, runID int64) error { 274 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/runs/%d", infos.Username, repositoryName, runID)) 275 | 276 | if githubURLErr != nil { 277 | return githubURLErr 278 | } 279 | 280 | resp, respErr := utils.SendRequest(utils.HTTPRequestData{ 281 | URL: githubURL, 282 | Method: "DELETE", 283 | Headers: infos.getHeaders(), 284 | Body: nil, 285 | }) 286 | 287 | if respErr != nil { 288 | return respErr 289 | } 290 | 291 | if resp.StatusCode != http.StatusNoContent { 292 | return errors.New(string(resp.Body)) 293 | } 294 | 295 | return nil 296 | } 297 | 298 | // Deletes the provided repository 299 | func DeleteRepository(infos Infos, repositoryName string) error { 300 | githubURL, githubURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s", infos.Username, repositoryName)) 301 | 302 | if githubURLErr != nil { 303 | return githubURLErr 304 | } 305 | 306 | resp, respErr := utils.SendRequest(utils.HTTPRequestData{ 307 | URL: githubURL, 308 | Method: "DELETE", 309 | Headers: infos.getHeaders(), 310 | Body: nil, 311 | }) 312 | 313 | if respErr != nil { 314 | return respErr 315 | } 316 | 317 | if resp.StatusCode != http.StatusNoContent { 318 | return fmt.Errorf("can not delete the repository, status code: %d", resp.StatusCode) 319 | } 320 | 321 | return nil 322 | } 323 | 324 | // Creates or updates a GitHub repository secret 325 | func CreateOrUpdateRepositorySecret(infos Infos, repositoryName, secretName, secretValue string) error { 326 | // First, it retrieves the repository public key 327 | githubPublicKeyURL, githubPublicKeyURLErr := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/secrets/public-key", infos.Username, repositoryName)) 328 | 329 | if githubPublicKeyURLErr != nil { 330 | return githubPublicKeyURLErr 331 | } 332 | 333 | respPublicKey, respPublicKeyErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 334 | URL: githubPublicKeyURL, 335 | Method: "GET", 336 | Headers: infos.getHeaders(), 337 | Body: nil, 338 | }) 339 | 340 | if respPublicKeyErr != nil { 341 | return respPublicKeyErr 342 | } 343 | 344 | if respPublicKey.StatusCode != http.StatusOK { 345 | return errors.New(utils.GetOrDefault(respPublicKey.Body, "message", "Can not retrieve the repository public key (1).").(string)) 346 | } 347 | 348 | key, keyOk := respPublicKey.Body["key"].(string) 349 | keyID, keyIDOk := respPublicKey.Body["key_id"].(string) 350 | 351 | if !keyOk || !keyIDOk { 352 | return errors.New(utils.GetOrDefault(respPublicKey.Body, "message", "Can not retrieve the repository public key (2).").(string)) 353 | } 354 | 355 | secretEncrypted, secretEncryptedErr := githubsecret.Encrypt(key, secretValue) 356 | 357 | if secretEncryptedErr != nil { 358 | return secretEncryptedErr 359 | } 360 | 361 | githubCreateOrUpdateURL, githubCreateOrUpdate := url.Parse(fmt.Sprintf("https://api.github.com/repos/%s/%s/actions/secrets/%s", infos.Username, repositoryName, secretName)) 362 | 363 | if githubCreateOrUpdate != nil { 364 | return githubCreateOrUpdate 365 | } 366 | 367 | reqCreateOrUpdateBody := map[string]any{ 368 | "encrypted_value": secretEncrypted, 369 | "key_id": keyID, 370 | } 371 | 372 | respCreateOrUpdate, respCreateOrUpdateErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 373 | URL: githubCreateOrUpdateURL, 374 | Method: "PUT", 375 | Headers: infos.getHeaders(), 376 | Body: reqCreateOrUpdateBody, 377 | }) 378 | 379 | if respCreateOrUpdateErr != nil { 380 | return respCreateOrUpdateErr 381 | } 382 | 383 | if respCreateOrUpdate.StatusCode != http.StatusCreated { 384 | return fmt.Errorf("can not update repository secrets, status code: %d", respCreateOrUpdate.StatusCode) 385 | } 386 | 387 | return nil 388 | } 389 | -------------------------------------------------------------------------------- /providers/github/repository.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "ipspinner/utils" 9 | "net/url" 10 | "slices" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type Repository struct { 17 | provider *GitHub 18 | sendingRequests map[string]*Worker 19 | name string 20 | aesKeyHex string 21 | nbTotalRequestsSent int 22 | stopped bool 23 | } 24 | 25 | type Worker struct { 26 | closed bool 27 | channel chan WorkerResponse 28 | } 29 | 30 | type WorkerResponse struct { 31 | ResponseData utils.HTTPResponseData 32 | Error error 33 | } 34 | 35 | func CreateRepositoryLauncher(provider *GitHub) (*Repository, error) { 36 | instance := Repository{ 37 | name: strings.ReplaceAll(utils.GenerateRandomSentence(3), " ", "-"), 38 | provider: provider, 39 | nbTotalRequestsSent: 0, 40 | sendingRequests: map[string]*Worker{}, 41 | stopped: false, 42 | aesKeyHex: "", 43 | } 44 | 45 | utils.Logger.Info().Str("launcher", instance.GetName()).Msg("Creating launcher.") 46 | 47 | aesKey, aesKeyErr := utils.Aes256GenerateKey() 48 | 49 | if aesKeyErr != nil { 50 | return &instance, aesKeyErr 51 | } 52 | 53 | instance.aesKeyHex = aesKey 54 | 55 | // Creates the github repository 56 | createErr, addErr := instance.PrepareSprayerRepository() 57 | 58 | if createErr != nil { 59 | return &instance, createErr 60 | } 61 | 62 | // If the repository has been successfully created but the files could not have been added => it deletes the repository 63 | if addErr != nil { 64 | deleteErr := DeleteRepository(instance.provider.GetInfos(), instance.GetName()) 65 | 66 | if deleteErr != nil { 67 | utils.Logger.Error().Err(deleteErr).Str("repositoryName", instance.GetName()).Msg("Cannot delete the previously created repository.") 68 | } 69 | 70 | return &instance, addErr 71 | } 72 | 73 | // Launches the go routine for constantly reading workflow runs 74 | go instance.LoopWorkflows() 75 | 76 | return &instance, nil 77 | } 78 | 79 | func (instance Repository) GetName() string { 80 | return instance.name 81 | } 82 | 83 | func (instance *Repository) GetProvider() utils.Provider { 84 | return instance.provider 85 | } 86 | 87 | func (instance Repository) GetNbTotalReqSent() int { 88 | return instance.nbTotalRequestsSent 89 | } 90 | 91 | func (instance Repository) SummarizeState() string { 92 | return fmt.Sprintf("Launcher %s : nbTotalRequestsSent=%d, name=%s", instance.GetName(), instance.GetNbTotalReqSent(), instance.GetName()) 93 | } 94 | 95 | //nolint:revive 96 | func (instance *Repository) PreloadHosts(ctx context.Context, allPreloadHosts []*url.URL) { 97 | utils.Logger.Info().Str("provider", instance.GetName()).Msg("This provider cannot preload hosts.") 98 | } 99 | 100 | func (instance *Repository) SendRequest(ctx context.Context, reqData utils.HTTPRequestData, allConfigs *utils.AllConfigs) (utils.HTTPResponseData, string, error) { //nolint:revive 101 | headersStr := "" 102 | 103 | if reqData.Headers != nil { 104 | for headerKey, headerVal := range reqData.Headers { 105 | headersStr += headerKey + "\n" + fmt.Sprintf("%v", headerVal) + "\n" 106 | } 107 | 108 | if len(headersStr) > 0 { 109 | headersStr = headersStr[:(len(headersStr) - 1)] 110 | } 111 | } 112 | 113 | bodyStr := "" 114 | 115 | if reqData.Body != nil { 116 | bodyStr = reqData.Body.String() 117 | } 118 | 119 | encryptedMethod, encryptedMethodErr := utils.Aes256Encrypt(reqData.Method, instance.aesKeyHex) 120 | if encryptedMethodErr != nil { 121 | return utils.HTTPResponseData{}, "", encryptedMethodErr 122 | } 123 | 124 | encryptedURL, encryptedURLErr := utils.Aes256Encrypt(reqData.URL.String(), instance.aesKeyHex) 125 | if encryptedURLErr != nil { 126 | return utils.HTTPResponseData{}, "", encryptedURLErr 127 | } 128 | 129 | encryptedHeaders, encryptedHeadersErr := utils.Aes256Encrypt(headersStr, instance.aesKeyHex) 130 | if encryptedHeadersErr != nil { 131 | return utils.HTTPResponseData{}, "", encryptedHeadersErr 132 | } 133 | 134 | encryptedBody, encryptedBodyErr := utils.Aes256Encrypt(bodyStr, instance.aesKeyHex) 135 | if encryptedBodyErr != nil { 136 | return utils.HTTPResponseData{}, "", encryptedBodyErr 137 | } 138 | 139 | runIdentifier, dispatchErr := DispatchWorkflow( 140 | instance.provider.GetInfos(), 141 | instance.GetName(), 142 | map[string]any{ 143 | "methodEnc": encryptedMethod, 144 | "urlEnc": encryptedURL, 145 | "headersEnc": encryptedHeaders, 146 | "bodyEnc": encryptedBody, 147 | }) 148 | 149 | if dispatchErr != nil { 150 | return utils.HTTPResponseData{}, "", dispatchErr 151 | } 152 | 153 | worker := CreateWorker() 154 | 155 | instance.sendingRequests[runIdentifier] = &worker 156 | 157 | instance.nbTotalRequestsSent++ 158 | 159 | githubResponse := <-worker.GetChannel() 160 | 161 | worker.Close() 162 | 163 | return githubResponse.ResponseData, fmt.Sprintf("runIdentifier=%s", runIdentifier), githubResponse.Error 164 | } 165 | 166 | //nolint:revive 167 | func (instance *Repository) IsAvailable() bool { 168 | return true 169 | } 170 | 171 | func (instance *Repository) IsStopped() bool { 172 | return instance.stopped 173 | } 174 | 175 | func (instance *Repository) Clear() bool { 176 | utils.Logger.Info().Str("launcher", instance.GetName()).Msg("Clearing launcher.") 177 | 178 | deleteErr := DeleteRepository(instance.provider.GetInfos(), instance.GetName()) 179 | 180 | utils.Logger.Debug().Str("repositoryName", instance.GetName()).Msg("Deleting GitHub repository.") 181 | 182 | if deleteErr != nil { 183 | utils.Logger.Error().Err(deleteErr).Str("repositoryName", instance.GetName()).Msg("Error while deleting GitHub repository.") 184 | 185 | return false 186 | } 187 | 188 | instance.stopped = true 189 | 190 | return true 191 | } 192 | 193 | // This function is used to launch a loop to constantly read workflow runs and tries to retrieve its log for the responses' data 194 | func (instance *Repository) LoopWorkflows() { 195 | for !instance.stopped { 196 | // Large time to avoid reaching the GitHub API rate limit (5000 req/h) when there are no waiting requests 197 | time.Sleep(time.Duration(max(5, 10/(len(instance.sendingRequests)+1))) * time.Second) //nolint:gomnd 198 | 199 | runs, runsErr := GetWorkflowRuns(instance.provider.GetInfos(), instance.GetName()) 200 | 201 | if runsErr != nil { 202 | utils.Logger.Warn().Err(runsErr).Msg("Can not retrieve workflow runs.") 203 | 204 | continue 205 | } 206 | 207 | // For each run, it retrieves its jobs 208 | for _, run := range runs { 209 | if !slices.Contains([]string{"completed", "cancelled", "failure", "skipped", "success"}, run.Status) { 210 | continue 211 | } 212 | 213 | jobs, jobsErr := GetWorkflowJobs(instance.provider.GetInfos(), instance.GetName(), run.ID) 214 | 215 | if jobsErr != nil { 216 | utils.Logger.Warn().Err(jobsErr).Int64("runId", run.ID).Msg("Can not retrieve workflow jobs.") 217 | 218 | continue 219 | } 220 | 221 | var workflowWorker *Worker 222 | 223 | var runIdentifier string 224 | 225 | // It tries to identify which job corresponds to which request 226 | for _, job := range jobs { 227 | for _, step := range job.Steps { 228 | // If the step name (which will be in the Workflow ID Provider) matches one sending request => it retrieves the corresponding chanel (on which the proxy is waiting for this request) 229 | if currentworker, ok := instance.sendingRequests[step.Name]; ok { 230 | workflowWorker = currentworker 231 | runIdentifier = step.Name 232 | 233 | break 234 | } 235 | } 236 | 237 | if workflowWorker != nil { 238 | break 239 | } 240 | } 241 | 242 | // If the chanel has been found (the workflow has been identified) 243 | if workflowWorker != nil { 244 | for _, job := range jobs { 245 | if job.Name == "Request" { 246 | // It checks if the request job has finished 247 | if !slices.Contains([]string{"in_progress", "queued"}, job.Status) { 248 | // It starts in a go routine the response data extraction 249 | go func(jobID int64, runID int64, currentWorker *Worker) { 250 | githubResponse := instance.ExtractResponseDataFromJobLogs(jobID) 251 | 252 | delete(instance.sendingRequests, runIdentifier) 253 | 254 | if !currentWorker.IsClosed() { 255 | currentWorker.GetChannel() <- githubResponse 256 | } else { 257 | utils.Logger.Warn().Int64("runId", runID).Msg("The worker channel is closed (the client may have terminated the connection).") 258 | } 259 | 260 | deleteErr := DeleteWorkflowRun(instance.provider.GetInfos(), instance.GetName(), runID) 261 | 262 | if deleteErr != nil { 263 | utils.Logger.Warn().Err(deleteErr).Int64("runId", runID).Msg("Cannot delete the workflow run.") 264 | } 265 | }(job.ID, run.ID, workflowWorker) 266 | 267 | break 268 | } 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | func (instance *Repository) PrepareSprayerRepository() (create, other error) { 277 | createErr := CreateRepository(instance.provider.GetInfos(), instance.GetName()) 278 | 279 | if createErr != nil { 280 | return createErr, nil 281 | } 282 | 283 | addAesKeyErr := CreateOrUpdateRepositorySecret(instance.provider.GetInfos(), instance.GetName(), "AES256_KEY_HEX", instance.aesKeyHex) 284 | 285 | if addAesKeyErr != nil { 286 | return nil, addAesKeyErr 287 | } 288 | 289 | addErr1 := AddFileToRepository(instance.provider.GetInfos(), instance.GetName(), "sprayer.py", SPRAYER_PY_FILE_BASE64, "add sprayer.yml") 290 | 291 | if addErr1 != nil { 292 | return nil, addErr1 293 | } 294 | 295 | addErr2 := AddFileToRepository(instance.provider.GetInfos(), instance.GetName(), "requirements.txt", REQUIREMENTS_TXT_FILE_BASE64, "add requirements.txt") 296 | 297 | if addErr2 != nil { 298 | return nil, addErr2 299 | } 300 | 301 | addErr3 := AddFileToRepository(instance.provider.GetInfos(), instance.GetName(), ".github/workflows/sprayer.yml", SPRAYER_YML_FILE_BASE64, "add sprayer.yml") 302 | 303 | if addErr3 != nil { 304 | return nil, addErr3 305 | } 306 | 307 | return nil, nil 308 | } 309 | 310 | func (instance *Repository) ExtractResponseDataFromJobLogs(jobID int64) WorkerResponse { 311 | logs, logsErr := GetWorkflowJobLogs(instance.provider.GetInfos(), instance.GetName(), jobID) 312 | 313 | if logsErr != nil { 314 | return WorkerResponse{ 315 | Error: logsErr, 316 | } 317 | } 318 | 319 | const RESP_ERROR_PREFIX = "RESP_ERR" //nolint:revive,stylecheck //uppercase more readable here 320 | 321 | const RESP_STATUS_PREFIX = "RESP_STATUS_ENCRYPTED_HEX" //nolint:revive,stylecheck //uppercase more readable here 322 | 323 | const RESP_HEADERS_PREFIX = "RESP_HEADERS_ENCRYPTED_HEX" //nolint:revive,stylecheck //uppercase more readable here 324 | 325 | const RESP_BODY_PREFIX = "RESP_BODY_ENCRYPTED_HEX" //nolint:revive,stylecheck //uppercase more readable here 326 | 327 | responseData := utils.HTTPResponseData{ 328 | StatusCode: -1, 329 | Headers: nil, 330 | Body: nil, 331 | } 332 | 333 | var statusCodeEncrStr *string 334 | 335 | var headersEncrStr *string 336 | 337 | var bodyEncrStr *string 338 | 339 | for _, log := range logs { 340 | logInfos := strings.Split(log, " ") 341 | 342 | if len(logInfos) < 3 { 343 | continue 344 | } 345 | 346 | if !slices.Contains([]string{RESP_STATUS_PREFIX, RESP_BODY_PREFIX, RESP_HEADERS_PREFIX, RESP_ERROR_PREFIX}, logInfos[1]) { 347 | continue 348 | } 349 | 350 | if logInfos[1] == RESP_ERROR_PREFIX { 351 | if len(logInfos) == 2 { 352 | return WorkerResponse{ 353 | Error: errors.New("an error happened while treating request"), 354 | } 355 | } 356 | 357 | decodedBytes, decodedBytesErr := base64.StdEncoding.DecodeString(logInfos[2]) 358 | 359 | if decodedBytesErr != nil { 360 | return WorkerResponse{ 361 | Error: decodedBytesErr, 362 | } 363 | } 364 | 365 | return WorkerResponse{ 366 | Error: errors.New(string(decodedBytes)), 367 | } 368 | } 369 | 370 | switch logInfos[1] { 371 | case RESP_STATUS_PREFIX: 372 | statusCodeEncrStr = &logInfos[2] 373 | case RESP_HEADERS_PREFIX: 374 | value := logInfos[2] 375 | 376 | if headersEncrStr != nil { 377 | value = *headersEncrStr + value 378 | } 379 | 380 | headersEncrStr = &value 381 | case RESP_BODY_PREFIX: 382 | value := logInfos[2] 383 | 384 | if bodyEncrStr != nil { 385 | value = *bodyEncrStr + value 386 | } 387 | 388 | bodyEncrStr = &value 389 | } 390 | } 391 | 392 | if statusCodeEncrStr != nil { 393 | data, dataErr := utils.Aes256Decrypt(*statusCodeEncrStr, instance.aesKeyHex) 394 | 395 | if dataErr != nil { 396 | return WorkerResponse{ 397 | Error: dataErr, 398 | } 399 | } 400 | 401 | decodedInt, decodedIntErr := strconv.Atoi(data) 402 | 403 | if decodedIntErr != nil { 404 | return WorkerResponse{ 405 | Error: decodedIntErr, 406 | } 407 | } 408 | 409 | responseData.StatusCode = decodedInt 410 | } 411 | 412 | if headersEncrStr != nil { 413 | data, dataErr := utils.Aes256Decrypt(*headersEncrStr, instance.aesKeyHex) 414 | 415 | if dataErr != nil { 416 | return WorkerResponse{ 417 | Error: dataErr, 418 | } 419 | } 420 | 421 | lines := strings.Split(data, "\n") 422 | dataMap := make(map[string]any) 423 | 424 | for i := 0; i < len(lines); i += 2 { 425 | if i+1 < len(lines) { 426 | key := lines[i] 427 | value := lines[i+1] 428 | dataMap[key] = value 429 | } 430 | } 431 | 432 | responseData.Headers = dataMap 433 | } 434 | 435 | if bodyEncrStr != nil { 436 | data, dataErr := utils.Aes256Decrypt(*bodyEncrStr, instance.aesKeyHex) 437 | 438 | if dataErr != nil { 439 | return WorkerResponse{ 440 | Error: dataErr, 441 | } 442 | } 443 | 444 | responseData.Body = []byte(data) 445 | } 446 | 447 | if responseData.StatusCode == -1 { 448 | return WorkerResponse{ 449 | Error: errors.New("no response found in the job logs"), 450 | } 451 | } 452 | 453 | return WorkerResponse{ 454 | Error: nil, 455 | ResponseData: responseData, 456 | } 457 | } 458 | 459 | func CreateWorker() Worker { 460 | return Worker{ 461 | channel: make(chan WorkerResponse), 462 | closed: false, 463 | } 464 | } 465 | 466 | func (instance *Worker) Close() { 467 | instance.closed = true 468 | } 469 | 470 | func (instance *Worker) IsClosed() bool { 471 | return instance.closed || instance.channel == nil 472 | } 473 | 474 | func (instance *Worker) GetChannel() chan WorkerResponse { 475 | return instance.channel 476 | } 477 | -------------------------------------------------------------------------------- /providers/azure/account.go: -------------------------------------------------------------------------------- 1 | package azure 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "ipspinner/utils" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" 13 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 14 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 15 | ) 16 | 17 | type Account struct { 18 | username string 19 | ID string 20 | userPrincipalName string 21 | tenantID string 22 | tokenCredential *azcore.TokenCredential 23 | createdRoleAssignments []AccountRoleAssignment 24 | needToBeCleared bool 25 | } 26 | 27 | type AccountRoleAssignment struct { 28 | subscriptionID string 29 | roleAssignmentID string 30 | } 31 | 32 | // ID and userPrincipalName can be empty 33 | func ConnectAccount(email, password, tenantID string, needToBeCleared bool, id, userPrincipalName string) (Account, error) { 34 | var cred azcore.TokenCredential 35 | 36 | cred, credsErr := getAzIdentityFromNewUsernamePasswordCredential(email, password, tenantID) 37 | 38 | if credsErr != nil { 39 | return Account{}, credsErr 40 | } 41 | 42 | username := strings.Split(email, "@")[0] 43 | 44 | account := Account{ 45 | username: username, 46 | tokenCredential: &cred, 47 | tenantID: tenantID, 48 | needToBeCleared: needToBeCleared, 49 | ID: id, 50 | userPrincipalName: userPrincipalName, 51 | } 52 | 53 | return account, nil 54 | } 55 | 56 | func (account *Account) loadUserInformations() error { 57 | token, tokenErr := account.GetAccessToken([]string{"https://graph.microsoft.com/.default"}) 58 | 59 | if tokenErr != nil { 60 | return tokenErr 61 | } 62 | 63 | headers := map[string]any{ 64 | "Authorization": utils.PrepareBearerHeader(token.Token), 65 | "Content-Type": "application/json", 66 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36", 67 | } 68 | 69 | reqURL, reqURLErr := url.Parse("https://graph.microsoft.com/v1.0/me") 70 | 71 | if reqURLErr != nil { 72 | return reqURLErr 73 | } 74 | 75 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 76 | URL: reqURL, 77 | Method: "GET", 78 | Headers: headers, 79 | }) 80 | 81 | if respErr != nil { 82 | return respErr 83 | } 84 | 85 | if resp.StatusCode != http.StatusOK { 86 | return errors.New("cannot retrieve account's informations") 87 | } 88 | 89 | accountID, accountIDRes := resp.Body["id"].(string) 90 | if !accountIDRes { 91 | return errors.New("can not retrieve account's ID") 92 | } 93 | 94 | account.ID = accountID 95 | 96 | upn, upnRes := resp.Body["userPrincipalName"].(string) 97 | if !upnRes { 98 | return errors.New("can not retrieve account's UPN") 99 | } 100 | 101 | account.userPrincipalName = upn 102 | 103 | return nil 104 | } 105 | 106 | func (account *Account) GetID() (string, error) { 107 | if len(account.ID) > 0 { 108 | return account.ID, nil 109 | } 110 | 111 | err := account.loadUserInformations() 112 | 113 | return account.ID, err 114 | } 115 | 116 | func (account *Account) GetUserPrincipalName() (string, error) { 117 | if len(account.userPrincipalName) > 0 { 118 | return account.userPrincipalName, nil 119 | } 120 | 121 | err := account.loadUserInformations() 122 | 123 | return account.userPrincipalName, err 124 | } 125 | 126 | func (adminAccount *Account) AddAccountAsContributorToSubscription(userAccount *Account, subscriptionID string) error { //nolint:stylecheck 127 | token, tokenErr := adminAccount.GetAccessToken([]string{"https://management.core.windows.net/.default"}) 128 | 129 | if tokenErr != nil { 130 | return tokenErr 131 | } 132 | 133 | headers := map[string]any{ 134 | "Authorization": utils.PrepareBearerHeader(token.Token), 135 | "Accept": "*/*", 136 | "Content-Type": "application/json", 137 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36", 138 | } 139 | 140 | roleAssignmentID := utils.GenerateUUIDv4() 141 | 142 | reqURL, reqURLErr := url.Parse("https://management.azure.com/batch?api-version=2020-06-01") 143 | 144 | if reqURLErr != nil { 145 | return reqURLErr 146 | } 147 | 148 | userAccountID, userAccountIDErr := userAccount.GetID() 149 | 150 | if userAccountIDErr != nil { 151 | return userAccountIDErr 152 | } 153 | 154 | reqData := map[string]any{ 155 | "requests": []map[string]any{ 156 | { 157 | "content": map[string]any{ 158 | "Id": roleAssignmentID, 159 | "Properties": map[string]any{ 160 | "Id": roleAssignmentID, 161 | "PrincipalId": userAccountID, 162 | "PrincipalType": "User", 163 | "RoleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c", 164 | "Scope": fmt.Sprintf("/subscriptions/%s", subscriptionID), 165 | "Condition": nil, 166 | "ConditionVersion": nil, 167 | }, 168 | }, 169 | "httpMethod": "PUT", 170 | "name": "b131bce0-1e07-4786-a666-2fa3c7235004", 171 | "requestHeaderDetails": map[string]any{ 172 | "commandName": "Microsoft_Azure_AD.AddRoleAssignments.batch", 173 | }, 174 | "url": fmt.Sprintf("https://management.azure.com/subscriptions/%s/providers/Microsoft.Authorization/roleAssignments/%s?api-version=2020-04-01-preview", subscriptionID, roleAssignmentID), 175 | }, 176 | }, 177 | } 178 | 179 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 180 | URL: reqURL, 181 | Method: "POST", 182 | Headers: headers, 183 | Body: reqData, 184 | }) 185 | 186 | if respErr != nil { 187 | return respErr 188 | } 189 | 190 | if resp.StatusCode != http.StatusOK { 191 | return fmt.Errorf("cannot add %s to the subscription #%s as Contributor", userAccount.username, subscriptionID) 192 | } 193 | 194 | userAccount.createdRoleAssignments = append(userAccount.createdRoleAssignments, AccountRoleAssignment{ 195 | subscriptionID: subscriptionID, 196 | roleAssignmentID: roleAssignmentID, 197 | }) 198 | 199 | return nil 200 | } 201 | 202 | func (adminAccount *Account) DeleteAccount(userAccount *Account) error { 203 | token, tokenErr := adminAccount.GetAccessToken([]string{"https://graph.microsoft.com/.default"}) 204 | 205 | if tokenErr != nil { 206 | return tokenErr 207 | } 208 | 209 | headers := map[string]any{ 210 | "Authorization": fmt.Sprintf("Bearer %s", token.Token), 211 | "Accept": "*/*", 212 | "Content-Type": "application/json", 213 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36", 214 | } 215 | 216 | reqURL, reqURLErr := url.Parse("https://graph.microsoft.com/v1.0/$batch") 217 | 218 | if reqURLErr != nil { 219 | return reqURLErr 220 | } 221 | 222 | userAccountID, userAccountIDErr := userAccount.GetID() 223 | 224 | if userAccountIDErr != nil { 225 | return userAccountIDErr 226 | } 227 | 228 | reqData := map[string]any{ 229 | "requests": []map[string]any{ 230 | { 231 | "id": userAccountID, 232 | "method": "DELETE", 233 | "url": fmt.Sprintf("/users/%s", userAccountID), 234 | }, 235 | }, 236 | } 237 | 238 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 239 | URL: reqURL, 240 | Method: "POST", 241 | Headers: headers, 242 | Body: reqData, 243 | }) 244 | 245 | if respErr != nil { 246 | return respErr 247 | } 248 | 249 | if resp.StatusCode != http.StatusOK { 250 | return fmt.Errorf("cannot delete %s user account", userAccount.username) 251 | } 252 | 253 | return nil 254 | } 255 | 256 | func (adminAccount *Account) DeleteCreatedRoleAssignments(userAccount *Account) error { 257 | token, tokenErr := adminAccount.GetAccessToken([]string{"https://management.core.windows.net/.default"}) 258 | 259 | if tokenErr != nil { 260 | return tokenErr 261 | } 262 | 263 | headers := map[string]any{ 264 | "Authorization": fmt.Sprintf("Bearer %s", token.Token), 265 | "Accept": "*/*", 266 | "Content-Type": "application/json", 267 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36", 268 | } 269 | 270 | reqURL, reqURLErr := url.Parse("https://management.azure.com/batch?api-version=2020-06-01") 271 | 272 | if reqURLErr != nil { 273 | return reqURLErr 274 | } 275 | 276 | allRequests := []map[string]any{} 277 | 278 | for _, roleAssignment := range userAccount.createdRoleAssignments { 279 | allRequests = append(allRequests, map[string]any{ 280 | "httpMethod": "DELETE", 281 | "name": utils.GenerateUUIDv4(), 282 | "requestHeaderDetails": map[string]any{ 283 | "commandName": "Microsoft_Azure_AD.DeleteRoleAssignment.batch", 284 | }, 285 | "url": fmt.Sprintf("https://management.azure.com/subscriptions/%s/providers/Microsoft.Authorization/roleAssignments/%s?api-version=2020-04-01-preview", roleAssignment.subscriptionID, roleAssignment.roleAssignmentID), 286 | }) 287 | } 288 | 289 | reqData := map[string]any{ 290 | "requests": allRequests, 291 | } 292 | 293 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 294 | URL: reqURL, 295 | Method: "POST", 296 | Headers: headers, 297 | Body: reqData, 298 | }) 299 | 300 | if respErr != nil { 301 | return respErr 302 | } 303 | 304 | if resp.StatusCode != http.StatusOK { 305 | return fmt.Errorf("cannot delete %s user account's created role assignments", userAccount.username) 306 | } 307 | 308 | return nil 309 | } 310 | 311 | func (adminAccount *Account) CreateAccount(username, password string, needToBeCleared bool) (Account, error) { 312 | token, tokenErr := adminAccount.GetAccessToken([]string{"https://graph.microsoft.com/.default"}) 313 | 314 | if tokenErr != nil { 315 | return Account{}, tokenErr 316 | } 317 | 318 | adminEmail, adminEmailErr := adminAccount.GetUserPrincipalName() 319 | 320 | if adminEmailErr != nil { 321 | return Account{}, adminEmailErr 322 | } 323 | 324 | if len(strings.Split(adminEmail, "@")) < 2 { 325 | return Account{}, fmt.Errorf("cannot determine Azure domain") 326 | } 327 | 328 | domain := strings.Split(adminEmail, "@")[1] 329 | 330 | headers := map[string]any{ 331 | "Authorization": fmt.Sprintf("Bearer %s", token.Token), 332 | "Accept": "*/*", 333 | "Content-Type": "application/json", 334 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36", 335 | } 336 | 337 | reqURL, reqURLErr := url.Parse("https://graph.microsoft.com/v1.0/$batch") 338 | 339 | if reqURLErr != nil { 340 | return Account{}, reqURLErr 341 | } 342 | 343 | email := fmt.Sprintf("%s@%s", username, domain) 344 | 345 | reqData := map[string]any{ 346 | "requests": []map[string]any{ 347 | { 348 | "id": utils.GenerateUUIDv4(), 349 | "method": "POST", 350 | "url": "/users", 351 | "body": map[string]any{ 352 | "accountEnabled": true, 353 | "displayName": username, 354 | "passwordProfile": map[string]any{ 355 | "forceChangePasswordNextSignIn": false, 356 | "password": password, 357 | }, 358 | "mailNickname": username, 359 | "userPrincipalName": email, 360 | }, 361 | "headers": map[string]any{ 362 | "Content-Type": "application/json", 363 | }, 364 | }, 365 | }, 366 | } 367 | 368 | resp, respErr := utils.SendJSONRequest(utils.HTTPRequestJSONData{ 369 | URL: reqURL, 370 | Method: "POST", 371 | Headers: headers, 372 | Body: reqData, 373 | }) 374 | 375 | if respErr != nil { 376 | return Account{}, respErr 377 | } 378 | 379 | var statusCode float64 380 | 381 | bodyResponses, ok := resp.Body["responses"].([]any) 382 | if !ok { 383 | return Account{}, errors.New("can not parse body responses") 384 | } 385 | 386 | bodyResponse, ok := bodyResponses[0].(map[string]any) 387 | if !ok { 388 | return Account{}, errors.New("can not parse body response") 389 | } 390 | 391 | bodyResponseStatusCode, ok := bodyResponse["status"].(float64) 392 | if !ok { 393 | return Account{}, errors.New("can not parse body status code") 394 | } 395 | 396 | statusCode = bodyResponseStatusCode 397 | 398 | if statusCode != http.StatusCreated { 399 | statusMessage := "Unknown error" 400 | 401 | if statusCode > 300 { //nolint:revive,gomnd 402 | responses, ok := resp.Body["responses"].([]any) 403 | if !ok || len(responses) == 0 { 404 | return Account{}, errors.New("unable to cast or empty responses") 405 | } 406 | 407 | response, ok := responses[0].(map[string]any) 408 | if !ok { 409 | return Account{}, errors.New("unable to cast response to map") 410 | } 411 | 412 | body, ok := response["body"].(map[string]any) 413 | if !ok { 414 | return Account{}, errors.New("unable to cast body to map") 415 | } 416 | 417 | errorField, ok := body["error"].(map[string]any) 418 | if !ok { 419 | return Account{}, errors.New("unable to cast error to map") 420 | } 421 | 422 | message, ok := errorField["message"].(string) 423 | if !ok { 424 | return Account{}, errors.New("unable to cast message to string") 425 | } 426 | 427 | statusMessage = message 428 | } 429 | 430 | return Account{}, fmt.Errorf("cannot create %s user account (%s)", username, statusMessage) 431 | } 432 | 433 | responses, ok := resp.Body["responses"].([]any) 434 | if !ok || len(responses) == 0 { 435 | return Account{}, errors.New("unable to cast or empty responses") 436 | } 437 | 438 | response, ok := responses[0].(map[string]any) 439 | if !ok { 440 | return Account{}, errors.New("unable to cast response to map") 441 | } 442 | 443 | body, ok := response["body"].(map[string]any) 444 | if !ok { 445 | return Account{}, errors.New("unable to cast body to map") 446 | } 447 | 448 | accountID, ok := body["id"].(string) 449 | if !ok { 450 | return Account{}, errors.New("unable to cast id to string") 451 | } 452 | 453 | accountUserPrincipalName, ok := body["userPrincipalName"].(string) 454 | if !ok { 455 | return Account{}, errors.New("unable to cast userPrincipalName to string") 456 | } 457 | 458 | return ConnectAccount(email, password, adminAccount.tenantID, needToBeCleared, accountID, accountUserPrincipalName) 459 | } 460 | 461 | func (account *Account) GetAccessToken(scopes []string) (azcore.AccessToken, error) { 462 | token, tokenErr := (*account.tokenCredential).GetToken(context.Background(), policy.TokenRequestOptions{ 463 | Scopes: scopes, 464 | }) 465 | 466 | return token, tokenErr 467 | } 468 | 469 | //nolint:gocritic 470 | func (account *Account) GetCredentials() *azcore.TokenCredential { 471 | return account.tokenCredential 472 | } 473 | 474 | // func (account *Account) createAccount(username, password string) (Account, error) { 475 | 476 | // } 477 | 478 | func getAzIdentityFromNewUsernamePasswordCredential(email, password, tenantID string) (*azidentity.UsernamePasswordCredential, error) { 479 | // Connection from tenantID, clientID, username and password 480 | // Client ID : the client (application) ID of an App Registration in the tenant. 481 | // https://learn.microsoft.com/en-us/javascript/api/@azure/identity/usernamepasswordcredential?view=azure-node-latest 482 | cred, err := azidentity.NewUsernamePasswordCredential(tenantID, "04b07795-8ddb-461a-bbee-02f9e1bf7b46", email, password, nil) 483 | 484 | return cred, err 485 | } 486 | -------------------------------------------------------------------------------- /providers/github/files.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | // Base 64 encoding of .github-files 4 | const SPRAYER_PY_FILE_BASE64 string = "aW1wb3J0IHJlcXVlc3RzCmltcG9ydCBvcwpmcm9tIHVybGxpYjMuZXhjZXB0aW9ucyBpbXBvcnQgSW5zZWN1cmVSZXF1ZXN0V2FybmluZwpmcm9tIENyeXB0by5DaXBoZXIgaW1wb3J0IEFFUwpmcm9tIENyeXB0byBpbXBvcnQgUmFuZG9tCmltcG9ydCB0eXBpbmcKaW1wb3J0IGJhc2U2NAoKQ0hVTktfU0laRSA9IDUwMDAKCnJlcXVpcmVkRW52VmFycyA9IFsiTUVUSE9EX0hFWCIsICJVUkxfSEVYIiwgIkhFQURFUlNfSEVYIiwgIkJPRFlfSEVYIiwgIkFFUzI1Nl9LRVlfSEVYIl0KbWlzc2luZ0VudlZhcnMgPSBbdmFyIGZvciB2YXIgaW4gcmVxdWlyZWRFbnZWYXJzIGlmIG9zLmdldGVudih2YXIpIGlzIE5vbmVdCgppZiBtaXNzaW5nRW52VmFyczoKICAgIG1pc3NpbmdWYXJzU3RyID0gIiwgIi5qb2luKG1pc3NpbmdFbnZWYXJzKQogICAgCiAgICByYWlzZSBWYWx1ZUVycm9yKGYiTWlzc2luZyBlbnZpcm9ubWVudCB2YXJpYWJsZXM6IHttaXNzaW5nVmFyc1N0cn0iKQoKZW52TWV0aG9kID0gYnl0ZXMuZnJvbWhleChvcy5nZXRlbnYoIk1FVEhPRF9IRVgiKSkKZW52VVJMID0gYnl0ZXMuZnJvbWhleChvcy5nZXRlbnYoIlVSTF9IRVgiKSkKZW52SGVhZGVycyA9IGJ5dGVzLmZyb21oZXgob3MuZ2V0ZW52KCJIRUFERVJTX0hFWCIsIGRlZmF1bHQ9IiIpKQplbnZCb2R5ID0gYnl0ZXMuZnJvbWhleChvcy5nZXRlbnYoIkJPRFlfSEVYIiwgZGVmYXVsdD0iIikpCmFlc0tleSA9IGJ5dGVzLmZyb21oZXgob3MuZ2V0ZW52KCJBRVMyNTZfS0VZX0hFWCIsIGRlZmF1bHQ9IiIpKQoKZGVmIGFlczI1Nl9lbmNyeXB0KGRhdGE6IGJ5dGVzLCBrZXk6IGJ5dGVzKSAtPiBzdHI6CiAgICAiIiIKICAgICAgICAgRW5jcnlwdCB1c2luZyBBRVMtMjU2LUdDTSByYW5kb20gaXYKICAgICAgICAna2V5JyBtdXN0IGJlIGJ5dGVzIGZyb20gaGV4LCBnZW5lcmF0ZSB3aXRoICdvcGVuc3NsIHJhbmQgLWhleCAzMicKICAgICAgICAgUmV0dXJucyBhIGhleCBzdHIKICAgICIiIgogICAgdHJ5OgogICAgICAgIGl2ID0gUmFuZG9tLmdldF9yYW5kb21fYnl0ZXMoMTIpICMgUmVjb21tZW5kZWQgbm9uY2Ugc2l6ZSBmb3IgQUVTIEdDTSA6IDEyIGJ5dGVzCgogICAgICAgIGNpcGhlciA9IEFFUy5uZXcoa2V5LCBBRVMuTU9ERV9HQ00sIGl2KQoKICAgICAgICBjaXBoZXJfZGF0YSA9IGNpcGhlci5lbmNyeXB0KGRhdGEpCiAgICAgICAgdGFnID0gY2lwaGVyLmRpZ2VzdCgpCgogICAgICAgIHJlc3VsdCA9IGl2LmhleCgpK2NpcGhlcl9kYXRhLmhleCgpK3RhZy5oZXgoKSAjIFJlc3VsdCA6IElWICsgQ0lQSEVSIERBVEEgKyBUQUcgKFRhZyBpcyB1c2VkIGJ5IEdDTSBmb3IgYXV0aGVudGljYXRpb24gcHVycG9zZXMpCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcHJpbnQoIkNhbm5vdCBlbmNyeXB0IGRhdGFzLi4uIikKICAgICAgICBwcmludChlKQogICAgICAgIGV4aXQoMSkKICAgIHJldHVybiByZXN1bHQKCmRlZiBhZXMyNTZfZGVjcnlwdChlbmNyeXB0ZWREYXRhOiBieXRlcywga2V5OiBieXRlcykgLT4gYnl0ZXM6CiAgICAiIiIKICAgICAgICAgRGVjcnlwdCB1c2luZyBBRVMtMjU2LUdDTSByYW5kb20gaXYKICAgICAgICAna2V5SGV4U3RyJyBtdXN0IGJlIGluIGhleCwgZ2VuZXJhdGUgd2l0aCAnb3BlbnNzbCByYW5kIC1oZXggMzInCiAgICAiIiIKICAgIHRyeToKICAgICAgICBpdiA9IGVuY3J5cHRlZERhdGFbOjEyXQogICAgICAgIGVuY3J5cHRlZF9kYXRhID0gZW5jcnlwdGVkRGF0YVsxMjotMTZdCiAgICAgICAgdGFnID0gZW5jcnlwdGVkRGF0YVstMTY6XQoKICAgICAgICBjaXBoZXIgPSBBRVMubmV3KGtleSwgQUVTLk1PREVfR0NNLCBpdikKICAgICAgICBkZWNyeXB0ZWQgPSBjaXBoZXIuZGVjcnlwdF9hbmRfdmVyaWZ5KGVuY3J5cHRlZF9kYXRhLCB0YWcpCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcHJpbnQoIkNhbm5vdCBkZWNyeXB0IGRhdGEuLi4iKQogICAgICAgIHByaW50KGUpCiAgICAgICAgZXhpdCgxKQogICAgcmV0dXJuIGRlY3J5cHRlZAoKZGVmIHBhcnNlSW5wdXRzKGVudk1ldGhvZDogYnl0ZXMsIGVudlVSTDogYnl0ZXMsIGVudkhlYWRlcnM6IGJ5dGVzLCBlbnZCb2R5OiBieXRlcywgYWVzS2V5OiBieXRlcykgLT4gdHlwaW5nLlR1cGxlW2J5dGVzLCBieXRlcywgZGljdCwgYnl0ZXNdOgogICAgbWV0aG9kID0gYWVzMjU2X2RlY3J5cHQoZW52TWV0aG9kLCBhZXNLZXkpCiAgICB1cmwgPSBhZXMyNTZfZGVjcnlwdChlbnZVUkwsIGFlc0tleSkKICAgIAogICAgaGVhZGVycyA9IHt9CiAgICBpZiBsZW4oZW52SGVhZGVycykgPiAwOgogICAgICAgIGhlYWRlcnNSYXcgPSBhZXMyNTZfZGVjcnlwdChlbnZIZWFkZXJzLCBhZXNLZXkpCiAgICAgICAgaGVhZGVyc0xpbmVzID0gaGVhZGVyc1Jhdy5zcGxpdGxpbmVzKCkKICAgICAgICBpZiBsZW4oaGVhZGVyc0xpbmVzKSUyICE9IDA6CiAgICAgICAgICAgIHJhaXNlIEV4Y2VwdGlvbigiQ2FuIG5vdCBwYXJzZSB0aGUgcmVxdWVzdCBoZWFkZXJzLiIpCgogICAgICAgIGZvciBpIGluIHJhbmdlKDAsIGxlbihoZWFkZXJzTGluZXMpLCAyKToKICAgICAgICAgICAgaGVhZGVyc1toZWFkZXJzTGluZXNbaV1dID0gaGVhZGVyc0xpbmVzW2krMV0KCiAgICBib2R5ID0gTm9uZQogICAgaWYgbGVuKGVudkJvZHkpID4gMDoKICAgICAgICBib2R5ID0gYWVzMjU2X2RlY3J5cHQoZW52Qm9keSwgYWVzS2V5KQoKICAgIHJldHVybiBtZXRob2QsIHVybCwgaGVhZGVycywgYm9keQoKZGVmIGVuY3J5cHRPdXRwdXRzKHN0YXR1czogaW50LCBoZWFkZXJzOiBkaWN0LCBib2R5OiBieXRlcywgYWVzS2V5OiBieXRlcykgLT4gdHlwaW5nLlR1cGxlW3N0ciwgc3RyLCBzdHJdOgogICAgc3RhdHVzSGV4ID0gIiIKICAgIGlmIHN0YXR1cyAhPSBOb25lOgogICAgICAgIHN0YXR1c0hleCA9IGFlczI1Nl9lbmNyeXB0KHN0cihzdGF0dXMpLmVuY29kZSgpLCBhZXNLZXkpCgogICAgaGVhZGVyc1N0ciA9ICIiCiAgICBpZiBoZWFkZXJzICE9IE5vbmU6CiAgICAgICAgZm9yIGtleSBpbiBoZWFkZXJzOgogICAgICAgICAgICBoZWFkZXJzU3RyICs9IGtleSArICJcbiIgKyBoZWFkZXJzW2tleV0gKyAiXG4iCiAgICAgICAgaWYgbGVuKGhlYWRlcnNTdHIpID4gMDoKICAgICAgICAgICAgaGVhZGVyc1N0ciA9IGhlYWRlcnNTdHJbOi0xXQogICAgaGVhZGVyc0hleCA9IGFlczI1Nl9lbmNyeXB0KGhlYWRlcnNTdHIuZW5jb2RlKCksIGFlc0tleSkKCiAgICBib2R5SGV4ID0gIiIKICAgIGlmIGJvZHkgIT0gTm9uZToKICAgICAgICBib2R5SGV4ID0gYWVzMjU2X2VuY3J5cHQoYm9keSwgYWVzS2V5KQoKICAgIHJldHVybiBzdGF0dXNIZXgsIGhlYWRlcnNIZXgsIGJvZHlIZXgKCmRlZiBtYWtlUmVxdWVzdChtZXRob2Q6IGJ5dGVzLCB1cmw6IGJ5dGVzLCBoZWFkZXJzOmRpY3Q9Tm9uZSwgYm9keTpieXRlcz1Ob25lKSAtPiB0eXBpbmcuVHVwbGVbaW50LCBkaWN0LCBieXRlcywgc3RyXToKICAgIHRyeToKICAgICAgICByZXNwb25zZSA9IHJlcXVlc3RzLnJlcXVlc3QobWV0aG9kLCB1cmwsIGhlYWRlcnM9aGVhZGVycywgZGF0YT1ib2R5LCB2ZXJpZnk9RmFsc2UsIGFsbG93X3JlZGlyZWN0cz1GYWxzZSwgc3RyZWFtPVRydWUpCiAgICAKICAgICAgICBzdGF0dXMgPSByZXNwb25zZS5zdGF0dXNfY29kZQogICAgICAgIGhlYWRlcnMgPSByZXNwb25zZS5oZWFkZXJzCiAgICAgICAgYm9keSA9IHJlc3BvbnNlLnJhdy5yZWFkKCkKCiAgICAgICAgcmV0dXJuIHN0YXR1cywgaGVhZGVycywgYm9keSwgTm9uZQogICAgZXhjZXB0IHJlcXVlc3RzLlJlcXVlc3RFeGNlcHRpb24gYXMgZToKICAgICAgICByZXR1cm4gTm9uZSwgTm9uZSwgTm9uZSwgYmFzZTY0LmI2NGVuY29kZShzdHIoZSkuZW5jb2RlKCkpLmRlY29kZSgpCgpyZXFNZXRob2QsIHJlcVVSTCwgcmVxSGVhZGVycywgcmVxQm9keSA9IHBhcnNlSW5wdXRzKGVudk1ldGhvZCwgZW52VVJMLCBlbnZIZWFkZXJzLCBlbnZCb2R5LCBhZXNLZXkpCgpyZXNwU3RhdHVzLCByZXNwSGVhZGVycywgcmVzcEJvZHksIHJlc3BFcnIgPSBtYWtlUmVxdWVzdChyZXFNZXRob2QsIHJlcVVSTCwgcmVxSGVhZGVycywgcmVxQm9keSkKCmlmIHJlc3BFcnIgIT0gTm9uZToKICAgIHByaW50KCJSRVNQX0VSUiIsIHJlc3BFcnIpCmVsc2U6CiAgICByZXNwU3RhdHVzSGV4LCByZXNwSGVhZGVyc0hleCwgcmVzcEJvZHlIZXggPSBlbmNyeXB0T3V0cHV0cyhyZXNwU3RhdHVzLCByZXNwSGVhZGVycywgcmVzcEJvZHksIGFlc0tleSkKCiAgICBwcmludCgiUkVTUF9TVEFUVVNfRU5DUllQVEVEX0hFWCIsIHJlc3BTdGF0dXNIZXgpCiAgICAKICAgICMgUHJpbnRzIGFyZSBjaHVua2VkIGluIG9yZGVyIHRvIGF2b2lkIEdpdEh1YiBpc3N1ZXMgYW5kIHNsb3duZXNzIG9mIHByaW50aW5nIGxhcmdlIHN0cmluZ3MKICAgIGZvciBpIGluIHJhbmdlKDAsIGxlbihyZXNwSGVhZGVyc0hleCksIENIVU5LX1NJWkUpOgogICAgICAgIHByaW50KCJSRVNQX0hFQURFUlNfRU5DUllQVEVEX0hFWCIsIHJlc3BIZWFkZXJzSGV4W2k6aSArIENIVU5LX1NJWkVdKQogICAgCiAgICAjIFByaW50cyBhcmUgY2h1bmtlZCBpbiBvcmRlciB0byBhdm9pZCBHaXRIdWIgaXNzdWVzIGFuZCBzbG93bmVzcyBvZiBwcmludGluZyBsYXJnZSBzdHJpbmdzCiAgICBmb3IgaSBpbiByYW5nZSgwLCBsZW4ocmVzcEJvZHlIZXgpLCBDSFVOS19TSVpFKToKICAgICAgICBwcmludCgiUkVTUF9CT0RZX0VOQ1JZUFRFRF9IRVgiLCByZXNwQm9keUhleFtpOmkgKyBDSFVOS19TSVpFXSk=" //nolint:revive,stylecheck 5 | const REQUIREMENTS_TXT_FILE_BASE64 string = "Y2VydGlmaQpjaGFyc2V0LW5vcm1hbGl6ZXIKaWRuYQpyZXF1ZXN0cwp1cmxsaWIzCnB5Y3J5cHRvZG9tZQ==" //nolint:revive,stylecheck 6 | const SPRAYER_YML_FILE_BASE64 string = "bmFtZTogU3ByYXllcgoKb246CiAgd29ya2Zsb3dfZGlzcGF0Y2g6CiAgICBpbnB1dHM6CiAgICAgIG1ldGhvZEVuYzoKICAgICAgICBkZXNjcmlwdGlvbjogJ1JlcXVlc3QgbWV0aG9kIGVuY29kZWQgYXMgQmFzZTY0JwogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgIHVybEVuYzoKICAgICAgICBkZXNjcmlwdGlvbjogJ1JlcXVlc3QgVVJMIGVuY29kZWQgYXMgQmFzZTY0JwogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgIGhlYWRlcnNFbmM6CiAgICAgICAgZGVzY3JpcHRpb246ICdSZXF1ZXN0IGhlYWRlcnMgZW5jb2RlZCBhcyBCYXNlNjQnCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgIGJvZHlFbmM6CiAgICAgICAgZGVzY3JpcHRpb246ICdSZXF1ZXN0IGJvZHkgZW5jb2RlZCBhcyBCYXNlNjQnCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgIHJ1bklkZW50aWZpZXI6CiAgICAgICAgZGVzY3JpcHRpb246ICdydW4gaWRlbnRpZmllcicKICAgICAgICByZXF1aXJlZDogZmFsc2UKCmpvYnM6CiAgaWQ6CiAgICBuYW1lOiBXb3JrZmxvdyBJRCBQcm92aWRlcgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogJHt7Z2l0aHViLmV2ZW50LmlucHV0cy5ydW5JZGVudGlmaWVyfX0KICAgICAgICBydW46IGVjaG8gcnVuIGlkZW50aWZpZXIgJHt7IGlucHV0cy5ydW5JZGVudGlmaWVyIH19CiAgcmVxOgogICAgbmFtZTogUmVxdWVzdAogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAgIC0gdXNlczogYWN0aW9ucy9jaGVja291dEB2MgogICAgICAtIG5hbWU6IFNldCB1cCBQeXRob24KICAgICAgICB1c2VzOiBhY3Rpb25zL3NldHVwLXB5dGhvbkB2MgogICAgICAgIHdpdGg6CiAgICAgICAgICBweXRob24tdmVyc2lvbjogJzMueCcKICAgICAgLSBuYW1lOiBJbnN0YWxsIGRlcGVuZGVuY2llcwogICAgICAgIHJ1bjogcHl0aG9uMyAtbSBwaXAgaW5zdGFsbCAtciByZXF1aXJlbWVudHMudHh0CiAgICAgIC0gbmFtZTogUnVuIHNjcmlwdAogICAgICAgIHJ1bjogcHl0aG9uIHNwcmF5ZXIucHkKICAgICAgICBlbnY6CiAgICAgICAgICBNRVRIT0RfSEVYOiAke3sgaW5wdXRzLm1ldGhvZEVuYyB9fQogICAgICAgICAgVVJMX0hFWDogJHt7IGlucHV0cy51cmxFbmMgfX0KICAgICAgICAgIEhFQURFUlNfSEVYOiAke3sgaW5wdXRzLmhlYWRlcnNFbmMgfX0gCiAgICAgICAgICBCT0RZX0hFWDogJHt7IGlucHV0cy5ib2R5RW5jIH19ICAKICAgICAgICAgIEFFUzI1Nl9LRVlfSEVYOiAke3sgc2VjcmV0cy5BRVMyNTZfS0VZX0hFWCB9fQogICAgICA=" //nolint:revive,stylecheck 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔁 IPSpinner 2 | 3 | IPSpinner is a local proxy which can be used to redirect all incoming requests through different chosen providers. The purpose is to create a pass-through proxy that rotates the source IP address of each request. As an example, running a bruteforce operation through IPSpinner will help to avoid being caught because the server will receive the requests from hundreds of different IP addresses. 4 | 5 | IPSpinner currently supports AWS (API Gateway), Azure (Cloud Shell) and GitHub (GitHub Actions). 6 | 7 | # Table of contents 8 | 9 | 1. [How it works?](#i-how-it-works) 10 | 1. [General](#1-general) 11 | 2. [Per provider & launcher](#2-per-provider-launcher) 12 | 1. [AWS API Gateway](#i-aws-api-gateway) 13 | 2. [Azure Cloud Shell](#ii-azure-cloud-shell) 14 | 3. [GitHub Actions](#iii-github-actions) 15 | 3. [Launcher comparison](#3-launcher-comparison) 16 | 2. [How to install?](##ii-how-to-install) 17 | 1. [Install Go](#1-install-go) 18 | 2. [Clone and build IPSpinner](#2-clone-and-build-ipspinner) 19 | 3. [Clean builds](#3-clean-builds) 20 | 3. [How to use?](#iii-how-to-use) 21 | 1. [General](#1-general-1) 22 | 1. [Command line arguments](#i-command-line-arguments) 23 | 2. [Configuration file](#ii-configuration-file) 24 | 2. [Per provider](#2-per-provider) 25 | 1. [AWS](#i-aws) 26 | 2. [Azure](#ii-azure) 27 | 3. [GitHub](#iii-github) 28 | 4. [How to ...?](#iv-how-to-) 29 | 1. [HTTP/2 support?](#1-http2-support) 30 | 31 | # I/ How it works? 32 | 33 | ## 1) General 34 | 35 |
37 | Figure 1: IPSpinner - Overall diagram
38 |
60 | Figure 2: AWS API Gateway - Overall diagram
61 |
67 | Figure 3: AWS API Gateway - Available IP addresses per region
68 |
74 | Figure 4: AWS API Gateway - IP addresses per country
75 |
83 | Figure 5: AWS API Gateway - Rotating process
84 |
101 | Figure 6: Azure Cloud Shell - Overall diagram
102 |
108 | Figure 7: Azure Cloud Shell - Available IP addresses per region
109 |
115 | Figure 8: Azure Cloud Shell - IP addresses per country
116 |
135 | Figure 9: GitHub Actions - Overall diagram
136 |
142 | Figure 10: GitHub Actions - Available IP addresses per region
143 |
150 | Figure 11: GitHub Actions - IP addresses per country
151 |