package shuffle import ( "archive/tar" "archive/zip" "bufio" "crypto/sha1" "bytes" "compress/gzip" "context" "crypto/md5" "crypto/sha256" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "os" "regexp" "runtime" "sort" "strconv" "strings" "sync" "time" "cloud.google.com/go/storage" docker "github.com/docker/docker/client" "gopkg.in/yaml.v2" uuid "github.com/satori/go.uuid" "github.com/frikky/kin-openapi/openapi3" //iocParser "github.com/Shuffle/indicator-parser/go/ioc" ) var downloadedImages = []string{} var pythonAllowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" var pythonReplacements = map[string]string{ "[": "", "]": "", "{": "", "}": "", "(": "", ")": "", "!": "", "@": "", "#": "", "$": "", "%": "", "^": "", "&": "", ":": "", ";": "", "<": "", ">": "", "'": "", } type countingWriter struct { n *int64 } func CopyFile(fromfile, tofile string) error { from, err := os.Open(fromfile) if err != nil { return err } defer from.Close() to, err := os.OpenFile(tofile, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return err } defer to.Close() _, err = io.Copy(to, from) if err != nil { return err } return nil } func GetCorrectActionName(parsed string) string { if strings.HasPrefix(parsed, "post ") || strings.HasPrefix(parsed, "post_") { parsed = parsed[5:] } else if strings.HasPrefix(parsed, "get list") || strings.HasPrefix(parsed, "get_list") { parsed = parsed[4:] } else if strings.HasPrefix(parsed, "head ") || strings.HasPrefix(parsed, "head_") { parsed = parsed[5:] } else if strings.HasPrefix(parsed, "put ") || strings.HasPrefix(parsed, "put_") { parsed = parsed[4:] } else if strings.HasPrefix(parsed, "patch ") || strings.HasPrefix(parsed, "patch_") { parsed = parsed[6:] } if strings.HasPrefix(parsed, "\"") { parsed = parsed[1:] } if strings.HasSuffix(parsed, "\"") { parsed = parsed[:len(parsed)-1] } return parsed } func FormatAppfile(filedata string) (string, string) { lines := strings.Split(filedata, "\n") newfile := []string{} classname := "" for _, line := range lines { if strings.Contains(line, "walkoff_app_sdk") { continue } // Remap logging. CBA this right now // This issue also persists in onprem apps because of await thingies.. :( // FIXME if strings.Contains(line, "console_logger") && strings.Contains(line, "await") { continue //line = strings.Replace(line, "console_logger", "logger", -1) //log.Println(line) } // Might not work with different import names // Could be fucked up with spaces everywhere? Idk if strings.Contains(line, "class") && strings.Contains(line, "(AppBase)") { items := strings.Split(line, " ") if len(items) > 0 && strings.Contains(items[1], "(AppBase)") { classname = strings.Split(items[1], "(")[0] } else { // This could break something.. classname = "TMP" } } if strings.Contains(line, "if __name__ ==") { break } // asyncio.run(HelloWorld.run(), debug=True) newfile = append(newfile, line) } filedata = strings.Join(newfile, "\n") return classname, filedata } // Streams the data into a zip to be used for a cloud function func StreamZipdata(ctx context.Context, identifier, pythoncode, requirements, bucketName string) (string, error) { filename := fmt.Sprintf("generated_cloudfunctions/%s.zip", identifier) buf := new(bytes.Buffer) zipWriter := zip.NewWriter(buf) if project.Environment == "cloud" { client, err := storage.NewClient(ctx) if err != nil { log.Printf("Failed to create datastore client: %v", err) return filename, err } bucket := client.Bucket(bucketName) obj := bucket.Object(filename) storageWriter := obj.NewWriter(ctx) defer storageWriter.Close() zipWriter = zip.NewWriter(storageWriter) } zipFile, err := zipWriter.Create("main.py") if err != nil { log.Printf("Packing failed to create zip file from bucket: %v", err) return filename, err } // Have to use Fprintln otherwise it tries to parse all strings etc. if _, err := fmt.Fprintln(zipFile, pythoncode); err != nil { return filename, err } //log.Printf("Merging requirements: %s", requirements) zipFile, err = zipWriter.Create("requirements.txt") if err != nil { log.Printf("Packing failed to create zip file from bucket: %v", err) return filename, err } if _, err := fmt.Fprintln(zipFile, requirements); err != nil { return filename, err } err = zipWriter.Close() if err != nil { log.Printf("Packing failed to close zip file writer from bucket: %v", err) return filename, err } return filename, nil } // used to load SDK func getGithubFile(githubUrl string) ([]byte, error) { // md5 the github url and use it as cache key ctx := context.Background() cacheKey := fmt.Sprintf("%x", md5.Sum([]byte(githubUrl))) if project.CacheDb { cache, err := GetCache(ctx, cacheKey) if err == nil { cacheData := []byte(cache.([]uint8)) return cacheData, nil } } // Load the data from this file and return. resp, err := http.Get(githubUrl) if err != nil { log.Printf("[ERROR] Failed to get app base from github: %s", err) return []byte{}, err } if resp.StatusCode >= 300 { log.Printf("[ERROR] Failed to get app base from github: %s", resp.Status) return []byte{}, errors.New(fmt.Sprintf("Failed to get app base from github: %s", resp.Status)) } defer resp.Body.Close() appbaseData, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERROR] Failed to read app base from github: %s", err) return []byte{}, err } log.Printf("[DEBUG] Loaded App SDK of length %d from github", len(appbaseData)) if project.CacheDb { err = SetCache(ctx, cacheKey, appbaseData, 30) if err != nil { log.Printf("[WARNING] Failed setting cache for org stats '%s': %s", cacheKey, err) } } return appbaseData, nil } func GetAppbase() ([]byte, []byte, error) { // 1. Have baseline in bucket/generated_apps/baseline // 2. Copy the baseline to a new folder with identifier name appbase := "../app_sdk/app_base.py" //static := "../app_sdk/static_baseline.py" //staticData, err := ioutil.ReadFile(static) //if err != nil { // return []byte{}, []byte{}, err //} staticData := []byte{} appbaseData, err := ioutil.ReadFile(appbase) if err != nil { // FIXME: Use an older commit of the file? githubUrl := "https://raw.githubusercontent.com/Shuffle/app_sdk/refs/heads/main/shuffle_sdk/shuffle_sdk.py" content, err := getGithubFile(githubUrl) return content, []byte{}, err return []byte{}, []byte{}, err } return appbaseData, staticData, nil } // Builds the structure for the new generated app in storage (copying baseline files) func GetAppbaseGCP(ctx context.Context, client *storage.Client, bucketName string) ([]byte, []byte, error) { // 1. Have baseline in bucket/generated_apps/baseline // 2. Copy the baseline to a new folder with identifier name loadFromGithub := true basePath := "generated_apps/baseline" reference := client.Bucket(bucketName).Object(fmt.Sprintf("%s/app_base.py", basePath)) // Check if it's more than 1 month old if reference != nil { attrs, err := reference.Attrs(ctx) if err == nil { if attrs.Updated.Before(time.Now().AddDate(0, -1, 0)) { log.Printf("[WARNING] App base is older than 1 month on github. Offloading to direct github ref") } else { loadFromGithub = false } } } if loadFromGithub { // FIXME: Use an older commit of the file githubUrl := "https://raw.githubusercontent.com/Shuffle/app_sdk/refs/heads/main/shuffle_sdk/shuffle_sdk.py" content, err := getGithubFile(githubUrl) return content, []byte{}, err } appbase, err := reference.NewReader(ctx) if err != nil { log.Printf("[WARNING] Failed to get app base from GCP BUCKET %#v. Offloading to direct github ref", bucketName) // FIXME: Use an older commit of the file githubUrl := "https://raw.githubusercontent.com/Shuffle/app_sdk/refs/heads/main/shuffle_sdk/shuffle_sdk.py" content, err := getGithubFile(githubUrl) return content, []byte{}, err } defer appbase.Close() appbaseData, err := ioutil.ReadAll(appbase) if err != nil { return []byte{}, []byte{}, err } return appbaseData, []byte{}, nil } func FixAppbase(appbase []byte) []string { record := true validLines := []string{} // Used to use static_baseline + app_base. Now it's only appbase :O for _, line := range strings.Split(string(appbase), "\n") { //if strings.Contains(line, "#STOPCOPY") { // //log.Println("Stopping copy") // break //} if record { validLines = append(validLines, line) } //if strings.Contains(line, "#STARTCOPY") { // //log.Println("Starting copy") // record = true //} } return validLines } // Builds the structure for the new generated app in storage (copying baseline files) func BuildStructureGCP(ctx context.Context, client *storage.Client, identifier, bucketName string) (string, error) { // 1. Have baseline in bucket/generated_apps/baseline // 2. Copy the baseline to a new folder with identifier name basePath := "generated_apps" //identifier := fmt.Sprintf("%s-%s", swagger.Info.Title, curHash) appPath := fmt.Sprintf("%s/%s", basePath, identifier) //fileNames := []string{"Dockerfile", "requirements.txt"} //fileNames := []string{"Dockerfile", "requirements.txt"} fileNames := []string{"Dockerfile"} requirements := GetAppRequirements() if len(requirements) > 0 { // Write the requirements text to the file just so that it's ready dst := client.Bucket(bucketName).Object(fmt.Sprintf("%s/%s", appPath, "requirements.txt")) if _, err := dst.NewWriter(ctx).Write([]byte(requirements)); err != nil { log.Printf("[ERROR] Failed to write requirements.txt during app build: %s", err) fileNames = append(fileNames, "requirements.txt") } } for _, file := range fileNames { src := client.Bucket(bucketName).Object(fmt.Sprintf("%s/baseline/%s", basePath, file)) dst := client.Bucket(bucketName).Object(fmt.Sprintf("%s/%s", appPath, file)) if _, err := dst.CopierFrom(src).Run(ctx); err != nil { return "", err } } return appPath, nil } // Builds the base structure for the app that we're making // Returns error if anything goes wrong. This has to work if // the python code is supposed to be generated func BuildStructure(swagger *openapi3.Swagger, curHash string) (string, error) { //log.Printf("%#v", swagger) // adding md5 based on input data to not overwrite earlier data. generatedPath := "generated" subpath := "../app_gen/python-lib/" identifier := fmt.Sprintf("%s-%s", swagger.Info.Title, curHash) appPath := fmt.Sprintf("%s/%s", generatedPath, identifier) os.MkdirAll(appPath, os.ModePerm) os.Mkdir(fmt.Sprintf("%s/src", appPath), os.ModePerm) // File path no longer required sourceDockerPath := fmt.Sprintf("%sbaseline/Dockerfile", subpath) destDockerPath := fmt.Sprintf("%s/%s", appPath, "Dockerfile") // Check if the full dest folders exists - otherwise make them parsedPathSplit := strings.Split(destDockerPath, "/") parsedPath := strings.Join(parsedPathSplit[0:len(parsedPathSplit)-1], "/") if _, err := os.Stat(parsedPath); os.IsNotExist(err) { os.MkdirAll(parsedPath, 0644) log.Printf("[INFO] Created folder path for Dockerfile to ensure it exists: %s", parsedPath) } err := CopyFile(sourceDockerPath, destDockerPath) if err != nil { // Keep it in blobs just in case? foundDockerfile := GetBaseDockerfile() if len(foundDockerfile) > 0 { // Writing it to both ioutil.WriteFile(sourceDockerPath, []byte(foundDockerfile), 0644) err = ioutil.WriteFile(destDockerPath, []byte(foundDockerfile), 0644) if err != nil { log.Printf("[ERROR] Failed to write Dockerfile from BaseDockerfile during app build: %s", err) return appPath, err } else { log.Printf("[INFO] Successfully wrote Dockerfile from BaseDockerfile to path %s", destDockerPath) } } else { log.Printf("[ERROR] Failed to move Dockerfile from location %s to location %s. Found Dockerfile len: %d", sourceDockerPath, destDockerPath, len(foundDockerfile)) } } parsedAppPath := fmt.Sprintf("%s/%s", appPath, "requirements.txt") requirements := GetAppRequirements() if len(requirements) > 0 { // Write it to the file just so that it's ready err = ioutil.WriteFile(parsedAppPath, []byte(requirements), 0644) if err != nil { log.Printf("[ERROR] Failed to write requirements.txt during app build: %s", err) } else { return appPath, nil } } err = CopyFile(fmt.Sprintf("%sbaseline/requirements.txt", subpath), fmt.Sprintf("%s/%s", appPath, "requirements.txt")) if err != nil { log.Printf("[ERROR] Failed to move requrements.txt") return appPath, err } return appPath, nil } func TrimToNum(r int) bool { if n := r - '0'; n >= 0 && n <= 9 { return false } return true } // Returns fixed function names based on a list of strings func GetValidParameters(parameters []string) []string { numbers := "0123456789" newParams := []string{} for _, param := range parameters { if param == "headers=\"\"" || param == "queries=\"\"" { newParams = append(newParams, param) continue } originalParam := param // Something with dashes not working? for key, val := range pythonReplacements { param = strings.Replace(param, key, val, -1) } for _, char := range param { if !strings.Contains(pythonAllowed, string(char)) { param = strings.Replace(param, string(char), "", -1) } } if len(param) > 0 && !ArrayContains(newParams, param) { newParams = append(newParams, param) } else { // Find some name for it just for the code h := md5.New() io.WriteString(h, originalParam) newName := strings.ToLower(fmt.Sprintf("%X", h.Sum(nil))) // Fix leading numbers newString := "" shouldAdd := false for _, char := range newName { if !strings.Contains(numbers, string(char)) { shouldAdd = true } if shouldAdd { newString += string(char) } } // Leading 0 not allowed newParams = append(newParams, newString) } } return newParams } // This function generates the python code that's being used. // This is really meta when you program it. Handling parameters is hard here. func MakePythoncode(swagger *openapi3.Swagger, name, url, method string, parameters, optionalQueries, headers []string, fileField string, api WorkflowApp, handleFile bool) (string, string) { method = strings.ToLower(method) queryString := "" queryData := "" extraHeaders := "" extraQueries := "" reservedKeys := []string{"BearerAuth", "ApiKeyAuth", "Oauth2", "BasicAuth", "JWT"} // Predefined for auth //invalidQueries := []string{"access_token", "username_basic", "password_basic", "apikey", "api_key"} // FIXME - this might break - need to check if ? or & should be set as query parameterData := "" if len(optionalQueries) > 0 { //if len(queryString queryString += ", " for index, query := range optionalQueries { // Check if it's a part of the URL already parsedQuery := FixFunctionName(query, "", true) newParams := GetValidParameters([]string{parsedQuery}) if len(newParams) > 0 { parsedQuery = newParams[0] } if strings.Contains(queryString, parsedQuery) { continue } queryString += fmt.Sprintf("%s=\"\"", parsedQuery) if index != len(optionalQueries)-1 { queryString += ", " } /* queryData += fmt.Sprintf(` if %s: url += f"&%s={%s}"`, query, query, query) */ queryData += fmt.Sprintf(` if %s: if isinstance(%s, list) or isinstance(%s, dict): try: %s = json.dumps(%s) except: pass params[requests.utils.quote("%s")] = requests.utils.quote(%s)`, parsedQuery, parsedQuery, parsedQuery, parsedQuery, parsedQuery, query, parsedQuery) } } else { //log.Printf("No optional queries?") } // api.Authentication.Parameters[0].Value = "BearerAuth" authenticationParameter := "" authenticationSetup := "" authenticationAddin := "" // Python configuration code that should work :) if swagger.Components.SecuritySchemes != nil { if swagger.Components.SecuritySchemes["BearerAuth"] != nil { authenticationParameter = ", apikey" authenticationSetup = "if apikey != \" \" and not apikey.startswith(\"Bearer\"): request_headers[\"Authorization\"] = f\"Bearer {apikey}\"" } else if swagger.Components.SecuritySchemes["BasicAuth"] != nil { authenticationParameter = ", username_basic, password_basic" authenticationSetup = "auth=None\n if username_basic or password_basic:\n if \"Authorization\" not in headers and \"Basic\" not in headers and not \"Bearer\" in headers:\n auth = requests.auth.HTTPBasicAuth(username_basic, password_basic)" //authenticationAddin = ", auth=(username_basic, password_basic)" authenticationAddin = ", auth=auth" } else if swagger.Components.SecuritySchemes["ApiKeyAuth"] != nil { authenticationParameter = ", apikey" //if len(securitySchemes["ApiKeyAuth"].Value.Description) > 0 { // //log.Printf("UPDATING AUTH!") // extraParam.Description = fmt.Sprintf("Start with %s", securitySchemes["ApiKeyAuth"].Value.Description) if swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.In == "header" { // This is a way to bypass apikeys by passing " " authenticationSetup = fmt.Sprintf(`if apikey != " ": request_headers["%s"] = apikey`, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name) // Fixes token prefixes (e.g. Token.. or SSWS..) if len(swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description) > 0 { trimmedDescription := strings.Trim(swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, " ") authenticationSetup = fmt.Sprintf("if apikey != \" \":\n if apikey.startswith(\"%s\"):\n request_headers[\"%s\"] = apikey\n else:\n apikey = apikey.replace(\"%s\", \"\", -1).strip()\n request_headers[\"%s\"] = f\"%s{apikey}\"", trimmedDescription, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description) } } else if swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.In == "query" { // This might suck lol //authenticationSetup = fmt.Sprintf("if apikey != \" \": params[\"%s\"] = requests.utils.quote(apikey)", swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name) trimmedDescription := strings.Trim(swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, " ") authenticationSetup = fmt.Sprintf("if apikey != \" \":\n if apikey.startswith(\"%s\"):\n params[\"%s\"] = requests.utils.quote(apikey)\n else:\n apikey = apikey.replace(\"%s\", \"\", -1).strip()\n params[\"%s\"] = requests.utils.quote(f\"%s{apikey}\")", trimmedDescription, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description) } } else if swagger.Components.SecuritySchemes["Oauth2"] != nil { //log.Printf("[DEBUG] Appending Oauth2 code") authenticationParameter = ", access_token" authenticationSetup = fmt.Sprintf("if access_token != \" \": request_headers[\"Authorization\"] = f\"Bearer {access_token}\"\n #request_headers[\"Content-Type\"] = \"application/json\"") } else if swagger.Components.SecuritySchemes["jwt"] != nil { //log.Printf("[DEBUG] Appending Oauth2 code") authenticationParameter = ", username_basic, password_basic" //api.Authentication.TokenUri = securitySchemes["jwt"].Value.In //authenticationSetup = fmt.Sprintf("authret = requests.get(f\"{url}%s\", headers=request_headers, auth=(username_basic, password_basic), verify=False)\n request_headers[\"Authorization\"] = f\"Bearer {authret.text}\"\n print(f\"{authret.text}\")", api.Authentication.TokenUri) // Add: client_id and client_secret in body as JSON? authType := "basic" // check x-jwt-auth-type if swagger.Components.SecuritySchemes["x-jwt-auth-type-json"] != nil { authType = "json" } if authType == "basic" { // ADD: accessToken = field authenticationSetup = fmt.Sprintf("authret = requests.get(f\"{url}%s\", headers=request_headers, auth=(username_basic, password_basic), verify=False)\n if 'access_token' in authret.text:\n request_headers[\"Authorization\"] = f\"Bearer {authret.json()['access_token']}\"\n elif 'jwt' in authret.text:\n request_headers[\"Authorization\"] = f\"Bearer {authret.json()['jwt']}\"\n elif 'accessToken' in authret.text:\n request_headers[\"Authorization\"] = f\"Bearer {authret.json()['accessToken']}\"\n else:\n request_headers[\"Authorization\"] = f\"Bearer {authret.text}\"\n print(f\"Found Bearer auth: {authret.text}\")", api.Authentication.TokenUri) } else { authenticationSetup = fmt.Sprintf("authret = requests.post(f\"{url}%s\", headers=request_headers, json={\"username\": username_basic, \"password\": password_basic}, verify=False)\n if 'access_token' in authret.text:\n request_headers[\"Authorization\"] = f\"Bearer {authret.json()['access_token']}\"\n elif 'jwt' in authret.text:\n request_headers[\"Authorization\"] = f\"Bearer {authret.json()['jwt']}\"\n elif 'accessToken' in authret.text:\n request_headers[\"Authorization\"] = f\"Bearer {authret.json()['accessToken']}\"\n else:\n request_headers[\"Authorization\"] = f\"Bearer {authret.text}\"\n print(f\"Found Bearer auth: {authret.text}\")", api.Authentication.TokenUri) } //log.Printf("[DEBUG] Appending jwt code for authenticationSetup:\n %s", authenticationSetup) } } urlSplit := strings.Split(url, "/") if strings.HasPrefix(url, "http") && len(urlSplit) > 2 { tmpUrl := strings.Join(urlSplit[3:len(urlSplit)], "/") if len(tmpUrl) > 0 { url = "/" + tmpUrl } else { if strings.HasSuffix(url, "/") { url = "/" } else { url = "" } } } else { tmpUrl := "" if len(urlSplit) > 2 { tmpUrl = "/" + strings.Join(urlSplit[3:len(urlSplit)], "/") } if !strings.HasPrefix(url, "/") { url = tmpUrl } } functionname := strings.ToLower(fmt.Sprintf("%s_%s", method, name)) if strings.Contains(strings.ToLower(name), strings.ToLower(method)) { functionname = strings.ToLower(name) } // Check for bad {} for python printf. // If it's NOT closed before the next / openBrackets := strings.Count(url, "{") closeBrackets := strings.Count(url, "}") if openBrackets != closeBrackets { removedChars := 0 for charPos, char := range url { if char != '{' { continue } // Find the next / or end of string nextClose := strings.Index(url[charPos:], "}") nextSlash := strings.Index(url[charPos:], "/") if nextClose == -1 || (nextSlash != -1 && nextSlash < nextClose) { // We have a problem log.Printf("[ERROR] Unbalanced bracket at position %d in URL %s", charPos, url) // Remove the bracket from the specific spot. url = url[0:charPos-removedChars] + url[charPos-removedChars+1:len(url)] removedChars += 1 } } openBrackets := strings.Count(url, "{") closeBrackets := strings.Count(url, "}") if openBrackets != closeBrackets { log.Printf("[ERROR] Unbalanced brackets in generated URL %s - this might cause issues, so we're skipping the function. App: %s. Autofixing.", url, name) // Makes the function not generate return functionname, "" } } urlParameter := ", url" urlInline := "{url}" // Specific check for SSL verification // This is critical for onprem stuff. // Added to_file as of July 2022 verifyParam := ", ssl_verify=False, to_file=False" verifyWrapper := `ssl_verify = True if str(ssl_verify).lower() == "true" or ssl_verify == "1" else False` verifyAddin := ", verify=ssl_verify" // Codegen for headers headerParserCode := "" queryParserCode := "" if len(parameters) > 0 { parameters = GetValidParameters(parameters) parameterData = fmt.Sprintf(", %s", strings.Join(parameters, ", ")) // This is gibberish :) for _, param := range parameters { if strings.Contains(param, "headers=") { headerParserCode = "if isinstance(headers, dict):\n request_headers = headers\n elif len(headers) > 0:\n for header in str(headers).split(\"\\n\"):\n if ':' in header:\n headersplit=header.split(':')\n request_headers[headersplit[0].strip()] = ':'.join(headersplit[1:]).strip()\n elif '=' in header:\n headersplit=header.split('=')\n request_headers[headersplit[0].strip()] = '='.join(headersplit[1:]).strip()" } else if strings.Contains(param, "queries=") { queryParserCode = "\n if len(queries) > 0:\n if isinstance(queries, dict):\n params=queries\n else:\n if queries[0] == \"?\" or queries[0] == \"&\":\n queries = queries[1:len(queries)]\n if queries[len(queries)-1] == \"?\" or queries[len(queries)-1] == \"&\":\n queries = queries[0:-1]\n for query in queries.split(\"&\"):\n if isinstance(query, list) or isinstance(query, dict):\n try:\n query = json.dumps(query)\n except:\n pass\n if '=' in query:\n headersplit=query.split('=')\n params[requests.utils.quote(headersplit[0].strip())] = requests.utils.quote(headersplit[1].strip())\n else:\n params[requests.utils.quote(query.strip())] = None\n params = '&'.join([k if v is None else f\"{k}={v}\" for k, v in params.items()])" } else { if !strings.Contains(url, fmt.Sprintf("{%s}", param)) { queryData += fmt.Sprintf(` if %s: if isinstance(%s, list) or isinstance(%s, dict): try: %s = json.dumps(%s) except: pass params[requests.utils.quote("%s")] = requests.utils.quote(%s)`, param, param, param, param, param, param, param) } } } } bodyParameter := "" bodyAddin := "" bodyFormatter := "" postParameters := []string{"post", "patch", "put", "delete"} for _, item := range postParameters { if method == item { bodyParameter = ", body=\"\"" bodyAddin = ", data=body" // FIXME: Does JSON data work? bodyFormatter = "try:\n body = \" \".join(body.strip().split()).encode(\"utf-8\")\n except:\n pass" } } preparedHeaders := "request_headers={}" if len(headers) > 0 { if method == "post" && len(fileField) > 0 { } else { preparedHeaders = "request_headers={" for count, header := range headers { headerSplit := strings.Split(header, "=") added := false if len(headerSplit) == 2 { if strings.Contains(preparedHeaders, headerSplit[0]) { continue } headerSplit[0] = strings.Replace(headerSplit[0], "\"", "", -1) headerSplit[0] = strings.Replace(headerSplit[0], "'", "", -1) headerSplit[1] = strings.Replace(headerSplit[1], "\"", "", -1) headerSplit[1] = strings.Replace(headerSplit[1], "'", "", -1) preparedHeaders += fmt.Sprintf(`"%s": "%s"`, headerSplit[0], headerSplit[1]) added = true } if count != len(headers)-1 && added { preparedHeaders += "," } } preparedHeaders += "}" } } fileBalance := "" fileAdder := `` fileGrabber := `` fileParameter := `` contentTypeRemoval := "pass" bodyParsing := "try:\n body = json.dumps(body)\n except:\n pass" if method == "post" && len(fileField) > 0 { fileParameter = ", file_id" //fileGrabber = "filedata = self.get_file(file_id)\n print(f\"FILEDATA: {filedata}\")" fileGrabber = "filedata = self.get_file(file_id)" contentTypeRemoval = "del request_headers[contentType]" // This indentation is confusing (but correct) ROFL fileAdder = fmt.Sprintf(`if not filedata["success"]: return {"success": False, "reason": f"{file_id} is not a valid File ID"} files = {"%s": (filedata["filename"], filedata["data"])}`, fileField) fileBalance = ", files=files" bodyParsing = "" } // Removes duplicate file IDs if strings.Contains(parameterData, `, file_id=""`) && strings.Contains(fileParameter, ", file_id") { parameterData = strings.Replace(parameterData, `, file_id=""`, "", -1) } else if strings.Contains(parameterData, `, file_id`) && strings.Contains(fileParameter, ", file_id") { parameterData = strings.Replace(parameterData, ", file_id", "", -1) } if swagger.Components.SecuritySchemes != nil { for key, value := range swagger.Components.SecuritySchemes { if ArrayContains(reservedKeys, key) { continue } //parsedKey := strings.Replace(key, "-", "_", -1) parsedKey := FixFunctionName(key, "", true) if value.Value.In == "header" { queryString += fmt.Sprintf(", %s=\"\"", parsedKey) if len(extraHeaders) > 0 { extraHeaders += "\n " } extraHeaders += fmt.Sprintf(`if %s != " ": request_headers["%s"] = %s`, parsedKey, key, parsedKey) } else if value.Value.In == "query" { //log.Printf("Handling extra queries for %#v", parsedKey) if strings.Contains(parsedKey, "=") { parsedKey = strings.Split(parsedKey, "=")[0] } queryString += fmt.Sprintf(", %s=\"\"", parsedKey) if len(extraQueries) > 0 { extraQueries += "\n " } extraQueries += fmt.Sprintf(`if %s != " ": params["%s"] = %s`, parsedKey, key, parsedKey) } else { //log.Printf("[WARNING] Can't handle type %s", value.Value.In) } } } // Extra param for url if it's changeable // Extra param for authentication scheme(s) // The last weird one is the body.. Tabs & spaces sucks. parsedParameters := fmt.Sprintf("%s%s%s%s%s%s%s", authenticationParameter, urlParameter, fileParameter, parameterData, queryString, bodyParameter, verifyParam, ) // Dedup parameters parsedParametersSplit := strings.Split(parsedParameters, ",") newParameters := []string{} usedParams := []string{} for _, param := range parsedParametersSplit { param = strings.Trim(param, " ") if param == "" { continue } paramsplit := strings.Split(param, "=") if len(paramsplit) > 1 { param = paramsplit[0] } if !ArrayContains(usedParams, param) { usedParams = append(usedParams, param) if len(paramsplit) > 1 { param = strings.Join(paramsplit, "=") } newParameters = append(newParameters, param) } } parsedParameters = strings.Join(newParameters, ", ") // Handles default return value handleFileString := "if not to_file:\n return self.prepare_response(ret)\n\n return ret.text" parsedDataCurlParser := "" if method == "post" || method == "patch" || method == "put" || method == "delete" { parsedDataCurlParser = `parsed_curl_command += f""" -d '{body}'""" if isinstance(body, str) else f""" -d '{body.decode("utf-8")}'"""` } // Makes sure to reformat references if !strings.HasPrefix(parsedParameters, ",") { parsedParameters = fmt.Sprintf(", %s", parsedParameters) } data := fmt.Sprintf(` def %s(self%s): print(f"Started function %s") params={} %s url=f"%s%s" %s %s %s %s %s %s %s %s %s %s if str(to_file).lower() == "true": to_file = True else: to_file = False if "http:/" in url and not "http://" in url: url = url.replace("http:/", "http://", -1) if "https:/" in url and not "https://" in url: url = url.replace("https:/", "https://", -1) if "http:///" in url: url = url.replace("http:///", "http://", -1) if "https:///" in url: url = url.replace("https:///", "https://", -1) if not "http://" in url and not "http" in url: url = f"http://{url}" %s found = False contentType = "" for key, value in request_headers.items(): if key.lower() == "user-agent": found = True if key.lower() == "content-type": contentType = key if len(contentType) > 0: %s if not found: request_headers["User-Agent"] = "Shuffle Automation" try: #parsed_headers = [sys.stdout.write(f" -H \"{key}: {value}\"") for key, value in request_headers.items()] parsed_headers = "" parsed_curl_command = f"curl -X%s {url} {parsed_headers}" %s self.action["parameters"].append({ "name": "shuffle_request_url", "value": f"{url}", }) self.action["parameters"].append({ "name": "shuffle_request_curl", "value": f"{parsed_curl_command}", }) self.action["parameters"].append({ "name": "shuffle_request_headers", "value": f"{json.dumps(parsed_headers)}", }) self.action_result["action"] = self.action print("[DEBUG] Updated values in self.action_result from OpenAPI app! (1)") except Exception as e: print(f"[WARNING] Something went wrong when adding extra returns (1). {e}") session = requests.Session() ret = session.%s(url, headers=request_headers, params=params%s%s%s%s) try: found = False for item in self.action["parameters"]: if item["name"] == "shuffle_response_status": found = True break if not found: self.action["parameters"].append({ "name": "shuffle_response_status", "value": f"{ret.status_code}", }) self.action["parameters"].append({ "name": "shuffle_response_length", "value": f"{len(ret.text)}", }) self.action["parameters"].append({ "name": "shuffle_request_cookies", "value": f"{json.dumps(session.cookies.get_dict())}", }) print("[DEBUG] Updated values in self.action_result from OpenAPI app! (2)") except Exception as e: print(f"[WARNING] Something went wrong when adding extra returns (2). {e}") if to_file: # If content encoding or transfer encoding is base64, decode it if ("content-encoding" in ret.headers.keys() and "base64" in ret.headers["content-encoding"].lower()) or ("transfer-encoding" in ret.headers.keys() and "base64" in ret.headers["transfer-encoding"].lower()) or ("content-transfer-encoding" in ret.headers.keys() and "base64" in ret.headers["content-transfer-encoding"].lower()): print("[DEBUG] Content encoding is base64, decoding it") ret.content = base64.b64decode(ret.content) filedata = { "filename": "response", "data": ret.content, } fileret = self.set_files([filedata]) if len(fileret) == 1: return {"success": True, "file_id": fileret[0], "status": ret.status_code} return fileret %s `, functionname, parsedParameters, functionname, preparedHeaders, urlInline, url, verifyWrapper, extraHeaders, extraQueries, authenticationSetup, headerParserCode, queryData, queryParserCode, bodyFormatter, fileGrabber, fileAdder, bodyParsing, contentTypeRemoval, strings.ToUpper(method), parsedDataCurlParser, method, authenticationAddin, bodyAddin, verifyAddin, fileBalance, handleFileString, ) // Use lowercase when checking if strings.Contains(strings.ToLower(functionname), "get_list_all_issues") { log.Printf("\n%s", data) } return functionname, data } func GetCustomActionCode(swagger *openapi3.Swagger, api WorkflowApp) string { authenticationParameter := "" authenticationSetup := "" authenticationAddin := "" if swagger.Components.SecuritySchemes != nil { if swagger.Components.SecuritySchemes["BearerAuth"] != nil { authenticationParameter = ", apikey" authenticationSetup = "if apikey != \" \" and not apikey.startswith(\"Bearer\"): parsed_headers[\"Authorization\"] = f\"Bearer {apikey}\"" } else if swagger.Components.SecuritySchemes["BasicAuth"] != nil { authenticationParameter = ", username_basic, password_basic" authenticationSetup = "auth=None\n if username_basic or password_basic:\n if \"Authorization\" not in headers and \"Basic\" not in headers and not \"Bearer\" in headers:\n auth = requests.auth.HTTPBasicAuth(username_basic, password_basic)" authenticationAddin = ", auth=auth" } else if swagger.Components.SecuritySchemes["ApiKeyAuth"] != nil { authenticationParameter = ", apikey" if swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.In == "header" { authenticationSetup = fmt.Sprintf(`if apikey != " ": parsed_headers["%s"] = apikey`, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name) if len(swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description) > 0 { trimmedDescription := strings.Trim(swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, " ") authenticationSetup = fmt.Sprintf("if apikey != \" \":\n if apikey.startswith(\"%s\"):\n parsed_headers[\"%s\"] = apikey\n else:\n apikey = apikey.replace(\"%s\", \"\", -1).strip()\n parsed_headers[\"%s\"] = f\"%s{apikey}\"", trimmedDescription, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description) } } else if swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.In == "query" { //authenticationSetup = fmt.Sprintf("if apikey != \" \": parsed_queries[\"%s\"] = requests.utils.quote(apikey)", swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name) trimmedDescription := strings.Trim(swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, " ") authenticationSetup = fmt.Sprintf("if apikey != \" \":\n if apikey.startswith(\"%s\"):\n parsed_queries[\"%s\"] = requests.utils.quote(apikey)\n else:\n apikey = apikey.replace(\"%s\", \"\", -1).strip()\n parsed_queries[\"%s\"] = requests.utils.quote(f\"%s{apikey}\")", trimmedDescription, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Name, swagger.Components.SecuritySchemes["ApiKeyAuth"].Value.Description) } } else if swagger.Components.SecuritySchemes["Oauth2"] != nil { authenticationParameter = ", access_token" authenticationSetup = fmt.Sprintf("if access_token != \" \": parsed_headers[\"Authorization\"] = f\"Bearer {access_token}\"\n #parsed_headers[\"Content-Type\"] = \"application/json\"") } else if swagger.Components.SecuritySchemes["jwt"] != nil { authenticationParameter = ", username_basic, password_basic" authenticationSetup = fmt.Sprintf("authret = requests.get(f\"{url}%s\", headers=parsed_headers, auth=(username_basic, password_basic), verify=False)\n if 'access_token' in authret.text:\n parsed_headers[\"Authorization\"] = f\"Bearer {authret.json()['access_token']}\"\n elif 'jwt' in authret.text:\n parsed_headers[\"Authorization\"] = f\"Bearer {authret.json()['jwt']}\"\n elif 'accessToken' in authret.text:\n parsed_headers[\"Authorization\"] = f\"Bearer {authret.json()['accessToken']}\"\n else:\n parsed_headers[\"Authorization\"] = f\"Bearer {authret.text}\"\n print(f\"Found Bearer auth: {authret.text}\")", api.Authentication.TokenUri) } } pythonCode := fmt.Sprintf(` def fix_url(self, url, path=False): if "hhttp" in url: url = url.replace("hhttp", "http") if url.startswith("http//"): url = url.replace("http//", "http://") if url.startswith("https//"): url = url.replace("https//", "https://") if "http:/" in url and not "http://" in url: url = url.replace("http:/", "http://", -1) if "https:/" in url and not "https://" in url: url = url.replace("https:/", "https://", -1) if "http:///" in url: url = url.replace("http:///", "http://", -1) if "https:///" in url: url = url.replace("https:///", "https://", -1) if not path and not "http://" in url and not "http" in url: url = f"http://{url}" return url def checkverify(self, verify): if str(verify).lower().strip() == "false": return False elif verify is None: return False elif verify: return True elif not verify: return False else: return True def is_valid_method(self, method): valid_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"] method = method.upper() if method in valid_methods: return method else: raise ValueError(f"Invalid HTTP method: {method}") def parse_headers(self, headers): parsed_headers = {} if headers: split_headers = headers.split("\n") self.logger.info(split_headers) for header in split_headers: if ":" in header: splititem = ":" elif "=" in header: splititem = "=" else: continue splitheader = header.split(splititem) if len(splitheader) >= 2: parsed_headers[splitheader[0].strip()] = splititem.join( splitheader[1:] ).strip() else: continue return parsed_headers def parse_queries(self, queries): parsed_queries = {} if not queries: return parsed_queries cleaned_queries = queries.strip() if not cleaned_queries: return parsed_queries cleaned_queries = " ".join(cleaned_queries.split()) splitted_queries = cleaned_queries.split("&") for query in splitted_queries: if not query: continue querysplit = query.split("=") if len(querysplit) == 0: parsed_queries[query.strip()] = "" else: queryvalue = "=".join(querysplit[1:]) parsed_queries[querysplit[0].strip()] = queryvalue.strip() return parsed_queries def prepare_response(self, request): try: parsedheaders = {} for key, value in request.headers.items(): parsedheaders[key] = value cookies = {} if request.cookies: for key, value in request.cookies.items(): cookies[key] = value jsondata = request.text try: jsondata = json.loads(jsondata) except: pass parseddata = { "status": request.status_code, "body": jsondata, "url": request.url, "headers": parsedheaders, "cookies":cookies, "success": True, } return json.dumps(parseddata) except Exception as e: print(f"[WARNING] Failed in request: {e}") return request.text def custom_action(self%s, method="", url="", headers="", queries="", path="", ssl_verify=False, body=""): url = self.fix_url(url) try: method = self.is_valid_method(method) except ValueError as e: self.logger.error(e) return {"error": str(e)} if not path: path = "/" path = self.fix_url(path, path=True) if path and path.startswith(url): path = path.replace(url, "", 1) if path and not path.startswith('/'): path = '/' + path url += path parsed_headers = {} parsed_queries = {} %s # Allows overwriting of existing headers with custom input ones additional_headers = self.parse_headers(headers) try: parsed_headers.update(additional_headers) except Exception as e: print(f"Header parse error: {e}") additional_queries = self.parse_queries(queries) try: parsed_queries.update(additional_queries) except Exception as e: print(f"Query parse error: {e}") ssl_verify = self.checkverify(ssl_verify) if isinstance(body, dict): try: body = json.dumps(body) except json.JSONDecodeError as e: self.logger.error(f"error : {e}") return {"error: Invalid JSON format for request body"} try: response = requests.request(method, url, headers=parsed_headers, params=parsed_queries, data=body, verify=ssl_verify%s) #response.raise_for_status() return self.prepare_response(response) except requests.RequestException as e: self.logger.error(f"Request failed: {e}") return {"error": f"Request failed: {e}"} `, authenticationParameter, authenticationSetup, authenticationAddin) return pythonCode } func AddCustomAction(swagger *openapi3.Swagger, api WorkflowApp) (WorkflowAppAction, string) { parameters := []WorkflowAppActionParameter{} pyCode := GetCustomActionCode(swagger, api) securitySchemes := swagger.Components.SecuritySchemes if securitySchemes != nil { if securitySchemes["BearerAuth"] != nil { parameters = append(parameters, WorkflowAppActionParameter{ Name: "apikey", Description: "The apikey to use", Multiline: false, Required: true, Example: "The API key to use. Space = skip", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } else if securitySchemes["ApiKeyAuth"] != nil { extraParam := WorkflowAppActionParameter{ Name: "apikey", Description: "The apikey to use", Multiline: false, Required: true, Example: "**********", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, } if len(securitySchemes["ApiKeyAuth"].Value.Description) > 0 { extraParam.Description = fmt.Sprintf("Start with %s", securitySchemes["ApiKeyAuth"].Value.Description) } parameters = append(parameters, extraParam) } else if securitySchemes["jwt"] != nil { parameters = append(parameters, WorkflowAppActionParameter{ Name: "username_basic", Description: "The username to use", Multiline: false, Required: true, Example: "The username to use", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "password_basic", Description: "The password to use", Multiline: false, Required: true, Example: "***********", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } else if securitySchemes["BasicAuth"] != nil { parameters = append(parameters, WorkflowAppActionParameter{ Name: "username_basic", Description: "The username to use", Multiline: false, Required: true, Example: "The username to use", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "password_basic", Description: "The password to use", Multiline: false, Required: true, Example: "***********", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } } parameters = append(parameters, WorkflowAppActionParameter{ Name: "method", Description: "The http method to use", Multiline: false, Required: true, Options: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, Example: "GET", Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "url", Description: "The URL of the API", Multiline: false, Required: true, Example: "https://api.example.com", Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "path", Description: "the path to add to the base url", Multiline: false, Required: false, Example: "/users/profile", Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "headers", Description: "Add or edit headers", Multiline: true, Required: false, Example: "Content-Type:application/json\nAccept:application/json", Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "queries", Description: "Add or edit queries", Multiline: true, Required: false, Example: "view=basic&redirect=test", Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "ssl_verify", Description: "Check if you want to verify request", Multiline: false, Options: []string{"False", "True"}, Required: false, Example: "False", Schema: SchemaDefinition{ Type: "string", }, }) parameters = append(parameters, WorkflowAppActionParameter{ Name: "body", Description: "The body to use", Multiline: true, Required: false, Example: `{"username": "example_user", "email": "user@example.com"}`, Schema: SchemaDefinition{ Type: "string", }, }) action := WorkflowAppAction{ Description: "add a custom action for your app", Name: "custom_action", NodeType: "action", Environment: "Shuffle", Parameters: parameters, } action.Returns.Schema.Type = "string" return action, pyCode } func GenerateYaml(swagger *openapi3.Swagger, newmd5 string) (*openapi3.Swagger, WorkflowApp, []string, error) { api := WorkflowApp{} //log.Printf("%#v", swagger.Info) if swagger.Info == nil { return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Info can't be empty.") } if len(swagger.Info.Title) == 0 { return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Info.Title can't be empty.") } if len(swagger.Servers) == 0 { //return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Servers can't be empty. Add 'servers':[{'url':'hostname.com'}'") //return swagger, WorkflowApp{}, []string{}, errors.New("Swagger.Servers can't be empty. Add 'servers':[{'url':'hostname.com'}'") swagger.Servers = openapi3.Servers{ &openapi3.Server{ URL: "https://hostname.com", }, } } api.Name = swagger.Info.Title api.Description = swagger.Info.Description // FIXME: Versioning issue? api.ID = newmd5 //uuid.NewV4().String() api.IsValid = true api.Link = swagger.Servers[0].URL // host does not exist lol if strings.HasSuffix(api.Link, "/") { api.Link = api.Link[:len(api.Link)-1] } example := "https://api-url" if len(api.Link) > 0 { example = api.Link linkSplit := strings.Split(api.Link, "/") if len(linkSplit) > 3 { example = strings.Join(linkSplit[0:3], "/") } //log.Printf("EXAMPLE: %s", example) } api.AppVersion = "1.1.0" api.Environment = "Shuffle" api.SmallImage = "" api.LargeImage = "" api.Sharing = false api.Verified = false api.Tested = false api.Invalid = false api.PrivateID = newmd5 api.Generated = true api.Activated = true // Setting up security schemes extraParameters := []WorkflowAppActionParameter{} if val, ok := swagger.Info.ExtensionProps.Extensions["x-logo"]; ok { j, err := json.Marshal(&val) if err == nil { if j[0] == 0x22 && j[len(j)-1] == 0x22 { j = j[1 : len(j)-1] } //log.Printf("%s", j) api.SmallImage = string(j) api.LargeImage = string(j) } } // Jesus what a clusterfuck. // Handles parsing of categories from OpenApi3 custom field if val, ok := swagger.Info.ExtensionProps.Extensions["x-categories"]; ok { //log.Printf("Categories: %#v", val) j, err := json.Marshal(&val) if err == nil { if j[0] == 0x22 && j[len(j)-1] == 0x22 { j = j[1 : len(j)-1] } parsedCategories := fmt.Sprintf(`{"categories": %s}`, string(j)) type parsed struct { Categories []string `json:"categories"` } var parse parsed err := json.Unmarshal([]byte(parsedCategories), &parse) if err != nil { log.Printf("Failed unmarshaling categories: %s", err) } else { api.Categories = parse.Categories } } } if len(swagger.Tags) > 0 { newTags := []string{} for _, tag := range swagger.Tags { newTags = append(newTags, tag.Name) } api.Tags = newTags } securitySchemes := swagger.Components.SecuritySchemes reservedKeys := []string{"BearerAuth", "ApiKeyAuth", "Oauth2", "BasicAuth", "jwt"} if securitySchemes != nil { //log.Printf("%#v", securitySchemes) api.Authentication = Authentication{ Required: true, Parameters: []AuthenticationParams{}, } // Used for python code generation lol // Not sure how this should work with oauth if securitySchemes["BearerAuth"] != nil { api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "apikey", Value: "", Example: "******", Description: securitySchemes["BearerAuth"].Value.Description, In: securitySchemes["BearerAuth"].Value.In, Scheme: securitySchemes["BearerAuth"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["BearerAuth"].Value.Scheme, }, }) //log.Printf("HANDLE BEARER AUTH") extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: "apikey", Description: "The apikey to use", Multiline: false, Required: true, Example: "The API key to use. Space = skip", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } else if securitySchemes["ApiKeyAuth"] != nil { //log.Printf("AUTH:%#v", securitySchemes["ApiKeyAuth"].Value) newAuthParam := AuthenticationParams{ Name: "apikey", Value: "", Example: "******", Description: securitySchemes["ApiKeyAuth"].Value.Description, In: securitySchemes["ApiKeyAuth"].Value.In, Scheme: securitySchemes["ApiKeyAuth"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["ApiKeyAuth"].Value.Scheme, }, } //Example: securitySchemes["ApiKeyAuth"].Value.Example, //log.Printf("HANDLE APIKEY AUTH") extraParam := WorkflowAppActionParameter{ Name: "apikey", Description: "The apikey to use", Multiline: false, Required: true, Example: "**********", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, } if len(securitySchemes["ApiKeyAuth"].Value.Description) > 0 { //log.Printf("UPDATING AUTH!") extraParam.Description = fmt.Sprintf("Start with %s", securitySchemes["ApiKeyAuth"].Value.Description) newAuthParam.Description = fmt.Sprintf("Start with %s", securitySchemes["ApiKeyAuth"].Value.Description) } api.Authentication.Parameters = append(api.Authentication.Parameters, newAuthParam) extraParameters = append(extraParameters, extraParam) } else if securitySchemes["Oauth2"] != nil { api.Authentication.Type = "oauth2" if val, ok := securitySchemes["Oauth2"].Value.ExtensionProps.Extensions["flow"]; ok { newValue := string(fmt.Sprintf("%s", string(val.(json.RawMessage)))) //log.Printf("DATA: %s", newValue) var parsed Oauth2Openapi err := json.Unmarshal([]byte(newValue), &parsed) if err != nil { log.Printf("[WARNING] Failed to unmarshal Oauth2 data for app %s", api.Name) } else { log.Printf("[DEBUG] Set up Oauth2 config for app %s during generation", api.Name) api.Authentication.Type = "oauth2" api.Authentication.RedirectUri = parsed.AuthorizationCode.AuthorizationUrl api.Authentication.TokenUri = parsed.AuthorizationCode.TokenUrl api.Authentication.RefreshUri = parsed.AuthorizationCode.RefreshUrl api.Authentication.Scope = parsed.AuthorizationCode.Scopes } } else { log.Printf("[ERROR] No Oauth2 data to parse for app %s - bad parsing?", api.Name) return swagger, WorkflowApp{}, []string{}, errors.New("Missing Oauth2 refreshUrl, scope, authorization URL or Token URL") } if val, ok := securitySchemes["Oauth2"].Value.ExtensionProps.Extensions["x-grant-type"]; ok { // Make val from json.rawMessage into a string newValue := string(fmt.Sprintf("%s", string(val.(json.RawMessage)))) // Check if quotes on it if len(newValue) > 2 && newValue[0] == '"' && newValue[len(newValue)-1] == '"' { newValue = newValue[1 : len(newValue)-1] } // November 2023: password & client_credentials // Fix mar 2024: set type to oauth2-app if len(newValue) > 0 { api.Authentication.GrantType = newValue api.Authentication.Type = "oauth2-app" } log.Printf("[DEBUG] Got special app build grant type: %s", newValue) } api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "client_id", Value: "", Example: "client_id", Description: securitySchemes["Oauth2"].Value.Description, In: securitySchemes["Oauth2"].Value.In, Scheme: securitySchemes["Oauth2"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["Oauth2"].Value.Scheme, }, }) /* api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "client_id", Value: "", Example: "client_id", Description: securitySchemes["Oauth2"].Value.Description, In: securitySchemes["Oauth2"].Value.In, Scheme: securitySchemes["Oauth2"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["Oauth2"].Value.Scheme, }, }) */ api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "client_secret", Value: "", Example: "client_secret", Description: securitySchemes["Oauth2"].Value.Description, In: securitySchemes["Oauth2"].Value.In, Scheme: securitySchemes["Oauth2"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["Oauth2"].Value.Scheme, }, }) // Check for securitySchemes //} else if securitySchemes["Oauth2"] != nil { } else if securitySchemes["jwt"] != nil { if len(securitySchemes["jwt"].Value.In) > 0 { api.Authentication.TokenUri = securitySchemes["jwt"].Value.In } api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "username_basic", Value: "", Example: "username", Description: "", In: "", Scheme: "", Schema: SchemaDefinition{ Type: securitySchemes["jwt"].Value.Scheme, }, }) api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "password_basic", Value: "", Example: "*****", Description: "", In: "", Scheme: "", Schema: SchemaDefinition{ Type: securitySchemes["jwt"].Value.Scheme, }, }) extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: "username_basic", Description: "The username to use", Multiline: false, Required: true, Example: "The username to use", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: "password_basic", Description: "The password to use", Multiline: false, Required: true, Example: "***********", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } else if securitySchemes["BasicAuth"] != nil { api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "username_basic", Value: "", Example: "username", Description: securitySchemes["BasicAuth"].Value.Description, In: securitySchemes["BasicAuth"].Value.In, Scheme: securitySchemes["BasicAuth"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["BasicAuth"].Value.Scheme, }, }) api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "password_basic", Value: "", Example: "*****", Description: securitySchemes["BasicAuth"].Value.Description, In: securitySchemes["BasicAuth"].Value.In, Scheme: securitySchemes["BasicAuth"].Value.Scheme, Schema: SchemaDefinition{ Type: securitySchemes["BasicAuth"].Value.Scheme, }, }) extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: "username_basic", Description: "The username to use", Multiline: false, Required: true, Example: "The username to use", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: "password_basic", Description: "The password to use", Multiline: false, Required: true, Example: "***********", Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } } for key, value := range securitySchemes { if ArrayContains(reservedKeys, key) { continue } //log.Printf("%s: %#v", key, value.Value) exampleData := fmt.Sprintf("Extra auth field (%s)", value.Value.In) api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: key, Value: "", Example: exampleData, Description: exampleData, In: value.Value.In, Scheme: "", Schema: SchemaDefinition{ Type: "string", }, }) extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: key, Multiline: false, Required: true, Description: exampleData, Example: exampleData, Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) } // Adds a link parameter if it's not already defined api.Authentication.Parameters = append(api.Authentication.Parameters, AuthenticationParams{ Name: "url", Description: "The URL of the app", Value: example, Example: example, Multiline: false, Required: true, Schema: SchemaDefinition{ Type: "string", }, }) extraParameters = append(extraParameters, WorkflowAppActionParameter{ Name: "url", Description: "The URL of the API", Value: example, Example: example, Multiline: false, Required: true, Configuration: true, Schema: SchemaDefinition{ Type: "string", }, }) // This is the python code to be generated // Could just as well be go at this point lol pythonFunctions := []string{} optionalParameters := []WorkflowAppActionParameter{} headerParam := WorkflowAppActionParameter{ Name: "headers", Description: "Add or edit headers", Multiline: true, Required: false, Example: "Content-Type=application/json\nAccept=application/json\r\n", Schema: SchemaDefinition{ Type: "string", }, } optionalParameters = append(optionalParameters, headerParam) optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ Name: "queries", Description: "Add or edit queries", Multiline: true, Required: false, Example: "view=basic&redirect=test", Schema: SchemaDefinition{ Type: "string", }, }) // Not validating by default, due to lots of people having issues with // SSL things optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ Name: "ssl_verify", Description: "Check if you want to verify request", Multiline: false, Required: false, Example: "True", Options: []string{ "False", "True", }, Schema: SchemaDefinition{ Type: "string", }, }) optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ Name: "to_file", Description: "Choose if we should write the result straight to a file or not", Multiline: false, Required: false, Example: "False", Options: []string{ "False", "True", }, Schema: SchemaDefinition{ Type: "string", }, }) // Fixing parameters with : newExtraParams := []WorkflowAppActionParameter{} newOptionalParams := []WorkflowAppActionParameter{} for _, param := range extraParameters { param.Name = FixParamname(param.Name) newExtraParams = append(newExtraParams, param) } for _, param := range optionalParameters { param.Name = FixParamname(param.Name) newOptionalParams = append(newOptionalParams, param) } extraParameters = newExtraParams optionalParameters = newOptionalParams //Verified bool `json:"verified" yaml:"verified" required:false datastore:"verified"` for actualPath, path := range swagger.Paths { //actualPath = strings.Replace(actualPath, ".", "", -1) actualPath = strings.Replace(actualPath, " ", "_", -1) actualPath = strings.Replace(actualPath, "\\", "", -1) if !api.Invalid && strings.HasPrefix(actualPath, "tmp") { log.Printf("[WARNING] Set api %s to invalid because of path %s", swagger.Info.Title, actualPath) api.Invalid = true } // FIXME: Handle everything behind questionmark (?) with dots as well. // https://godoc.org/github.com/getkin/kin-openapi/openapi3#PathItem if path.Get != nil { action, curCode := HandleGet(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } if path.Connect != nil { action, curCode := HandleConnect(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } if path.Head != nil { action, curCode := HandleHead(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } if path.Delete != nil { action, curCode := HandleDelete(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } if path.Post != nil { action, curCode := HandlePost(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } if path.Patch != nil { action, curCode := HandlePatch(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } if path.Put != nil { action, curCode := HandlePut(swagger, api, extraParameters, path, actualPath, optionalParameters) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) } // Has to be here because its used differently above. // FIXING this is done during export instead? //log.Printf("OLDPATH: %s", actualPath) //if strings.Contains(actualPath, "?") { // actualPath = strings.Split(actualPath, "?")[0] //} //log.Printf("NEWPATH: %s", actualPath) //newPaths[actualPath] = path } action, curCode := AddCustomAction(swagger, api) api.Actions = append(api.Actions, action) pythonFunctions = append(pythonFunctions, curCode) return swagger, api, pythonFunctions, nil } // FIXME - have this give a real version? func VerifyApi(api WorkflowApp) WorkflowApp { if api.AppVersion == "" { api.AppVersion = "1.0.0" } return api } func GetBasePython() string { baseString := `import requests import json import urllib3 from shuffle_sdk import AppBase class %s(AppBase): """ Autogenerated class by Shuffler """ __version__ = "%s" app_name = "%s" def __init__(self, redis, logger, console_logger=None): self.verify = False urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) super().__init__(redis, logger, console_logger) %s if __name__ == "__main__": %s.run() ` // From old when we actually used asyncio (: //#asyncio.run(%s.run(), debug=True) return baseString } func DumpPythonGCP(ctx context.Context, client *storage.Client, basePath, name, version string, pythonFunctions []string, bucketName string) (string, error) { parsedCode := fmt.Sprintf(GetBasePython(), name, version, name, strings.Join(pythonFunctions, "\n"), name) // Create bucket handle bucket := client.Bucket(bucketName) obj := bucket.Object(fmt.Sprintf("%s/src/app.py", basePath)) w := obj.NewWriter(ctx) if _, err := fmt.Fprintln(w, parsedCode); err != nil { return "", err } // Close, just like writing a file. if err := w.Close(); err != nil { return "", err } return parsedCode, nil } func DumpPython(basePath, name, version string, pythonFunctions []string) (string, error) { //log.Printf("%#v", api) //log.Printf(strings.Join(pythonFunctions, "\n")) parsedCode := fmt.Sprintf(GetBasePython(), name, version, name, strings.Join(pythonFunctions, "\n"), name) err := ioutil.WriteFile(fmt.Sprintf("%s/src/app.py", basePath), []byte(parsedCode), os.ModePerm) if err != nil { return "", err } //fmt.Println(parsedCode) //log.Println(string(data)) return parsedCode, nil } func DumpApiGCP(ctx context.Context, client *storage.Client, swagger *openapi3.Swagger, basePath string, api WorkflowApp, bucketName string) error { //log.Printf("%#v", api) data, err := yaml.Marshal(api) if err != nil { log.Printf("Error with yaml marshal: %s", err) return err } // Create bucket handle bucket := client.Bucket(bucketName) obj := bucket.Object(fmt.Sprintf("%s/app.yaml", basePath)) w := obj.NewWriter(ctx) if _, err := fmt.Fprintln(w, string(data)); err != nil { return err } // Close, just like writing a file. if err := w.Close(); err != nil { return err } openapidata, err := yaml.Marshal(swagger) if err != nil { log.Printf("Error with yaml marshal: %s", err) return err } obj = bucket.Object(fmt.Sprintf("%s/openapi.yaml", basePath)) //log.Println(string(openapidata)) w = obj.NewWriter(ctx) if _, err := fmt.Fprintln(w, string(openapidata)); err != nil { return err } // Close, just like writing a file. if err := w.Close(); err != nil { return err } //log.Println(string(data)) return nil } func DumpApi(basePath string, api WorkflowApp) error { //log.Printf("%#v", api) data, err := yaml.Marshal(api) if err != nil { log.Printf("Error with yaml marshal: %s", err) return err } err = ioutil.WriteFile(fmt.Sprintf("%s/api.yaml", basePath), []byte(data), os.ModePerm) if err != nil { return err } //log.Println(string(data)) return nil } func GetRunnerOnprem(classname string) string { return fmt.Sprintf(` # Run the actual thing after we've checked params def run(request): print("Started execution!") action = request.get_json() #print(action) #print(type(action)) authorization_key = action.get("authorization") current_execution_id = action.get("execution_id") if action and "name" in action and "app_name" in action: %s.run(action=action) return f'Attempting to execute function {action["name"]} in app {action["app_name"]}' else: return f'Invalid action' `, classname) } func GetRunnerGCP(classname string) string { return fmt.Sprintf(` # Run the actual thing after we've checked params def run(request): try: action = request.get_json(force=True) except: return f'Error parsing JSON' if action == None: return f'No JSON detected' #authorization_key = action.get("authorization") #current_execution_id = action.get("execution_id") if action and "name" in action and "app_name" in action: %s.run(action=action) return f'Attempting to execute function {action["name"]} in app {action["app_name"]}' return f'Action ran!' `, classname) } func DeployAppToDatastore(ctx context.Context, workflowapp WorkflowApp) error { err := SetWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err != nil { log.Printf("[ERROR] Failed setting workflowapp: %s", err) return err } else { log.Printf("[INFO] Added %s:%s to the database", workflowapp.Name, workflowapp.AppVersion) } return nil } func FixParamname(paramname string) string { paramname = strings.Replace(paramname, ".", "", -1) paramname = strings.Replace(paramname, ":", "", -1) paramname = strings.Replace(paramname, ",", "", -1) paramname = strings.Replace(paramname, ".", "", -1) paramname = strings.Replace(paramname, "&", "", -1) paramname = strings.Replace(paramname, "/", "", -1) paramname = strings.Replace(paramname, "\\", "", -1) paramname = strings.Replace(paramname, "!", "", -1) paramname = strings.Replace(paramname, "?", "", -1) paramname = strings.Replace(paramname, "@", "", -1) paramname = strings.Replace(paramname, "#", "", -1) paramname = strings.Replace(paramname, "$", "", -1) paramname = strings.Replace(paramname, "&", "", -1) paramname = strings.Replace(paramname, "*", "", -1) paramname = strings.Replace(paramname, "(", "", -1) paramname = strings.Replace(paramname, ")", "", -1) paramname = strings.Replace(paramname, "[", "", -1) paramname = strings.Replace(paramname, "]", "", -1) paramname = strings.Replace(paramname, "{", "", -1) paramname = strings.Replace(paramname, "}", "", -1) paramname = strings.Replace(paramname, `"`, "", -1) paramname = strings.Replace(paramname, `'`, "", -1) paramname = strings.Replace(paramname, `|`, "", -1) paramname = strings.Replace(paramname, `~`, "", -1) paramname = strings.Replace(paramname, " ", "_", -1) paramname = strings.Replace(paramname, "-", "_", -1) return paramname } // FIXME: // https://docs.python.org/3.2/reference/lexical_analysis.html#identifiers // This is used to build the python functions. func FixFunctionName(functionName, actualPath string, lowercase bool) string { if len(functionName) == 0 { functionName = actualPath } validCharacters := []rune("_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") newname := "" for _, char := range functionName { if string(char) == " " { newname += "_" continue } for _, rune := range validCharacters { if char == rune { newname += string(char) break } } } functionName = newname if lowercase == true { functionName = strings.ToLower(functionName) } return functionName } // Returns a valid param name func ValidateParameterName(name string) string { invalid := []string{"False", "await", "else", "import", "pass", "None", "break", "except", "in", "raise", "True", "class", "finally", "is", "return", "and", "continue", "for", "lambda", "try", "as", "def", "from", "nonlocal", "while", "assert", "del", "global", "not", "with", "async", "elif", "if", "or", "yield", } newname := name for _, item := range invalid { if item == name { //log.Printf("%s is NOT a valid parameter name!", item) newname = fmt.Sprintf("%s_shuffle", item) break } } newname = strings.Replace(newname, " ", "_", -1) newname = strings.Replace(newname, ",", "_", -1) newname = strings.Replace(newname, ".", "_", -1) newname = strings.Replace(newname, "|", "_", -1) newname = strings.Replace(newname, "-", "_", -1) return newname } func HandleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm functionName := FixFunctionName(path.Connect.Summary, actualPath, true) //func FixParamname(paramname string) string { baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } newDesc := fmt.Sprintf("%s\n\n%s", path.Connect.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Connect", path.Connect.Summary), Label: fmt.Sprintf("%s", path.Connect.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Connect.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } action.Returns.Schema.Type = "string" handleFile := false //log.Println(path.Parameters) // Parameters: []WorkflowAppActionParameter{}, //firstQuery := true optionalQueries := []string{} parameters := []string{} headersFound := []string{} if len(path.Connect.Parameters) > 0 { for counter, param := range path.Connect.Parameters { if param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = strings.Replace(parsedName, "-", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Connect.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } //log.Printf("What: %#v", optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1]) //log.Printf("HI: %s", //optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-2]) // Removing newlines at the end if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] } log.Printf("%#v", optionalParameters[setIndex].Value) } } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "connect", parameters, optionalQueries, headersFound, "", api, handleFile) if len(functionname) > 0 { action.Name = functionname } return action, curCode } func HandleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm functionName := FixFunctionName(path.Get.Summary, actualPath, true) baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } newDesc := fmt.Sprintf("%s\n\n%s", path.Get.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Get", path.Get.Summary), Label: fmt.Sprintf("%s", path.Get.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Get.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } action.Returns.Schema.Type = "string" // Check if it should return as file (binary) // FIXME: Don't JUST specif text/plain to allow this. handleFile := false if strings.Contains(path.Get.Summary, "Download") { if defaultInfo, ok := path.Get.Responses["default"]; ok { if content, ok := defaultInfo.Value.Content["text/plain"]; ok { if content.Schema.Value.Type == "string" && content.Schema.Value.Format == "binary" { handleFile = true } } } } // Parameters: []WorkflowAppActionParameter{}, //firstQuery := true optionalQueries := []string{} // FIXME - remove this when authentication is properly introduced parameters := []string{} headersFound := []string{} if len(path.Get.Parameters) > 0 { for counter, param := range path.Get.Parameters { if param.Value == nil || param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Get.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) // Skipping simial if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] //optionalParameters[setIndex].Example = optionalParameters[setIndex].Example[0 : len(optionalParameters[setIndex].Example)-1] } //log.Printf("%#v", optionalParameters[setIndex].Value) } } else { //log.Printf("No headers found for %s", functionName) } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "get", parameters, optionalQueries, headersFound, "", api, handleFile) if len(functionname) > 0 { action.Name = functionname } return action, curCode } func HandleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm functionName := FixFunctionName(path.Head.Summary, actualPath, true) baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } newDesc := fmt.Sprintf("%s\n\n%s", path.Head.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Head", path.Head.Summary), Label: fmt.Sprintf("%s", path.Head.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Head.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } action.Returns.Schema.Type = "string" handleFile := false //log.Println(path.Parameters) // Parameters: []WorkflowAppActionParameter{}, //firstQuery := true optionalQueries := []string{} parameters := []string{} headersFound := []string{} if len(path.Head.Parameters) > 0 { for counter, param := range path.Head.Parameters { if param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Head.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } //log.Printf("What: %#v", optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1]) //log.Printf("HI: %s", //optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-2]) // Removing newlines at the end if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] } //log.Printf("%#v", optionalParameters[setIndex].Value) } } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "head", parameters, optionalQueries, headersFound, "", api, handleFile) if len(functionname) > 0 { action.Name = functionname } return action, curCode } func HandleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm functionName := FixFunctionName(path.Delete.Summary, actualPath, true) baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } newDesc := fmt.Sprintf("%s\n\n%s", path.Delete.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Delete", path.Delete.Summary), Label: fmt.Sprintf("%s", path.Delete.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Delete.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } if val, ok := path.Delete.ExtensionProps.Extensions["x-required-fields"]; ok { j, err := json.Marshal(&val) if err == nil { if j[0] == 0x22 && j[len(j)-1] == 0x22 { j = j[1 : len(j)-1] } } newValue := []string{} err = json.Unmarshal(j, &newValue) if err == nil { action.RequiredBodyFields = newValue //log.Printf("Setting required bodyfields: %#v", newValue) } else { log.Printf("[ERROR] Failed to unmarshal required bodyfields %s: %s", string(j), err) } } action.Returns.Schema.Type = "string" handleFile := false //log.Println(path.Parameters) // Parameters: []WorkflowAppActionParameter{}, //firstQuery := true optionalQueries := []string{} parameters := []string{} headersFound := []string{} if len(path.Delete.Parameters) > 0 { for counter, param := range path.Delete.Parameters { if param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Delete.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } //log.Printf("What: %#v", optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1]) //log.Printf("HI: %s", //optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-2]) // Removing newlines at the end if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] } //log.Printf("%#v", optionalParameters[setIndex].Value) } } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "delete", parameters, optionalQueries, headersFound, "", api, handleFile) if len(functionname) > 0 { action.Name = functionname } return action, curCode } func HandlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm //log.Printf("PATH: %s", actualPath) functionName := FixFunctionName(path.Post.Summary, actualPath, true) baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } newDesc := fmt.Sprintf("%s\n\n%s", path.Post.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Post", path.Post.Summary), Label: fmt.Sprintf("%s", path.Post.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Post.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } if val, ok := path.Post.ExtensionProps.Extensions["x-required-fields"]; ok { j, err := json.Marshal(&val) if err == nil { if j[0] == 0x22 && j[len(j)-1] == 0x22 { j = j[1 : len(j)-1] } } newValue := []string{} err = json.Unmarshal(j, &newValue) if err == nil { action.RequiredBodyFields = newValue //log.Printf("Setting required bodyfields: %#v", newValue) } else { log.Printf("[ERROR] Failed to unmarshal required bodyfields %s: %s", string(j), err) } } action.Returns.Schema.Type = "string" handleFile := false // Parameters: []WorkflowAppActionParameter{}, // FIXME - add data for POST stuff //firstQuery := true optionalQueries := []string{} parameters := []string{} fileField := "" if path.Post.RequestBody != nil { value := path.Post.RequestBody.Value if val, ok := value.Content["multipart/form-data"]; ok { if val.Schema.Value != nil { if innerval, ok := val.Schema.Value.Properties["fieldname"]; ok { if extensionvalue, ok := innerval.Value.ExtensionProps.Extensions["value"]; ok { fieldname := extensionvalue.(json.RawMessage) newName := string(fmt.Sprintf("%s", string(fieldname))) if newName[0] == 0x22 && newName[len(newName)-1] == 0x22 { parsedName := newName[1 : len(newName)-1] //log.Printf("Parse name: %s", parsedName) fileField = parsedName curParam := WorkflowAppActionParameter{ Name: "file_id", Description: "Files to be uploaded", Multiline: false, Required: true, Schema: SchemaDefinition{ Type: "string", }, } action.Parameters = append(action.Parameters, curParam) } } } } } } headersFound := []string{} if len(path.Post.Parameters) > 0 { for counter, param := range path.Post.Parameters { if param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Post.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } //log.Printf("What: %#v", optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1]) //log.Printf("HI: %s", //optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-2]) // Removing newlines at the end if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] } //log.Printf("%#v", optionalParameters[setIndex].Value) } } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "post", parameters, optionalQueries, headersFound, fileField, api, handleFile) if len(functionname) > 0 { action.Name = functionname } //log.Printf("PARAMS: %d", len(action.Parameters)) //for _, param := range action.Parameters { // log.Printf("%#v", param) //} return action, curCode } func HandlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm functionName := FixFunctionName(path.Patch.Summary, actualPath, true) baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) newDesc := fmt.Sprintf("%s\n\n%s", path.Patch.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Patch", path.Patch.Summary), Label: fmt.Sprintf("%s", path.Patch.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Patch.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } if val, ok := path.Patch.ExtensionProps.Extensions["x-required-fields"]; ok { j, err := json.Marshal(&val) if err == nil { if j[0] == 0x22 && j[len(j)-1] == 0x22 { j = j[1 : len(j)-1] } } newValue := []string{} err = json.Unmarshal(j, &newValue) if err == nil { action.RequiredBodyFields = newValue //log.Printf("Setting required bodyfields: %#v", newValue) } else { log.Printf("[ERROR] Failed to unmarshal required bodyfields %s: %s", string(j), err) } } action.Returns.Schema.Type = "string" if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } handleFile := false //log.Println(path.Parameters) // Parameters: []WorkflowAppActionParameter{}, //firstQuery := true optionalQueries := []string{} parameters := []string{} headersFound := []string{} if len(path.Patch.Parameters) > 0 { for counter, param := range path.Patch.Parameters { if param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Patch.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } //log.Printf("What: %#v", optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1]) //log.Printf("HI: %s", //optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-2]) // Removing newlines at the end if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] } //log.Printf("%#v", optionalParameters[setIndex].Value) } } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "patch", parameters, optionalQueries, headersFound, "", api, handleFile) if len(functionname) > 0 { action.Name = functionname } return action, curCode } func HandlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []WorkflowAppActionParameter, path *openapi3.PathItem, actualPath string, optionalParameters []WorkflowAppActionParameter) (WorkflowAppAction, string) { // What to do with this, hmm functionName := FixFunctionName(path.Put.Summary, actualPath, true) baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) if strings.Contains(baseUrl, "_shuffle_replace_") { //log.Printf("[DEBUG] : %s", baseUrl) m := regexp.MustCompile(`_shuffle_replace_\d+`) baseUrl = m.ReplaceAllString(baseUrl, "") } newDesc := fmt.Sprintf("%s\n\n%s", path.Put.Description, baseUrl) action := WorkflowAppAction{ Description: newDesc, Name: fmt.Sprintf("%s %s", "Put", path.Put.Summary), Label: fmt.Sprintf("%s", path.Put.Summary), NodeType: "action", Environment: api.Environment, Parameters: extraParameters, } if val, ok := path.Put.ExtensionProps.Extensions["x-label"]; ok { labels := []string{} j, err := json.Marshal(&val) if err == nil { err = json.Unmarshal(j, &labels) if err == nil { action.CategoryLabel = labels } else { log.Printf("[ERROR] Could not unmarshal x-label array: %s", err) } } else { log.Printf("[ERROR] Could not marshal x-label: %s", err) } } if val, ok := path.Put.ExtensionProps.Extensions["x-required-fields"]; ok { j, err := json.Marshal(&val) if err == nil { if j[0] == 0x22 && j[len(j)-1] == 0x22 { j = j[1 : len(j)-1] } } newValue := []string{} err = json.Unmarshal(j, &newValue) if err == nil { action.RequiredBodyFields = newValue //log.Printf("Setting required bodyfields: %#v", newValue) } else { log.Printf("[ERROR] Failed to unmarshal required bodyfields %s: %s", string(j), err) } } action.Returns.Schema.Type = "string" handleFile := false //log.Println(path.Parameters) // Parameters: []WorkflowAppActionParameter{}, //firstQuery := true optionalQueries := []string{} parameters := []string{} headersFound := []string{} if len(path.Put.Parameters) > 0 { for counter, param := range path.Put.Parameters { if param.Value.Schema == nil { continue } else if param.Value.In == "header" { headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } parsedName := param.Value.Name parsedName = strings.Replace(parsedName, " ", "_", -1) parsedName = strings.Replace(parsedName, ",", "_", -1) parsedName = strings.Replace(parsedName, ".", "_", -1) parsedName = strings.Replace(parsedName, "|", "_", -1) parsedName = ValidateParameterName(parsedName) param.Value.Name = parsedName path.Put.Parameters[counter].Value.Name = parsedName // Force it as a string to avoid nil-pointer if param.Value.Schema.Value == nil { param.Value.Schema.Value = &openapi3.Schema{ Type: "string", } } curParam := WorkflowAppActionParameter{ Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, Schema: SchemaDefinition{ Type: param.Value.Schema.Value.Type, }, } if param.Value.Example != nil { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Example = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Example = exampleVal } if param.Value.Name == "body" { if exampleVal, ok := param.Value.Example.(string); !ok { curParam.Value = fmt.Sprintf("%v", param.Value.Example) } else { curParam.Value = exampleVal } } } if val, ok := param.Value.ExtensionProps.Extensions["multiline"]; ok { j, err := json.Marshal(&val) if err == nil { b, err := strconv.ParseBool(string(j)) if err == nil { curParam.Multiline = b } } } if param.Value.Required { action.Parameters = append(action.Parameters, curParam) } else { optionalParameters = append(optionalParameters, curParam) } if param.Value.In == "path" { parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) if !param.Value.Required { optionalQueries = append(optionalQueries, param.Value.Name) continue } parameters = append(parameters, param.Value.Name) if strings.Contains(baseUrl, fmt.Sprintf("%s={%s}", param.Value.Name, param.Value.Name)) { continue } if strings.Contains(baseUrl, fmt.Sprintf("{%s}", param.Value.Name)) { continue } //if firstQuery && !strings.Contains(baseUrl, "?") { // baseUrl = fmt.Sprintf("%s?%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} else { // baseUrl = fmt.Sprintf("%s&%s={%s}", baseUrl, param.Value.Name, param.Value.Name) //} //firstQuery = false } } } if len(headersFound) > 0 { setIndex := -1 for paramIndex, param := range optionalParameters { if param.Name == "headers" { setIndex = paramIndex break } } if setIndex >= 0 { for _, header := range headersFound { if !strings.Contains(header, "=") { continue } headerKey := strings.Split(header, "=")[0] if strings.Contains(optionalParameters[setIndex].Value, headerKey) { continue } optionalParameters[setIndex].Value = fmt.Sprintf("%s%s\n", optionalParameters[setIndex].Value, header) } //log.Printf("What: %#v", optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1]) //log.Printf("HI: %s", //optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-2]) // Removing newlines at the end if len(optionalParameters[setIndex].Value) > 0 && optionalParameters[setIndex].Value[len(optionalParameters[setIndex].Value)-1] == 0xa { optionalParameters[setIndex].Value = optionalParameters[setIndex].Value[0 : len(optionalParameters[setIndex].Value)-1] } //log.Printf("%#v", optionalParameters[setIndex].Value) } } // Must be here 'cus they should be last headerKey := `headers=""` if !ArrayContains(parameters, headerKey) { parameters = append(parameters, headerKey) } queryKey := `queries=""` if !ArrayContains(parameters, queryKey) { parameters = append(parameters, queryKey) } // ensuring that they end up last in the specification // (order is ish important for optional params) - they need to be last. for _, optionalParam := range optionalParameters { optionalParam.Name = strings.ToLower(optionalParam.Name) action.Parameters = append(action.Parameters, optionalParam) } functionname, curCode := MakePythoncode(swagger, functionName, baseUrl, "put", parameters, optionalQueries, headersFound, "", api, handleFile) if len(functionname) > 0 { action.Name = functionname } return action, curCode } func GetAppRequirements() string { return "requests==2.32.3\nurllib3==2.3.0\nliquidpy==0.8.2\nMarkupSafe==3.0.2\nflask[async]==3.1.0\npython-dateutil==2.9.0.post0\nPyJWT==2.10.1\ncryptography==44.0.2\nshufflepy==0.2.2\nshuffle-sdk==0.0.38" } // Removes JSON values from the input func RemoveJsonValues(input []byte, depth int64) ([]byte, string, error) { // Make the byte into a map[string]interface{} so we can iterate over it keyToken := "" var jsonParsed map[string]interface{} err := json.Unmarshal(input, &jsonParsed) if err != nil { return input, keyToken, err } // Sort the keys so we can iterate over them in order keys := make([]string, 0, len(jsonParsed)) for k := range jsonParsed { keys = append(keys, k) } sort.Strings(keys) // Iterate over the map[string]interface{} and remove the values for _, k := range keys { keyToken += k // Get the value of the key as a map[string]interface{} //log.Printf("k: %v, %#v", k, jsonParsed[k]) // Check if it's a list or not if _, ok := jsonParsed[k].([]interface{}); ok { // Recurse this function newListItem := []interface{}{} for loopItem, v := range jsonParsed[k].([]interface{}) { _ = loopItem if parsedValue, ok := v.(map[string]interface{}); ok { // Marshal the value newParsedValue, err := json.MarshalIndent(parsedValue, "", "\t") if err != nil { log.Printf("[ERROR] Error in index %d of key %s: %v", loopItem, k, err) continue } returnJson, newKeyToken, err := RemoveJsonValues([]byte(string(newParsedValue)), depth+1) _ = newKeyToken if err != nil { log.Printf("[ERROR] Error: %v", err) } else { //log.Printf("returnJson (1): %v", string(returnJson)) // Unmarshal the byte back into a map[string]interface{} var jsonParsed2 map[string]interface{} err := json.Unmarshal(returnJson, &jsonParsed2) if err != nil { log.Printf("[ERROR] Error: %v", err) } else { newListItem = append(newListItem, jsonParsed2) } } } else if _, ok := v.([]interface{}); ok { // FIXME: No loop in loop for now log.Printf("[ERROR] No Handler Error in index %d of key %s: %v", loopItem, k, err) } else if _, ok := v.(string); ok { newListItem = append(newListItem, "") } else if _, ok := v.(float64); ok { newListItem = append(newListItem, 0) } else if _, ok := v.(bool); ok { newListItem = append(newListItem, false) } else { //log.Printf("[ERROR] No Handler Error in index %d of key %s: %v", loopItem, k, err) } } jsonParsed[k] = newListItem } // Check if it's a string if _, ok := jsonParsed[k].(string); ok { // Remove the value jsonParsed[k] = "" } else if _, ok := jsonParsed[k].(float64); ok { jsonParsed[k] = 0 } else if _, ok := jsonParsed[k].(bool); ok { jsonParsed[k] = false } else if _, ok := jsonParsed[k].(map[string]interface{}); ok { newParsedValue, err := json.MarshalIndent(jsonParsed[k].(map[string]interface{}), "", "\t") if err != nil { log.Printf("[ERROR] Error in key %s: %v", k, err) continue } returnJson, newKeyToken, err := RemoveJsonValues([]byte(string(newParsedValue)), depth+1) if depth < 3 && len(newKeyToken) > 0 { keyToken += "." + newKeyToken } if err != nil { log.Printf("[ERROR] Error: %v", err) } else { //log.Printf("returnJson (2): %v", string(returnJson)) // Unmarshal the byte back into a map[string]interface{} var jsonParsed2 map[string]interface{} err := json.Unmarshal(returnJson, &jsonParsed2) if err != nil { log.Printf("[ERROR] Error: %v", err) } else { jsonParsed[k] = jsonParsed2 } } } else { //log.Printf("[ERROR] No Handler Error in key %s: %v", k, err) } // Check if the value is a map[string]interface{} //if _, ok := v.(map[string]interface{}); ok { // // Remove the value // v = nil //} } // Marshal the map[string]interface{} back into a byte input, err = json.MarshalIndent(jsonParsed, "", "\t") if err != nil { return input, keyToken, err } return input, keyToken, nil } // TIL: func splitRef(full string) (host, repo, tag string) { parts := strings.SplitN(full, "/", 2) host = parts[0] remainder := "" if len(parts) > 1 { remainder = parts[1] } tag = "latest" if i := strings.LastIndex(remainder, ":"); i != -1 { repo = remainder[:i] tag = remainder[i+1:] } else { repo = remainder } log.Printf("%s -> %s %s %s", full, host, repo, tag) return } func hexOf(b []byte) string { h := sha256.Sum256(b) return fmt.Sprintf("%x", h[:]) } // upload a single layer.tar by gzipping on the fly and streaming to registry using chunked upload. // returns (compressedDigest, compressedSize, diffID). func uploadLayerToRegistry(regBase, repoPath string, layer io.Reader) (string, int64, string, error) { startURL := fmt.Sprintf("%s/v2/%s/blobs/uploads/", regBase, repoPath) reqStart, _ := http.NewRequest(http.MethodPost, startURL, nil) loc, err := followLocation(reqStart) if err != nil { return "", 0, "", fmt.Errorf("start upload: %s", err) } // pipe: gzip(layer) -> (count+hash) -> PATCH pr, pw := io.Pipe() var wg sync.WaitGroup var compBytes int64 compHash := sha256.New() diffHash := sha256.New() wg.Add(1) go func() { defer wg.Done() defer pw.Close() gzw, _ := gzip.NewWriterLevel(io.MultiWriter(pw, countingWriter{&compBytes}, compHash), gzip.BestSpeed) _, copyErr := io.Copy(gzw, io.TeeReader(layer, diffHash)) if cerr := gzw.Close(); copyErr == nil { copyErr = cerr } if copyErr != nil { _ = pw.CloseWithError(copyErr) } }() reqPatch, _ := http.NewRequest("PATCH", loc, pr) reqPatch.Header.Set("Content-Type", "application/octet-stream") loc, err = followLocation(reqPatch) if err != nil { return "", 0, "", fmt.Errorf("patch upload: %w", err) } wg.Wait() compDigest := "sha256:" + fmt.Sprintf("%x", compHash.Sum(nil)) diffID := "sha256:" + fmt.Sprintf("%x", diffHash.Sum(nil)) finalURL := loc if strings.Contains(finalURL, "?") { finalURL = finalURL + "&digest=" + compDigest } else { finalURL = finalURL + "?digest=" + compDigest } reqPut, _ := http.NewRequest(http.MethodPut, finalURL, nil) if _, err := regDo(reqPut); err != nil { return "", 0, "", fmt.Errorf("finalize upload: %w", err) } return compDigest, compBytes, diffID, nil } func uploadBlobOnce(regBase, repoPath string, r io.Reader, size int64, digest, contentType string) error { startURL := fmt.Sprintf("%s/v2/%s/blobs/uploads/", regBase, repoPath) reqStart, _ := http.NewRequest("POST", startURL, nil) loc, err := followLocation(reqStart) if err != nil { return fmt.Errorf("start upload: %s", err) } reqPatch, _ := http.NewRequest("PATCH", loc, r) if contentType == "" { contentType = "application/octet-stream" } reqPatch.Header.Set("Content-Type", contentType) loc, err = followLocation(reqPatch) if err != nil { return fmt.Errorf("patch upload: %s", err) } finalURL := loc if strings.Contains(finalURL, "?") { finalURL = finalURL + "&digest=" + digest } else { finalURL = finalURL + "?digest=" + digest } reqPut, _ := http.NewRequest("PUT", finalURL, nil) _, err = regDo(reqPut) return err } func followLocation(req *http.Request) (string, error) { resp, err := regDo(req) if err != nil { return "", err } loc := resp.Header.Get("Location") resp.Body.Close() if loc == "" { return "", fmt.Errorf("missing Location in response to %s %s", req.Method, req.URL.String()) } u, _ := url.Parse(loc) if !u.IsAbs() { base := &url.URL{Scheme: req.URL.Scheme, Host: req.URL.Host, Path: loc} return base.String(), nil } return loc, nil } func regDo(req *http.Request) (*http.Response, error) { // TODO: inject Authorization for registry if required c := &http.Client{Timeout: 0} resp, err := c.Do(req) if err != nil { return nil, err } return resp, nil } func (w countingWriter) Write(p []byte) (int, error) { *w.n += int64(len(p)) return len(p), nil } func DownloadDockerImageBackend(topClient *http.Client, imageName string) error { // Check environment SHUFFLE_AUTO_IMAGE_DOWNLOAD if os.Getenv("SHUFFLE_AUTO_IMAGE_DOWNLOAD") == "false" { log.Printf("[DEBUG] SHUFFLE_AUTO_IMAGE_DOWNLOAD is false. NOT downloading image %s", imageName) return nil } var dwnImage sync.Mutex // Remove from downloadedImages after 5 minutes for a redownload time.AfterFunc(time.Minute*5, func() { dwnImage.Lock() defer dwnImage.Unlock() cleanedImages := downloadedImages[:0] // len=0 cap=same, ptr same for _, img := range downloadedImages { if img != imageName { cleanedImages = append(cleanedImages, img) } } downloadedImages = cleanedImages }) if ArrayContains(downloadedImages, imageName) && project.Environment == "worker" { log.Printf("[DEBUG] Image %s already downloaded - not re-downloading. This only applies to workers.", imageName) return nil } baseUrl := os.Getenv("BASE_URL") //log.Printf("[DEBUG] Trying to download image %s from backend %s as it doesn't exist", imageName, baseUrl) if !ArrayContains(downloadedImages, imageName) { downloadedImages = append(downloadedImages, imageName) } dockerImgUrl := fmt.Sprintf("%s/api/v1/get_docker_image?image=%s", baseUrl, strings.Replace(imageName, " ", "-", -1)) isCloudDownload := false if strings.Contains(baseUrl, "ngrok") || strings.Contains(baseUrl, "shuffler.io") || strings.Contains(baseUrl, ".run.app") { log.Printf("[DEBUG] Downloading as GET request with redirects") dockerImgUrl = fmt.Sprintf("%s/api/v1/get_docker_image?image=%s", baseUrl, strings.Replace(imageName, " ", "-", -1)) isCloudDownload = true } else { //log.Printf("[DEBUG] Downloading image as POST request WITHOUT redirects due to not being cloud") } // Set request timeout to 5 min (max) topClient.Timeout = time.Minute * 10 arch := runtime.GOARCH if strings.Contains(strings.ToLower(arch), "arm") { if strings.Contains(dockerImgUrl, "?") { dockerImgUrl = fmt.Sprintf("%s&arch=%s", dockerImgUrl, arch) } else { dockerImgUrl = fmt.Sprintf("%s?arch=%s", dockerImgUrl, arch) } } relevantBody := DockerRequestCheck{ Name: strings.Replace(imageName, " ", "-", -1), } marshalledBody, err := json.Marshal(relevantBody) if err != nil { log.Printf("[ERROR] Failed to marshal body to be sent: %s", err) return err } req, err := http.NewRequest( "POST", dockerImgUrl, bytes.NewBuffer(marshalledBody), ) if err != nil { log.Printf("[ERROR] Failed to create request for %s: %s", imageName, err) return err } if isCloudDownload { log.Printf("[DEBUG] Running GET request for cloud download for URL %s", dockerImgUrl) req.Method = "GET" req.Body = nil } // Specific to the worker authorization := os.Getenv("AUTHORIZATION") if len(authorization) > 0 { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization)) } else { // Specific to Orborus auth (org + auth) -> environment auth authorization = os.Getenv("AUTH") if len(authorization) > 0 { //log.Printf("[DEBUG] Found Orborus environment auth - adding to header.") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization)) org := os.Getenv("ORG") if len(org) > 0 { req.Header.Add("Org-Id", org) } } else { log.Printf("[WARNING] No auth found - running backend download without it.") } } if len(os.Getenv("IS_KUBERNETES")) > 0 && os.Getenv("IS_KUBERNETES") == "true" { log.Printf("[INFO] In kubernetes pushing it to private registry") localRegistry := os.Getenv("SHUFFLE_STREAM_PRIVATE_REGISTRY") if localRegistry == "" { log.Printf("[ERROR] No private registry defined") return err } imgPath := strings.TrimPrefix(strings.ReplaceAll(imageName, " ", "-"), "/") refStr := fmt.Sprintf("%s/%s", strings.TrimSuffix(localRegistry, "/"), imgPath) insecure := os.Getenv("SHUFFLE_STREAM_PRIVATE_REGISTRY_INSECURE") == "true" scheme := "https" if insecure { scheme = "http" } regHost, repoPath, tag := splitRef(refStr) regBase := scheme + "://" + regHost resp, err := topClient.Do(req.Clone(req.Context())) if err != nil { log.Printf("[ERROR] Bucket request failed") return fmt.Errorf("bucket request failed: %w", err) } if resp.StatusCode != 200 { defer resp.Body.Close() log.Printf("[ERROR] Bucket request failed bad staus code") return fmt.Errorf("bucket bad status %s", resp.Status) } br := bufio.NewReader(resp.Body) var tarR *tar.Reader if peek, _ := br.Peek(2); len(peek) == 2 && peek[0] == 0x1f && peek[1] == 0x8b { gr, err := gzip.NewReader(br) if err != nil { resp.Body.Close() log.Printf("[ERROR] Gzip init failed") return fmt.Errorf("gzip init failed: %w", err) } defer gr.Close() defer resp.Body.Close() tarR = tar.NewReader(gr) } else { defer resp.Body.Close() tarR = tar.NewReader(br) } type desc struct { Digest string Size int64 } layerByDiff := map[string]desc{} var diffOrder []string var configJSON []byte // walk tar once: upload layers & capture config for { hdr, err := tarR.Next() if err == io.EOF { break } if err != nil { return fmt.Errorf("tar read: %w", err) } name := hdr.Name switch { case strings.HasSuffix(name, "/layer.tar"): // stream this layer: compute diffID (uncompressed), gzip+upload to registry, record digest & size compDig, compSize, diffID, err := uploadLayerToRegistry(regBase, repoPath, tarR) if err != nil { log.Printf("[ERROR] Failed to upload layer %s", err) return fmt.Errorf("upload layer %s: %w", name, err) } layerByDiff[diffID] = desc{Digest: compDig, Size: compSize} case strings.HasSuffix(name, ".json") && name != "manifest.json": if len(configJSON) == 0 { var buf bytes.Buffer if _, err := io.Copy(&buf, tarR); err != nil { log.Printf("[ERROR] Failed to read config.json") return fmt.Errorf("read config.json: %w", err) } var probe struct { RootFS struct { DiffIDs []string `json:"diff_ids"` } `json:"rootfs"` } if json.Unmarshal(buf.Bytes(), &probe) == nil && len(probe.RootFS.DiffIDs) > 0 { configJSON = buf.Bytes() for _, d := range probe.RootFS.DiffIDs { if !strings.HasPrefix(d, "sha256:") { d = "sha256:" + d } diffOrder = append(diffOrder, d) } } } default: } } if len(configJSON) == 0 || len(diffOrder) == 0 { log.Printf("[ERROR] Failed to docker save tar config") return fmt.Errorf("docker save tar missing config") } layers := make([]map[string]any, 0, len(diffOrder)) for _, d := range diffOrder { desc, ok := layerByDiff[d] if !ok { log.Printf("[ERROR] Failed to upload layer (2)") return fmt.Errorf("missing uploaded layer for diffID %s", d) } layers = append(layers, map[string]any{ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": desc.Size, "digest": desc.Digest, }) } cfgDigest := "sha256:" + hexOf(configJSON) if err := uploadBlobOnce(regBase, repoPath, bytes.NewReader(configJSON), int64(len(configJSON)), cfgDigest, "application/octet-stream"); err != nil { log.Printf("[ERROR] Failed to upload config (2)") return fmt.Errorf("upload config: %w", err) } manifest := map[string]any{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": map[string]any{ "mediaType": "application/vnd.docker.container.image.v1+json", "size": len(configJSON), "digest": cfgDigest, }, "layers": layers, } manBytes, _ := json.Marshal(manifest) putURL := fmt.Sprintf("%s/v2/%s/manifests/%s", regBase, repoPath, tag) reqM, _ := http.NewRequest(http.MethodPut, putURL, bytes.NewReader(manBytes)) reqM.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") reqM.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json") if _, err := regDo(reqM); err != nil { log.Printf("[ERROR] Failed to put mainfest") return fmt.Errorf("put manifest: %w", err) } log.Printf("[INFO] Pushed image to private registry as %s", refStr) return nil } newresp, err := topClient.Do(req) if err != nil { log.Printf("[ERROR] Failed download request for %s: %s", imageName, err) return err } if newresp.Request.URL.String() != dockerImgUrl { log.Printf("[DEBUG] Redirected download URL: %s -> %s", dockerImgUrl, newresp.Request.URL.String()) } defer newresp.Body.Close() if newresp.StatusCode != 200 { //log.Printf("[ERROR] Docker download for image %s (backend) StatusCode (1): %d", imageName, newresp.StatusCode) return errors.New(fmt.Sprintf("Failed to get image - status code %d", newresp.StatusCode)) } newImageName := strings.Replace(imageName, "/", "_", -1) newFileName := newImageName + ".tar" tar, err := os.Create(newFileName) if err != nil { log.Printf("[WARNING] Failed creating file: %s", err) return err } defer tar.Close() _, err = io.Copy(tar, newresp.Body) if err != nil { log.Printf("[ERROR] Failed response body copying for file %s: %s", newFileName, err) return err } tar.Seek(0, 0) dockercli, err := docker.NewEnvClient() if err != nil { log.Printf("[ERROR] Unable to create docker client (3): %s", err) return err } //log.Printf("[DEBUG] Starting to load zip file for image %s. This is a background process and may take a while.", imageName) //imageLoadResponse, err := dockercli.ImageLoad(context.Background(), tar, true) defer dockercli.Close() //imageLoadResponse, err := dockercli.ImageLoad(context.Background(), tar) imageLoadResponse, err := dockercli.ImageLoad(context.Background(), tar) if err != nil { log.Printf("[ERROR] Failed loading docker images: %s", err) return err } //log.Printf("[DEBUG] Finished loading zip file for image %s", imageName) defer imageLoadResponse.Body.Close() body, err := ioutil.ReadAll(imageLoadResponse.Body) if err != nil { log.Printf("[ERROR] Failed reading docker image: %s", err) return err } if strings.Contains(string(body), "no such file") { return errors.New(string(body)) } os.Remove(newFileName) if strings.Contains(strings.ToLower(string(body)), "error") { log.Printf("[ERROR] Error loading image %s: %s", imageName, string(body)) return errors.New(string(body)) } baseTag := strings.Split(imageName, ":") if len(baseTag) > 1 { tag := baseTag[1] //log.Printf("[DEBUG] Creating tag copies of downloaded containers from tag %s", tag) // Remapping ctx := context.Background() dockercli.ImageTag(ctx, imageName, fmt.Sprintf("frikky/shuffle:%s", tag)) dockercli.ImageTag(ctx, imageName, fmt.Sprintf("registry.hub.docker.com/frikky/shuffle:%s", tag)) downloadedImages = append(downloadedImages, fmt.Sprintf("frikky/shuffle:%s", tag)) downloadedImages = append(downloadedImages, fmt.Sprintf("registry.hub.docker.com/frikky/shuffle:%s", tag)) } //log.Printf("[INFO] Successfully loaded image %s: %s", imageName, string(body)) return nil } func GetAppNameSplit(version DockerRequestCheck) (string, string, string, error) { if len(version.Image) > 0 && len(version.Name) == 0 { version.Name = version.Image } if len(version.Name) == 0 { return "", "", "", errors.New("No image name found") } identifier := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(version.Name, "_", "-"), ".", "-")) if strings.Contains(identifier, ":") { identifier = strings.Split(identifier, ":")[1] } appname := version.Name appnameSplit := strings.Split(version.Name, ":") if len(appnameSplit) > 1 { appname = appnameSplit[1] } appId := "" appnameSplit2 := strings.Split(appname, ".") appnameSplit3 := strings.Split(appname, "_") if len(appnameSplit2) <= 2 { // Check last item of app split on _ if it's an md5 if len(appnameSplit3) > 1 && len(appnameSplit3[len(appnameSplit3)-1]) == 32 { appId = appnameSplit3[len(appnameSplit3)-1] // Remove the md5 from the appname appname = strings.Join(appnameSplit3[0:len(appnameSplit3)-1], "_") } else { err := errors.New(fmt.Sprintf("Invalid image appname format: %s", version.Name)) return "", "", "", err } } baseAppname := appnameSplit2[0][0 : len(appnameSplit2[0])-2] // In case of a weird version name/number that is not semantic if len(appnameSplit2) > 3 { // JUST remove the LAST dot parts newstring := "" for cnt, part := range appnameSplit2 { if cnt > len(appnameSplit2)-2 { continue } newstring += part + "." } // Removes the last _1 of _1.0.0 (or similar) in versions baseAppname = newstring[0 : len(newstring)-5] } if debug { log.Printf("%#v - BASEAPPNAME: %#v, %#v", appname, baseAppname, appnameSplit2) } // Check if baseAppname ends with _ and if so, remove it if len(appId) > 0 { // Remove appId from baseAppname by removing last _ part baseAppname = strings.Join(appnameSplit3[0:len(appnameSplit3)-1], "_") } appVersion := "" if len(appnameSplit2) >= 2 { appVersion = appnameSplit2[0][len(appnameSplit2[0])-1:] + "." + strings.Join(appnameSplit2[1:], ".") } appname = strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(appname, "_", "-"), ".", "-")) return appname, baseAppname, appVersion, nil } func handleDatastoreAutomationWebhook(ctx context.Context, marshalledBody []byte, cacheData CacheKeyData, automation DatastoreAutomation, url, runType string) error { var err error // Dedup here with cache cacheName := fmt.Sprintf("automation_%s_%s_%s", runType, cacheData.Category, cacheData.Key) _, err = GetCache(ctx, cacheName) if err == nil { if debug { log.Printf("[DEBUG] Found existing cache for %s - skipping execution to prevent duplicates", cacheName) } return nil } // Makes sure we wait 2500ms SetCache(ctx, cacheName, []byte("1"), 2500, true) if runType == "run_workflow" { } else if runType == "webhook" { webhookUrl := "" for _, option := range automation.Options { if option.Key == "webhook_url" { webhookUrl = option.Value break } } if !strings.HasPrefix(webhookUrl, "http") { return errors.New(fmt.Sprintf("Webhook URL %s is not valid", webhookUrl)) } parsedBody := WorkflowAppAction{ AppID: "HTTP", AppName: "HTTP", Environment: "cloud", //Name: "custom_command", Name: "POST", NodeType: "action", Parameters: []WorkflowAppActionParameter{ WorkflowAppActionParameter{ Name: "url", Value: webhookUrl, }, WorkflowAppActionParameter{ Name: "method", Value: "POST", }, WorkflowAppActionParameter{ Name: "body", Value: string(marshalledBody), }, WorkflowAppActionParameter{ Name: "headers", Value: "Content-Type: application/json\nAccept: application/json", }, }, } if project.Environment != "cloud" { environments, err := GetEnvironments(ctx, cacheData.OrgId) if err != nil { return err } for _, env := range environments { if env.Default { parsedBody.Environment = env.Name break } } } marshalledBody, err = json.Marshal(parsedBody) if err != nil { log.Printf("[ERROR] Failed to marshal parsedBody for webhook %s: %s", webhookUrl, err) return err } } // Find a user to use org, err := GetOrg(ctx, cacheData.OrgId) if err != nil { return err } foundApikey := "" for _, user := range org.Users { if user.Role != "admin" { continue } if len(user.ApiKey) > 0 { foundApikey = user.ApiKey break } else { foundUser, err := GetUser(ctx, user.Id) if err != nil { log.Printf("[ERROR] Failed to get user by ID %s: %s", user.Id, err) continue } if len(foundUser.ApiKey) > 0 { foundApikey = foundUser.ApiKey break } } } // Send request to backendUrl := os.Getenv("BASE_URL") if len(os.Getenv("SHUFFLE_CLOUDRUN_URL")) > 0 && strings.Contains(os.Getenv("SHUFFLE_CLOUDRUN_URL"), "http") { backendUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL") } //parsedUrl := fmt.Sprintf("%s/api/v1/apps/HTTP/run", backendUrl) parsedUrl := fmt.Sprintf("%s%s", backendUrl, url) client := &http.Client{ Timeout: time.Second * 5, } req, err := http.NewRequest( "POST", parsedUrl, bytes.NewBuffer(marshalledBody), ) if err != nil { log.Printf("[ERROR] Failed to create request for webhook %s: %s", parsedUrl, err) return err } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", foundApikey)) req.Header.Add("Org-Id", cacheData.OrgId) resp, err := client.Do(req) if err != nil { log.Printf("[ERROR] Failed to send webhook request to %s: %s", parsedUrl, err) return err } body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERROR] Failed to read response body from webhook request to %s: %s", parsedUrl, err) return err } defer resp.Body.Close() if resp.StatusCode >= 300 { if runType == "run_workflow" { log.Printf("[ERROR] Datastore Automation: Workflow Run request to %s failed with status code %d", parsedUrl, resp.StatusCode) return errors.New(fmt.Sprintf("Workflow Run request failed with status code %d. Body: %s", resp.StatusCode, body)) } else { log.Printf("[ERROR] Datastore Automation: Webhook request to %s failed with status code %d", parsedUrl, resp.StatusCode) return errors.New(fmt.Sprintf("Webhook request failed with status code %d. Body: %s", resp.StatusCode, body)) } } return nil } func handleRunDatastoreAutomation(ctx context.Context, cacheData CacheKeyData, automation DatastoreAutomation) error { if len(cacheData.OrgId) == 0 { return errors.New("CacheKeyData.OrgId is required for handleRunAutomation") } if len(cacheData.Category) == 0 { return errors.New("CacheKeyData.Category is required for handleRunAutomation") } if ctx == nil { ctx = context.Background() } parsedName := strings.ReplaceAll(strings.ToLower(automation.Name), " ", "_") // These are ran pre-execution if parsedName == "security_rules" { return nil } // Unmarshal cacheData.Value to parsedOutput parsedOutput := map[string]interface{}{} if err := json.Unmarshal([]byte(cacheData.Value), &parsedOutput); err != nil { log.Printf("[ERROR] Failed to unmarshal cacheData.Value: %s", err) parsedOutput = map[string]interface{}{} parsedOutput["value"] = cacheData.Value } if parsedOutput == nil { parsedOutput = map[string]interface{}{} } parsedOutput["shuffle_datastore"] = map[string]interface{}{ "action": "update", "key": cacheData.Key, "category": cacheData.Category, "org_id": cacheData.OrgId, "timestamp": cacheData.Edited, "workflow_id": cacheData.WorkflowId, "suborg_distribution": cacheData.SuborgDistribution, "tags": cacheData.Tags, } marshalledBody, err := json.Marshal(parsedOutput) if err != nil { log.Printf("[ERROR] Failed to marshal parsedOutput. Key %s, Category: %s, org: %s, err: %s", cacheData.Key, cacheData.Category, cacheData.OrgId, err) return err } backendUrl := "https://shuffler.io" if len(os.Getenv("BASE_URL")) > 0 { backendUrl = os.Getenv("BASE_URL") } if len(os.Getenv("SHUFFLE_CLOUDRUN_URL")) > 0 && strings.Contains(os.Getenv("SHUFFLE_CLOUDRUN_URL"), "http") { backendUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL") } org, err := GetOrg(ctx, cacheData.OrgId) if err != nil { return err } foundApikey := "" for _, user := range org.Users { foundUser, err := GetUser(ctx, user.Id) if err != nil { continue } if len(foundUser.Role) == 0 || foundUser.Role == "org-reader" { continue } if len(foundUser.ApiKey) > 0 { foundApikey = foundUser.ApiKey break } } if parsedName == "correlate_categories" { // Correlations don't matter anymore as ngrams are automatic. Cleaned up // november 2025 after adding graphic system to datastore } else if parsedName == "run_ai_agent" { log.Printf("[DEBUG] AI agent: Handling run_ai_agent automation for key %s in category %s", cacheData.Key, cacheData.Category) if len(foundApikey) == 0 { log.Printf("[ERROR] No admin user with API key found for org %s", cacheData.OrgId) return errors.New("No admin user with API key found") } // Already handled check for optionKey, option := range automation.Options { // 'remove' icon in the UI does this if option.Disabled { continue } if len(option.Value) < 10 { //log.Printf("[DEBUG] Actions info too short: %s - skipping", option.Key) continue } agentTagName := fmt.Sprintf("agent-%s", option.Key) if ArrayContains(cacheData.Tags, agentTagName) { continue } // Check if previous has finished/timed out // This allows next to run. Default agent cache timeout is 30 seconds~ if optionKey > 0 { oldKey := automation.Options[optionKey-1] oldCacheName := fmt.Sprintf("%s_%s_%s_%s", cacheData.Key, cacheData.Category, cacheData.OrgId, oldKey.Key) _, err := GetCache(ctx, oldCacheName) if err == nil { if debug { log.Printf("[DEBUG] PREV agent cache hit for %s - skipping for now", oldCacheName) } continue } } // As a fallback in case of slow datastore update // Prevents super quick reruns cacheName := fmt.Sprintf("%s_%s_%s_%s", cacheData.Key, cacheData.Category, cacheData.OrgId, option.Key) _, err := GetCache(ctx, cacheName) if err == nil { //log.Printf("[DEBUG] Cache hit for %s - skipping to avoid re-running agent", cacheName) continue } // 30 seconds SetCache(ctx, cacheName, []byte("1"), 60000, true) if !strings.Contains(option.Key, "action") { log.Printf("[WARNING] Agent option key %s does not contain 'action' - skipping to avoid confusion. This may cause the agent to not run if no other options are present.", option.Key) continue } allowedApps := strings.Join(option.Apps, ",") parsedParams := []map[string]string{ map[string]string{ "name": "app_name", "value": allowedApps, }, map[string]string{ "name": "action", "value": "API", }, } // option.Value += fmt.Sprintf("\n%s", cacheData.Value) parsedParams = append(parsedParams, map[string]string{ "name": "input", "value": fmt.Sprintf("TASK: %s\n\nKey: %s\nCategory: %s\n\nRAW DATA:\n%s", option.Value, cacheData.Key, cacheData.Category, cacheData.Value), }) agentUrl := fmt.Sprintf("%s/api/v1/apps/agent_starter/run", backendUrl) agentStartRequest := AgentStartRequest{ //ID string `json:"id"` Name: "agent", AppName: "AI Agent", AppID: "shuffle_agent", AppVersion: "1.0.0", Environment: "cloud", Parameters: parsedParams, } newParsedBody, err := json.Marshal(agentStartRequest) if err != nil { log.Printf("[ERROR] Failed to marshal body for ai agent execution: %s", err) return err } client := GetExternalClient(agentUrl) req, err := http.NewRequest( "POST", agentUrl, bytes.NewBuffer(newParsedBody), ) if err != nil { log.Printf("[ERROR] Failed to create request for enrichment workflow execution: %s", err) return err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", foundApikey)) req.Header.Set("Org-Id", cacheData.OrgId) req.Header.Set("X-Internal-Caller", "handleRunDatastoreAutomation") resp, err := client.Do(req) if err != nil { log.Printf("[ERROR] Failed to send enrichment workflow execution request: %s", err) return err } // Makes sure we don't re-run the same twice cacheData.Tags = append(cacheData.Tags, agentTagName) err = SetDatastoreKeyMeta(ctx, cacheData) if err != nil { log.Printf("[ERROR] Failed to set cache key after running AI agent: %s", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERROR] Failed to read response body from AI AGENT execution request: %s", err) return err } if debug { log.Printf("[DEBUG] RESP FOR RUNNING AI AGENT (%d): %s", resp.StatusCode, string(body)) } break } } else if parsedName == "enrich" { // Prevent recursion cacheKey := fmt.Sprintf("enrich_wait_%s_%s_%s", cacheData.OrgId, cacheData.Category, cacheData.Key) // Validates if the data is the same. Need a proper data diff //md5sum := Md5sum([]byte(cacheData.Value)) //log.Printf("VALUE (%s):\n\n%s\n\n", md5sum, cacheData.Value) data, err := GetCache(ctx, cacheKey) if err == nil && data != nil { //cacheData := []byte(data.([]uint8)) //if string(cacheData) == md5sum { // return nil //} return nil } if debug { log.Printf("[DEBUG] Running enrich automation for key %s in category %s", cacheData.Key, cacheData.Category) } //SetCache(ctx, cacheKey, []byte("1"), 1) var timeout int32 = 5000 if project.Environment != "cloud" { timeout = 60000 } SetCache(ctx, cacheKey, []byte("1"), timeout, true) if cacheData.Enrichments != nil && len(cacheData.Enrichments) > 0 { } // Send the data into shuffle_tools => parse_ioc? // Or generate a workflow that runs for it? :thinking: // Example process: // 1. Ingest IOC (hash/IP/domain/alert) => inject into datastore category // 2. Query reputation + passive DNS + WHOIS + SSL CT. // 3. Run lookup in historic sightings (SIEM, MISP). // 4. If file hash: submit to sandbox + static YARA. // 5. Map results to ATT&CK techniques and assign a risk score. // 6. Push enriched alert to SIEM/EDR/SOAR for automated playbook or analyst triage. // 7. If high confidence, add to blocklists / trigger containment / share via STIX/TAXII or MISP. // Getting started: // 1. Check for enrichments key. Stop if it exists. /* types := []iocParser.IndicatorType{ iocParser.IPV4, iocParser.URL_LINK, iocParser.Domain, iocParser.Email, } foundIocs := iocParser.Parse(string(marshalledBody), types) log.Printf("RESP: %#v", foundIocs) if len(foundIocs) == 0 { log.Printf("[DEBUG] No IOCs found to enrich.") return nil } log.Printf("[DEBUG] Found %d IOCs to enrich.", len(foundIocs)) for _, foundIoc := range foundIocs { log.Printf("[DEBUG] Found IOC: %#v", foundIoc) } */ if len(foundApikey) == 0 { log.Printf("[ERROR] No admin user with API key found for org %s", cacheData.OrgId) return errors.New("No admin user with API key found") } // Uses the same as the API /api/v*/workflows/generate seedString := fmt.Sprintf("%s_Enable Threat feeds_webhook", cacheData.OrgId) hash := sha1.New() hash.Write([]byte(seedString)) hashBytes := hash.Sum(nil) uuidBytes := make([]byte, 16) copy(uuidBytes, hashBytes) relevantWorkflowId := uuid.Must(uuid.FromBytes(uuidBytes)).String() // FIXME: If workflow doesn't exist - generate it fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", backendUrl, relevantWorkflowId) if debug { log.Printf("[DEBUG] Running enrich automation workflow %s for key %s in category %s", relevantWorkflowId, cacheData.Key, cacheData.Category) } executionRequest := ExecutionRequest{ ExecutionArgument: string(marshalledBody), ExecutionSource: fmt.Sprintf("datastore|%s|%s", cacheData.Category, cacheData.Key), } newParsedBody, err := json.Marshal(executionRequest) if err != nil { log.Printf("[ERROR] Failed to marshal body for enrichment workflow execution: %s", err) return err } client := GetExternalClient(fullUrl) req, err := http.NewRequest( "POST", fullUrl, bytes.NewBuffer(newParsedBody), ) if err != nil { log.Printf("[ERROR] Failed to create request for enrichment workflow execution: %s", err) return err } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", foundApikey)) req.Header.Add("Org-Id", cacheData.OrgId) resp, err := client.Do(req) if err != nil { log.Printf("[ERROR] Failed to send enrichment workflow execution request: %s", err) return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERROR] Failed to read response body from enrichment workflow execution request: %s", err) return err } if resp.StatusCode != 200 { log.Printf("[ERROR] Enrichment workflow execution request failed with status code %d. Body: %s", resp.StatusCode, string(body)) } if debug { log.Printf("[DEBUG] RESP FOR RUNNING ENRICHMENT (%d): %s", resp.StatusCode, string(body)) } } else if parsedName == "run_workflow" { for _, option := range automation.Options { if option.Key != "workflow_id" { continue } if len(option.Value) == 0 { continue } cacheData.WorkflowId = option.Value workflowIds := strings.Split(option.Value, ",") handled := []string{} for _, workflowId := range workflowIds { workflowId = strings.TrimSpace(workflowId) if ArrayContains(handled, workflowId) { continue } handled = append(handled, workflowId) formattedBodyStruct := ExecutionRequest{ ExecutionSource: fmt.Sprintf("datastore_%s_%s", cacheData.Category, cacheData.Key), ExecutionArgument: string(marshalledBody), } marshalledFormattedBody, err := json.Marshal(formattedBodyStruct) if err != nil { log.Printf("[ERROR] Failed in marshalling data in 'run_workflow' datastore automation for workflow %s", workflowId) } else { marshalledBody = marshalledFormattedBody } go handleDatastoreAutomationWebhook(ctx, marshalledBody, cacheData, automation, fmt.Sprintf("/api/v1/workflows/%s/execute", workflowId), "run_workflow") } break } } else if parsedName == "send_webhook" { if debug { log.Printf("[DEBUG] Sending webhook for url %s", automation.Options[0].Value) } return handleDatastoreAutomationWebhook(ctx, marshalledBody, cacheData, automation, "/api/v1/apps/HTTP/run", "webhook") // Send the webhook using the HTTP app with a POST request } else { return fmt.Errorf("Unknown automation name %s", automation.Name) } return nil } func PushTarStreamToRegistry(r io.Reader, refStr string) error { return nil } // Primarily used for cross-region app propagation of public apps on first run // Also used to rebuild apps when necessary IF buildApp = true is passed. func LoadAppConfigFromMain(fileId string, buildApp bool) { // Send request to /api/v1/apps/{fileId}/config // Parse out the config and add it to the database ctx := context.Background() app, err := GetApp(ctx, fileId, User{}, false) if err == nil && len(app.Name) > 0 && len(app.ID) > 0 { log.Printf("[INFO] Found app %s (%s) for config loading. Running cross-region DOWNLOAD shuffler.io->local if it's generated==false (python). Actions: %d. Generated: %t", app.Name, app.ID, len(app.Actions), app.Generated) //if len(app.Actions) > 0 { // return //} // Python apps can't be distributed this easily (sadly) if !app.Generated { return } } app.ID = fileId backendHost := fmt.Sprintf("https://shuffler.io") appApi := fmt.Sprintf("%s/api/v1/apps/%s/config", backendHost, fileId) if buildApp { if os.Getenv("BASE_URL") != "" { backendHost = os.Getenv("BASE_URL") } if os.Getenv("SHUFFLE_CLOUDRUN_URL") != "" { backendHost = os.Getenv("SHUFFLE_CLOUDRUN_URL") } // Check cache if it happened recently JUST IN CASE // Done per region appApi = fmt.Sprintf("%s/api/v1/apps/%s/config", backendHost, fileId) _, err := GetCache(ctx, appApi) if err == nil { return } SetCache(ctx, appApi, []byte("1"), 60) log.Printf("[WARNING] Auto-rebuilding app with ID %s. This is primarily when custom_action does not exist for generated apps (old apps)", appApi) } client := &http.Client{} req, err := http.NewRequest( "GET", appApi, nil, ) if err != nil { log.Printf("[ERROR] Failed creating request for app config: %s", err) return } resp, err := client.Do(req) if err != nil { log.Printf("[ERROR] Failed getting app config: %s", err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERROR] Failed reading app config: %s", err) return } if resp.StatusCode != 200 { log.Printf("[ERROR] Failed getting app config for ID %s: %d. Body: %s", fileId, resp.StatusCode, string(body)) return } newApp := AppParser{} err = json.Unmarshal(body, &newApp) if err != nil { log.Printf("[ERROR] Failed unmarshaling app config: %s", err) return } //log.Printf("[INFO] Got app config: %s", string(body)) if !newApp.Success { log.Printf("[ERROR] No success in app config for id %s", fileId) return } if len(newApp.App) == 0 { log.Printf("[ERROR] No app found for id %s", app.ID) } else { err = json.Unmarshal(newApp.App, &app) if err != nil { log.Printf("[ERROR] Failed unmarshaling app for id %s: %s", app.ID, err) return } err = SetWorkflowAppDatastore(ctx, *app, app.ID) if err != nil { log.Printf("[ERROR] Failed saving app for id %s: %s", app.ID, err) } } if len(newApp.OpenAPI) > 0 || buildApp { // Save the data to the database with the ParsedOpenApi struct parsedOpenApi := ParsedOpenApi{} err = json.Unmarshal(newApp.OpenAPI, &parsedOpenApi) if err != nil { log.Printf("[ERROR] Failed unmarshaling openapi for id %s: %s", app.ID, err) return } err = SetOpenApiDatastore(ctx, parsedOpenApi.ID, parsedOpenApi) if err != nil { log.Printf("[ERROR] Failed saving openapi for id %s: %s", app.ID, err) } // Run verify openapi here to make sure we send the correct request actualOpenApi := &openapi3.Swagger{} err = json.Unmarshal([]byte(parsedOpenApi.Body), &actualOpenApi) if err != nil { log.Printf("[ERROR] Problem with openapi3 mapping pre rebuild for %s: %s", parsedOpenApi.ID, err) } parsedOpenApiBody, err := json.Marshal(actualOpenApi) if err != nil { log.Printf("[ERROR] Failed unmarshalling %s", parsedOpenApi.ID) return } if buildApp { // Only partial part of it //type Test struct { // Editing bool `datastore:"editing"` // Id string `datastore:"id"` // Image string `datastore:"image"` //} // easiest way to force a rebuild by injecting 2 fields parsedOpenApiBody = []byte(strings.TrimSuffix(string(parsedOpenApiBody), "}")) parsedOpenApiBody = []byte(fmt.Sprintf(`%s, "id": "%s", "editing": true}`, string(parsedOpenApiBody), string(parsedOpenApi.ID))) log.Printf("\n\n\nNew body: %s\n\n", string(parsedOpenApiBody)) } baseurl := "http://localhost:5002" if os.Getenv("BASE_URL") != "" { baseurl = os.Getenv("BASE_URL") } if os.Getenv("SHUFFLE_CLOUDRUN_URL") != "" { baseurl = os.Getenv("SHUFFLE_CLOUDRUN_URL") } fullUrl := fmt.Sprintf("%s/api/v1/verify_swagger", baseurl) req, err := http.NewRequest( "POST", fullUrl, bytes.NewBuffer(parsedOpenApiBody), ) if err != nil { log.Printf("[ERROR] Failed creating request for openapi verification: %s", err) return } if len(os.Getenv("SHUFFLE_OPS_DASHBOARD_APIKEY")) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("SHUFFLE_OPS_DASHBOARD_APIKEY"))) } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { log.Printf("[ERROR] Failed verifying openapi: %s", err) return } defer resp.Body.Close() if resp.StatusCode != 200 { log.Printf("[ERROR] Failed building openapi for ID %s: %d", fileId, resp.StatusCode) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERROR] Failed reading openapi verification: %s", err) return } log.Printf("[INFO] OpenAPI build: %s", string(body)) } else { log.Printf("[ERROR] No openapi found for id %s", app.ID) } }