import React, { useState, useEffect } from "react"; import { makeStyles } from "@mui/styles"; import { BrowserView, MobileView } from "react-device-detect"; import theme from '../theme.jsx'; import { Paper, Typography, FormControlLabel, Button, Divider, Select, MenuItem, FormControl, Switch, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Tooltip, Breadcrumbs, Drawer, CircularProgress, Chip, IconButton, } from "@mui/material"; import { Publish as PublishIcon, LockOpen as LockOpenIcon, FileCopy as FileCopyIcon, Delete as DeleteIcon, Remove as RemoveIcon, Add as AddIcon, CheckCircle as CheckCircleIcon, AttachFile as AttachFileIcon, Apps as AppsIcon, ErrorOutline as ErrorOutlineIcon, AddAPhoto as AddAPhotoIcon, AddAPhotoOutlined as AddAPhotoOutlinedIcon, ZoomInOutlined as ZoomInOutlinedIcon, ZoomOutOutlined as ZoomOutOutlinedIcon, Loop as LoopIcon, AddPhotoAlternate as AddPhotoAlternateIcon, CallMerge as CallMergeIcon, CloudDownload as CloudDownloadIcon, OpenInNew as OpenInNewIcon, } from "@mui/icons-material"; import { v4 as uuidv4 } from "uuid"; import { useNavigate, Link, useParams } from "react-router-dom"; import YAML from "yaml"; import { MuiChipsInput } from "mui-chips-input"; //import { useAlert import { ToastContainer, toast } from "react-toastify" import words from "shellwords"; import AvatarEditor from "react-avatar-editor"; const surfaceColor = "#27292D"; const inputColor = "#383B40"; const bodyDivStyle = { margin: "auto", width: "900px", zoom: 0.8, }; const actionListStyle = { paddingLeft: "10px", paddingRight: "10px", paddingBottom: "10px", paddingTop: "10px", marginTop: "5px", display: "flex", color: "white", position: "relative", backgroundColor: theme.palette.platformColor, }; const boxStyle = { color: "white", flex: "1", marginLeft: "10px", marginRight: "10px", paddingLeft: "30px", paddingRight: "30px", paddingBottom: "30px", paddingTop: "30px", display: "flex", flexDirection: "column", backgroundColor: theme.palette.backgroundColor, }; const dividerStyle = { marginBottom: "10px", marginTop: "10px", height: "1px", width: "100%", backgroundColor: "grey", }; const appIconStyle = { marginLeft: "5px", }; export const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important", }, }); const rewrite = (args) => { return args.reduce(function (args, a) { if (0 === a.indexOf("-X")) { args.push("-X"); args.push(a.slice(2)); } else { args.push(a); } return args; }, []); }; const parseField = (s) => { return s.split(/: (.+)/); }; const isURL = (s) => { return /^https?:\/\//.test(s); }; // Parses CURL to a real request const parseCurl = (s) => { //console.log("CURL: ", s) if (0 != s.indexOf("curl ")) { console.log("Not curl start"); return ""; } try { var args = rewrite(words.split(s)); } catch (e) { return s; } var out = { method: "GET", header: {} }; var state = ""; args.forEach(function (arg) { switch (true) { case isURL(arg): out.url = arg; break; case arg === "-A" || arg === "--user-agent": state = "user-agent"; break; case arg === "-H" || arg === "--header": state = "header"; break; case arg === "-d" || arg === "--data" || arg === "--data-ascii": state = "data"; break; case arg === "-u" || arg === "--user": state = "user"; break; case arg === "-I" || arg === "--head": out.method = "HEAD"; break; case arg === "-X" || arg === "--request": state = "method"; break; case arg === "-b" || arg === "--cookie": state = "cookie"; break; case arg === "--compressed": out.header["Accept-Encoding"] = out.header["Accept-Encoding"] || "deflate, gzip"; break; case !!arg: switch (state) { case "header": var field = parseField(arg); out.header[field[0]] = field[1]; state = ""; break; case "user-agent": out.header["User-Agent"] = arg; state = ""; break; case "data": if (out.method === "GET" || out.method === "HEAD") out.method = "POST"; out.header["Content-Type"] = out.header["Content-Type"] || "application/x-www-form-urlencoded"; out.body = out.body ? out.body + "&" + arg : arg; state = ""; break; case "user": out.header["Authorization"] = "Basic " + btoa(arg); state = ""; break; case "method": out.method = arg; state = ""; break; case "cookie": out.header["Set-Cookie"] = arg; state = ""; break; } break; } }); return out; }; // Basically CRUD for each category + special // These are already tracked in the shuffle/shuffle-shared/blobs.go file // as backend should be used for managing this long-term export const appCategories = [ { "name": "Communication", "color": "#FFC107", "icon": "communication", "action_labels": ["List Messages", "Send Message", "Get Message", "Search messages", "List Attachments", "Get Attachment", "Get Contact"], }, { "name": "SIEM", "color": "#FFC107", "icon": "siem", "action_labels": ["Search", "List Alerts", "Close Alert", "Get Alert", "Create detection", "Add to lookup list", "Isolate endpoint",], }, { "name": "Eradication", "color": "#FFC107", "icon": "eradication", "action_labels": ["List Alerts", "Close Alert", "Get Alert", "Create detection", "Block hash", "Search Hosts", "Isolate host", "Unisolate host", "Trigger host scan",], }, { "name": "Cases", "color": "#FFC107", "icon": "cases", "action_labels": ["List tickets", "Get ticket", "Create ticket", "Close ticket", "Add comment", "Update ticket", "Search tickets"], }, { "name": "Assets", "color": "#FFC107", "icon": "assets", "action_labels": ["List Assets", "Get Asset", "Search Assets", "Search Users", "Search endpoints", "Search vulnerabilities"], }, { "name": "Intel", "color": "#FFC107", "icon": "intel", "action_labels": ["Get IOC", "Search IOC", "Create IOC", "Update IOC", "Delete IOC",], }, { "name": "IAM", "color": "#FFC107", "icon": "iam", "action_labels": ["Reset Password", "Enable user", "Disable user", "Get Identity", "Get Asset", "Search Identity", "Get KMS Key",], }, { "name": "Network", "color": "#FFC107", "icon": "network", "action_labels": ["Get Rules", "Allow IP", "Block IP",], }, { "name": "Other", "color": "#FFC107", "icon": "other", "action_labels": ["Update Info", "Get Info", "Get Status", "Get Version", "Get Health", "Get Config", "Get Configs", "Get Configs by type", "Get Configs by name", "Run script"], }, ] export const base64_decode = (str) => { return decodeURIComponent( atob(str).split("").map(function (c) { return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); }).join("") ); }; // Loops through properties to find the actual JSON output to use const getJsonObject = (properties) => { // Loop inside the JSON object and get the value of each key let jsonObject = {}; for (let key in properties) { const property = properties[key]; let subloop = false if (property.hasOwnProperty("type")) { if (property.type === "object" || property.type === "array") { subloop = true } } if (subloop) { if (property.hasOwnProperty("items") && property.items.hasOwnProperty("properties")) { const jsonret = getJsonObject(property.items.properties); if (property.type === "array") { //console.log("ARRAY!!") jsonObject[key] = [jsonret]; } else { jsonObject[key] = jsonret; } } else { if (property.hasOwnProperty("properties")) { const jsonret = getJsonObject(property.properties); if (property.type === "array") { //console.log("ARRAY2!!") jsonObject[key] = [jsonret]; } else { jsonObject[key] = jsonret; } } else { //console.log("No items or properties found: ", property); } } } else { if (property.hasOwnProperty("example")) { jsonObject[key] = property.example; } else if (property.hasOwnProperty("enum") && property.enum.length > 0) { jsonObject[key] = property.enum[0]; } else if (property.hasOwnProperty("default")) { jsonObject[key] = property.default; } else if (property.hasOwnProperty("maximum")) { jsonObject[key] = property.maximum; } else if (property.hasOwnProperty("minimum")) { jsonObject[key] = property.minimum; } else if (property.hasOwnProperty("type")) { if (property.type === "integer" || property.type === "number") { jsonObject[key] = 0; } else if (property.type === "boolean") { jsonObject[key] = false; } else if (property.type === "string") { jsonObject[key] = ""; } else { console.log("Unknown type: ", property); } } else { console.log("No example or enum found: ", property); } } } return jsonObject } // Should be different if logged in :| const AppCreator = (defaultprops) => { const { globalUrl, isLoaded } = defaultprops; const classes = useStyles(); //const alert = useAlert(); const params = useParams(); var props = JSON.parse(JSON.stringify(defaultprops)) props.match = {} props.match.params = params var upload = ""; let navigate = useNavigate(); const increaseAmount = 50; const actionNonBodyRequest = ["GET", "HEAD", "CONNECT"]; const actionBodyRequest = ["POST", "PUT", "PATCH", "DELETE"]; const authenticationOptions = [ "No authentication", "API key", "Bearer auth", "Basic auth", "JWT", "Oauth2", ]; const apikeySelection = ["Header", "Query"]; const [name, setName] = useState(""); const [contact, setContact] = useState(""); const [file, setFile] = useState(""); const [fileBase64, setFileBase64] = useState(""); const [isAppLoaded, setIsAppLoaded] = useState(false); const [isEditing, setIsEditing] = useState(false); const [description, setDescription] = useState(""); const [baseUrl, setBaseUrl] = useState(""); const [authenticationRequired, setAuthenticationRequired] = useState(false); const [authenticationOption, setAuthenticationOption] = useState( authenticationOptions[0] ); const [newWorkflowTags, setNewWorkflowTags] = React.useState([]); const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]); const [parameterName, setParameterName] = useState(""); const [parameterLocation, setParameterLocation] = useState( apikeySelection.length > 0 ? apikeySelection[0] : "" ); const [refreshUrl, setRefreshUrl] = useState(""); const [projectCategories, setProjectCategories] = useState([]); const [selectedCategory, setSelectedCategory] = useState(""); const [update, setUpdate] = useState(""); const [urlPathParameters] = useState([]); const [basedata, setBasedata] = React.useState({}); const [actions, setActions] = useState([]); const [filteredActions, setFilteredActions] = useState([]); const [errorCode, setErrorCode] = useState(""); const [appBuilding, setAppBuilding] = useState(false); const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false); const [actionAmount, setActionAmount] = useState(increaseAmount); const [oauth2Scopes, setOauth2Scopes] = useState([]); const [oauth2Type, setOauth2Type] = useState("delegated"); //client_credentials const [oauth2GrantType, setOauth2GrantType] = useState(""); const defaultAuth = { name: "", type: "header", example: "", }; const [extraAuth, setExtraAuth] = useState([]); const [app, setApp] = useState({}); const [appAuthentication, setAppAuthentication] = React.useState([]); const [selectedAction, setSelectedAction] = useState({}); const [authLoaded, setAuthLoaded] = useState(false); // From 2023: Example to handle action labels // Goal: Make this dynamically load from the backend // and make categories + labels modifyable. // Categories are the main categories in the App Framework const [categories, setCategories] = useState(appCategories) const redirectOpenApi = () => { navigate(`/apps/new?id=${appValidation}`) } const newUpload = React.useRef(null); const [openApiError, setOpenApiError] = React.useState(""); const [validation, setValidation] = React.useState(""); const [appValidation, setAppValidation] = React.useState(""); const [openApi, setOpenApi] = React.useState(""); const [openApiData, setOpenApiData] = React.useState(""); const [openApiModal, setOpenApiModal] = React.useState(false); const [appDownloadData, setAppDownloadData] = React.useState(""); useEffect(() => { console.log("In useEffect for openApiData: ", openApiData) }, [openApiData]); const uploadFile = (e) => { console.log("In uploadFile") const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; const files = isDropzone ? e.dataTransfer.files : e.target.files; const reader = new FileReader(); try { reader.addEventListener("load", (e) => { const content = e.target.result; console.log("set openapi data! ", content) setOpenApiData(content); setOpenApiModal(true); }); } catch (e) { console.log("Error in dropzone: ", e); } try { reader.readAsText(files[0]); } catch (error) { toast("Failed to read file"); } } const escapeApiData = (apidata) => { //console.log(apidata) try { return JSON.stringify(JSON.parse(apidata)); } catch (error) { console.log("JSON DECODE ERROR - TRY YAML"); } try { const parsed = YAML.parse(YAML.stringify(apidata)); //const parsed = YAML.parse(apidata)) return YAML.stringify(parsed); } catch (error) { console.log("YAML DECODE ERROR - TRY SOMETHING ELSE?: " + error); setOpenApiError("Local error: " + error.toString()); } return ""; } const validateOpenApi = (openApidata) => { var newApidata = escapeApiData(openApidata); if (newApidata === "") { // Used to return here newApidata = openApidata; return; } //console.log(newApidata) setValidation(true); fetch(globalUrl + "/api/v1/validate_openapi", { method: "POST", headers: { Accept: "application/json", }, body: openApidata, credentials: "include", }) .then((response) => { setValidation(false); return response.json(); }) .then((responseJson) => { if (responseJson.success) { setAppValidation(responseJson.id); } else { if (responseJson.reason !== undefined) { setOpenApiError(responseJson.reason); } toast("An error occurred in the response"); } }) .catch((error) => { setValidation(false); toast(error.toString()); setOpenApiError(error.toString()); }); }; const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); useEffect(() => { if (window.location.pathname.includes("apps/edit")) { setIsEditing(true); handleEditApp(props.match.params.appid); } else { checkQuery(); } }, []); const handleEditApp = (appid) => { fetch(globalUrl + "/api/v1/apps/" + appid + "/config", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { window.location.pathname = "/apps"; } return response.json(); }) .then((responseJson) => { if (responseJson.success === false) { toast("Failed to get the app"); setIsAppLoaded(true); window.location.pathname = "/search"; } else { parseIncomingOpenapiData(responseJson); } }) .catch((error) => { toast(error.toString()); }); }; // Checks if there is an ID in the query, and gets it if it doesn't exist. const checkQuery = () => { var urlParams = new URLSearchParams(window.location.search); if (!urlParams.has("id")) { setActionAmount(0); setIsAppLoaded(true); return; } //handleEditApp(urlParams.get("id")) // THIS has to stay due to ID may not exist as normal app yet fetch(globalUrl + "/api/v1/get_openapi/" + urlParams.get("id"), { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { throw new Error("NOT 200 :O"); } return response.json(); }) .then((responseJson) => { setIsAppLoaded(true); if (!responseJson.success) { toast("Failed to get app config. Do you have access?"); } else { parseIncomingOpenapiData(responseJson); } }) .catch((error) => { console.log("Error: ", error.toString()); toast(error.toString()); }); }; const setFileFromb64 = () => { //const img = document.getElementById('logo') //var canvas = document.createElement('canvas') //var ctx = canvas.getContext('2d') //img.onload = function() { // console.log("LOADED?") // ctx.drawImage(img, 0, 0) // const canvasUrl = canvas.toDataURL() // console.log(canvasUrl) // setFileBase64(canvasUrl) //} }; const handleGetRef = (parameter, data) => { try { if (parameter === null || parameter["$ref"] === undefined) { //console.log("$ref not found in getref for: ", parameter) return parameter; } } catch (e) { console.log("Failed getting $ref of ", parameter); return parameter; } const paramsplit = parameter["$ref"].split("/"); if (paramsplit[0] !== "#") { console.log("Bad param: ", paramsplit); return parameter; } var newitem = data; for (let paramkey in paramsplit) { var tmpparam = paramsplit[paramkey]; if (tmpparam === "#") { continue; } if (newitem[tmpparam] === undefined) { return parameter; } newitem = newitem[tmpparam]; } return newitem; }; // Sets the data up as it should be at later points // This is the data FROM the database, not what's being saved const parseIncomingOpenapiData = (data) => { var parsedDecoded = "" try { const decoded = base64_decode(data.openapi) parsedDecoded = decoded } catch (e) { console.log("Failed JSON parsing: ", e) parsedDecoded = data } if (data.openapi === null) { toast("Failed to load OpenAPI for app. Please contact support if this persists.") setIsAppLoaded(true); return } //console.log("Decoded: ", parsedDecoded) const parsedapp = data.openapi === undefined || data.openapi === null ? data : JSON.parse(parsedDecoded); data = parsedapp.body === undefined ? parsedapp : parsedapp.body; var jsonvalid = false; var tmpvalue = ""; try { data = JSON.parse(data); jsonvalid = true; } catch (e) { console.log("Error JSON: ", e); } if (!jsonvalid) { try { data = YAML.parse(data); jsonvalid = true; } catch (e) { console.log("Error YAML: ", e); } } if (!jsonvalid) { toast("OpenAPI data is invalid."); return; } setBasedata(data); console.log("Info: ", data) try { if (data.info !== null && data.info !== undefined) { if (data.info.title !== undefined && data.info.title !== null) { if (data.info.title.endsWith(" API")) { data.info.title = data.info.title.substring(0, data.info.title.length - 4) } else if (data.info.title.endsWith("API")) { data.info.title = data.info.title.substring(0, data.info.title.length - 3) } if (data.info.title.length > 29) { setName(data.info.title.slice(0, 29)); } else { setName(data.info.title); } } setDescription(data.info.description); document.title = "Apps - " + data.info.title; if (data.info["x-logo"] !== undefined) { if (data.info["x-logo"].url !== undefined) { //console.log("PARSED LOGO: ", data.info["x-logo"].url); setFileBase64(data.info["x-logo"].url); } else { setFileBase64(data.info["x-logo"]); } //console.log(""); //console.log(""); //console.log("LOGO: ", data.info["x-logo"]); //console.log(""); //console.log(""); } if (data.info.contact !== undefined) { setContact(data.info.contact); } if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) { if (typeof data.info["x-categories"] === "array") { } else { } setNewWorkflowCategories(data.info["x-categories"]); } } } catch (e) { console.log("Failed setting info: ", e) } console.log("Tags: ", data.tags) try { if (data.tags !== undefined && data.tags.length > 0) { var newtags = []; for (let tagkey in data.tags) { if (data.tags[tagkey].name.length > 50) { console.log("Skipping tag because it's too long: ",data.tags[tagkey].name.length); continue; } newtags.push(data.tags[tagkey].name); } if (newtags.length > 10) { newtags = newtags.slice(0, 9); } setNewWorkflowTags(newtags); } } catch (e) { console.log("Failed to parse tags: ", e) } // This is annoying (: console.log("Security schemes 1: ", data.securitySchemes) // Weird generator problems to be handle var securitySchemes = undefined try { if (data.securitySchemes !== undefined) { securitySchemes = data.securitySchemes if (securitySchemes === undefined) { securitySchemes = data.securityDefinitions; } } if (securitySchemes === undefined && data.components !== undefined) { securitySchemes = data.components.securitySchemes; if (securitySchemes === undefined) { securitySchemes = data.components.securityDefinitions; } } } catch (e) { console.log("Failed to parse security schemes: ", e) } console.log("Security schemes 2: ", securitySchemes) const allowedfunctions = [ "GET", "CONNECT", "HEAD", "DELETE", "POST", "PATCH", "PUT", ]; var newActions = []; var wordlist = {}; var all_categories = []; var parentUrl = "" console.log("Paths: ", data.paths) if (data.paths !== null && data.paths !== undefined) { for (let [path, pathvalue] of Object.entries(data.paths)) { for (let [method, methodvalue] of Object.entries(pathvalue)) { if (methodvalue === null) { toast("Skipped method (null)" + method); continue; } if (!allowedfunctions.includes(method.toUpperCase())) { // Typical YAML issue if (method !== "parameters") { console.log("Invalid method: ", method, "data: ", methodvalue); //toast("Skipped method (not allowed): " + method); } continue; } //console.log("METHOD: ", methodvalue) var tmpname = methodvalue.summary; if ( methodvalue.operationId !== undefined && methodvalue.operationId !== null && methodvalue.operationId.length > 0 && (tmpname === undefined || tmpname.length === 0) ) { tmpname = methodvalue.operationId; } if (tmpname !== undefined && tmpname !== null) { tmpname = tmpname.replaceAll(".", " "); } if ((tmpname === undefined || tmpname === null) && methodvalue.description !== undefined && methodvalue.description !== null && methodvalue.description.length > 0) { tmpname = methodvalue.description.replaceAll(".", " ").replaceAll("_", " ") } var newaction = { name: tmpname, description: methodvalue.description, url: path, file_field: "", method: method.toUpperCase(), headers: "", queries: [], paths: [], body: "", errors: [], example_response: "", action_label: "No Label", required_bodyfields: [], }; if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) { console.log("LABEL: ", methodvalue["x-label"]) var correctlabel = "" const labels = methodvalue["x-label"].split(",") for (let labelkey in labels) { var label = labels[labelkey].trim() if (label.toLowerCase() === "no label") { continue } // Remove quotes and escapes label = label.replace(/['"]+/g, '') label = label.replace(/\\/g, '') //label = label.replace("_", " ", -1) //label = label.charAt(0).toUpperCase() + label.slice(1) correctlabel = label break } console.log("LABEL: ", correctlabel) // FIX: Map labels only if they're actually in the category list //newaction.action_label = methodvalue["x-label"] newaction.action_label = correctlabel } if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) { newaction.required_bodyfields = methodvalue["x-required-fields"] } if (newaction.url !== undefined && newaction.url !== null && newaction.url.includes("_shuffle_replace_")) { //const regex = /_shuffle_replace_\d/i; const regex = /_shuffle_replace_\d+/i newaction.url = newaction.url.replaceAll(new RegExp(regex, 'g'), "") } // Finding category if (path.includes("/")) { const pathsplit = path.split("/"); var categoryindex = -1; // Stupid way of finding a category/grouping for (let splitkey in pathsplit) { if (pathsplit[splitkey].includes("_shuffle_replace_")) { //const regex = /_shuffle_replace_\d/i; const regex = /_shuffle_replace_\d+/i //console.log("NEW: ", pathsplit[splitkey] = pathsplit[splitkey].replaceAll(new RegExp(regex, 'g'), "") } if ( pathsplit[splitkey].length > 0 && pathsplit[splitkey] !== "v1" && pathsplit[splitkey] !== "v2" && pathsplit[splitkey] !== "api" && pathsplit[splitkey] !== "1.0" && pathsplit[splitkey] !== "apis" ) { newaction["category"] = pathsplit[splitkey]; if (!all_categories.includes(pathsplit[splitkey])) { all_categories.push(pathsplit[splitkey]); } break; } } } if (path === "/files/{file_id}/content") { //console.log("FILE DOWNLOAD Method: ", path, method, methodvalue) } // Typescript? I think not ;) if (methodvalue["requestBody"] !== undefined) { if (methodvalue["requestBody"]["$ref"] !== undefined && methodvalue["requestBody"]["$ref"] !== null) { // Handle ref // console.log("Ref: ", methodvalue["requestBody"]["$ref"]) const parameter = handleGetRef({ $ref: methodvalue["requestBody"]["$ref"]}, data); console.log("PARAM: ", parameter) if (parameter.content !== undefined && parameter.content !== null) { methodvalue["requestBody"]["content"] = parameter.content console.log("Set content!") } } if (methodvalue["requestBody"]["content"] !== undefined) { // Handle content - XML or JSON // if ( methodvalue["requestBody"]["content"]["application/json"] !== undefined ) { //newaction["headers"] = "" //"Content-Type=application/json\nAccept=application/json"; if ( methodvalue["requestBody"]["content"]["application/json"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["application/json"]["schema"] !== null ) { //console.log("Schema: ", methodvalue["requestBody"]["content"]["application/json"]["schema"]) try { if (methodvalue["requestBody"]["content"]["application/json"]["schema"]["properties"] !== undefined) { // Read out properties from a JSON object const jsonObject = getJsonObject(methodvalue["requestBody"]["content"]["application/json"]["schema"]["properties"]) //console.log("JSON OBJECT: ", jsonObject) if (jsonObject !== undefined && jsonObject !== null) { try { newaction["body"] = JSON.stringify(jsonObject, null, 2) } catch (e) { console.log("JSON object parse error: ", e) } } //newaction["body"] = JSON.stringify(jsonObject, null, 2); var tmpobject = {}; for (let prop of methodvalue["requestBody"]["content"]["application/json"]["schema"]["properties"]) { tmpobject[prop] = `\$\{${prop}\}`; } //console.log("Data: ", data) for (let subkey in methodvalue["requestBody"]["content"]["application/json"]["schema"]["required"]) { const tmpitem = methodvalue["requestBody"]["content"]["application/json"]["schema"]["required"][subkey]; tmpobject[tmpitem] = `\$\{${tmpitem}\}`; } newaction["body"] = JSON.stringify(tmpobject, null, 2); } else if ( methodvalue["requestBody"]["content"]["application/json"]["schema"]["$ref"] !== undefined && methodvalue["requestBody"]["content"]["application/json"]["schema"]["$ref"] !== null) { const retRef = handleGetRef(methodvalue["requestBody"]["content"]["application/json"]["schema"], data); var newbody = {}; // Can handle default, required, description and type for (let propkey in retRef.properties) { console.log("replace: ", propkey) const parsedkey = propkey.replaceAll(" ", "_").toLowerCase(); newbody[parsedkey] = "${" + parsedkey + "}"; } newaction["body"] = JSON.stringify(newbody, null, 2); } } catch (e) { console.log("RequestBody json error: ", e, path) } } } else if ( methodvalue["requestBody"]["content"]["application/xml"] !== undefined ) { console.log("METHOD XML: ", methodvalue); //newaction["headers"] = "" //"Content-Type=application/xml\nAccept=application/xml"; if ( methodvalue["requestBody"]["content"]["application/xml"][ "schema" ] !== undefined && methodvalue["requestBody"]["content"]["application/xml"][ "schema" ] !== null ) { try { if ( methodvalue["requestBody"]["content"]["application/xml"][ "schema" ]["properties"] !== undefined ) { var tmpobject = {}; for (let [prop, propvalue] of Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["properties"])) { tmpobject[prop] = `\$\{${prop}\}`; } for (let [subkey,subkeyval] in Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["required"])) { const tmpitem = methodvalue["requestBody"]["content"][ "application/xml" ]["schema"]["required"][subkey]; tmpobject[tmpitem] = `\$\{${tmpitem}\}`; } //console.log("OBJ XML: ", tmpobject) //newaction["body"] = XML.stringify(tmpobject, null, 2) } } catch (e) { console.log("RequestBody xml error: ", e, path) } } } else { if (methodvalue["requestBody"]["content"]["example"] !== undefined) { if (methodvalue["requestBody"]["content"]["example"]["example"] !== undefined) { newaction["body"] = methodvalue["requestBody"]["content"]["example"]["example"] } } if (methodvalue["requestBody"]["content"]["multipart/form-data"] !== undefined) { if ( methodvalue["requestBody"]["content"][ "multipart/form-data" ]["schema"] !== undefined && methodvalue["requestBody"]["content"][ "multipart/form-data" ]["schema"] !== null ) { try { if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["type"] === "object") { const fieldname = methodvalue["requestBody"]["content"][ "multipart/form-data" ]["schema"]["properties"]["fieldname"]; if (fieldname !== undefined) { //console.log("FIELDNAME: ", fieldname); newaction.file_field = fieldname["value"]; } else { for (const [subkey, subvalue] of Object.entries(methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"])) { if (subkey.includes("file")) { console.log("Found subkey field for file: ", path, method, methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"]) newaction.file_field = subkey break } } if (newaction.file_field === undefined || newaction.file_field === null || newaction.file_field.length === 0) { console.log("No file fieldname found: ", methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"]) } } } else { console.log("No type found: ", methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]) } } catch (e) { console.log("Multipart/form error: ", e, path) } } } else { var schemas = []; const content = methodvalue["requestBody"]["content"]; if (content !== undefined && content !== null) { //console.log("CONTENT: ", content) for (const [subkey, subvalue] of Object.entries(content)) { if (subvalue["schema"] !== undefined && subvalue["schema"] !== null) { console.log("SCHEMA: ", subvalue["schema"]) if (subvalue["schema"]["$ref"] !== undefined && subvalue["schema"]["$ref"] !== null) { console.log("SCHEMA FOUND REF!") if (!schemas.includes(subvalue["schema"]["$ref"])) { schemas.push(subvalue["schema"]["$ref"]); } } } else { if (subvalue["example"] !== undefined && subvalue["example"] !== null) { newaction["body"] = subvalue["example"] } else { console.log("ERROR: couldn't find schema for ", subvalue, method, path); } } } } try { if (schemas.length === 1) { const parameter = handleGetRef({ $ref: schemas[0] }, data); console.log("Reading type from parameter: ", parameter) if (parameter.properties !== undefined && parameter["type"] === "object") { var newbody = {}; for (let propkey in parameter.properties) { console.log("propkey2: ", propkey) const parsedkey = propkey.replaceAll(" ", "_").toLowerCase(); if (parameter.properties[propkey].type === undefined) { console.log( "Skipping (4): ", parameter.properties[propkey] ); continue; } if (parameter.properties[propkey].type === "string") { if ( parameter.properties[propkey].description !== undefined ) { newbody[parsedkey] = parameter.properties[propkey].description; } else { newbody[parsedkey] = ""; } } else if ( parameter.properties[propkey].type.includes("int") || parameter.properties[propkey].type.includes("uint64") ) { newbody[parsedkey] = 0; } else if ( parameter.properties[propkey].type.includes("boolean") ) { newbody[parsedkey] = false; } else if ( parameter.properties[propkey].type.includes("array") ) { newbody[parsedkey] = []; } else { console.log( "CANT HANDLE JSON TYPE (4)", parameter.properties[propkey].type, parameter.properties[propkey], path ); newbody[parsedkey] = []; } } newaction["body"] = JSON.stringify(newbody, null, 2); } else { console.log( "CANT HANDLE PARAM: (4) ", parameter.properties, path ); } } } catch (e) { console.log("Param Error: ", e, path) } } } } } if ( methodvalue.responses !== undefined && methodvalue.responses !== null ) { if (methodvalue.responses.default !== undefined) { if (methodvalue.responses.default.content !== undefined) { if ( methodvalue.responses.default.content["text/plain"] !== undefined ) { if ( methodvalue.responses.default.content["text/plain"]["schema"] !== undefined) { if (methodvalue.responses.default.content["text/plain"]["schema"]["example"] !== undefined) { newaction.example_response = methodvalue.responses.default.content["text/plain"]["schema"]["example"] } if (methodvalue.responses.default.content["text/plain"]["schema"]["format"] === "binary" && methodvalue.responses.default.content["text/plain"]["schema"]["type"] === "string") { newaction.example_response = "shuffle_file_download" } } } } } else { var selectedReturn = ""; if (methodvalue.responses["200"] !== undefined) { selectedReturn = "200"; } else if (methodvalue.responses["201"] !== undefined) { selectedReturn = "201"; } // Parsing examples. This should be standardized lol if (methodvalue.responses[selectedReturn] !== undefined) { const selectedExample = methodvalue.responses[selectedReturn]; if (selectedExample["content"] !== undefined) { if ( selectedExample["content"]["application/json"] !== undefined ) { if ( selectedExample["content"]["application/json"]["schema"] !== undefined && selectedExample["content"]["application/json"]["schema"] !== null ) { //console.log("JSON Output: ", selectedExample["content"]["application/json"]["schema"]) if (selectedExample["content"]["application/json"]["schema"]["properties"] !== undefined && selectedExample["content"]["application/json"]["schema"]["properties"] !== null) { const jsonObject = getJsonObject(selectedExample["content"]["application/json"]["schema"]["properties"]) if (jsonObject !== undefined && jsonObject !== null) { try { newaction.example_response = JSON.stringify(jsonObject, null, 2) } catch (e) { console.log("JSON object output parse error: ", e) } } } if (selectedExample["content"]["application/json"]["schema"]["$ref"] !== undefined) { //console.log("REF EXAMPLE: ", selectedExample["content"]["application/json"]["schema"]) const parameter = handleGetRef( selectedExample["content"]["application/json"][ "schema" ], data ); //console.log("Reading parameter type 2", parameter) if (parameter.properties !== undefined && parameter["type"] === "object") { var newbody = {}; for (let propkey in parameter.properties) { //console.log("propkey3: ", propkey) const parsedkey = propkey.replaceAll(" ", "_").toLowerCase(); if (parameter.properties[propkey].type === undefined) { console.log( "Skipping (1): ", parameter.properties[propkey] ); continue; } if ( parameter.properties[propkey].type === "string" ) { if ( parameter.properties[propkey].description !== undefined ) { newbody[parsedkey] = parameter.properties[propkey].description; } else { newbody[parsedkey] = ""; } } else if ( parameter.properties[propkey].type.includes("int") ) { newbody[parsedkey] = 0; } else if ( parameter.properties[propkey].type.includes( "boolean" ) ) { newbody[parsedkey] = false; } else if ( parameter.properties[propkey].type.includes( "array" ) ) { //console.log("Added empty array. Base is: ", parameter.properties[propkey].type) //const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"], data) newbody[parsedkey] = []; } else { console.log("CANT HANDLE JSON TYPE ", parameter.properties[propkey].type,parameter.properties[propkey] ); newbody[parsedkey] = []; } } newaction.example_response = JSON.stringify( newbody, null, 2 ); } else { console.log( "CANT HANDLE PARAM: (1) ", parameter.properties ); } } else { // Just selecting the first one. bleh. if ( selectedExample["content"]["application/json"][ "schema" ]["allOf"] !== undefined ) { //console.log("ALLOF: ", selectedExample["content"]["application/json"]["schema"]["allOf"]) //console.log("BAD EXAMPLE: (SKIP ALLOF) ", selectedExample["content"]["application/json"]["schema"]["allOf"]) var selectedComponent = selectedExample["content"]["application/json"][ "schema" ]["allOf"]; if (selectedComponent.length >= 1) { selectedComponent = selectedComponent[0]; const parameter = handleGetRef( selectedComponent, data ); console.log("Reading parameter type 3!") if (parameter.properties !== undefined && parameter["type"] === "object") { var newbody = {}; for (let propkey in parameter.properties) { console.log("propkey4: ", propkey) const parsedkey = propkey.replaceAll(" ", "_").toLowerCase(); if ( parameter.properties[propkey].type === undefined ) { console.log( "Skipping (2): ", parameter.properties[propkey] ); continue; } if ( parameter.properties[propkey].type === "string" ) { if ( parameter.properties[propkey] .description !== undefined ) { newbody[parsedkey] = parameter.properties[propkey].description; } else { newbody[parsedkey] = ""; } } else if ( parameter.properties[propkey].type.includes( "int" ) ) { newbody[parsedkey] = 0; } else if ( parameter.properties[propkey].type.includes( "boolean" ) ) { newbody[parsedkey] = false; } else { console.log( "CANT HANDLE JSON TYPE (2) ", parameter.properties[propkey].type ); newbody[parsedkey] = []; } } newaction.example_response = JSON.stringify( newbody, null, 2 ); //newaction.example_response = JSON.stringify(parameter.properties, null, 2) } else { //newaction.example_response = parameter.properties console.log( "CANT HANDLE PARAM: (3) ", parameter.properties ); } } else { } } else if ( selectedExample["content"]["application/json"][ "schema" ]["properties"] !== undefined ) { if ( selectedExample["content"]["application/json"][ "schema" ]["properties"]["data"] !== undefined ) { const parameter = handleGetRef( selectedExample["content"]["application/json"][ "schema" ]["properties"]["data"], data ); console.log("Reading type 3: ", parameter) if (parameter.properties !== undefined && parameter["type"] === "object") { var newbody = {}; for (let propkey in parameter.properties) { console.log("propkey5: ", propkey) const parsedkey = propkey .replaceAll(" ", "_") .toLowerCase(); if ( parameter.properties[propkey].type === undefined ) { console.log( "Skipping (3): ", parameter.properties[propkey] ); continue; } if ( parameter.properties[propkey].type === "string" ) { if ( parameter.properties[propkey] .description !== undefined ) { newbody[parsedkey] = parameter.properties[propkey].description; } else { newbody[parsedkey] = ""; } console.log(parameter.properties[propkey]); } else if ( parameter.properties[propkey].type.includes( "int" ) ) { newbody[parsedkey] = 0; } else { console.log( "CANT HANDLE JSON TYPE (3) ", parameter.properties[propkey].type ); newbody[parsedkey] = []; } } newaction.example_response = JSON.stringify( newbody, null, 2 ); //newaction.example_response = JSON.stringify(parameter.properties, null, 2) } else { //newaction.example_response = parameter.properties console.log( "CANT HANDLE PARAM: (3) ", parameter.properties ); } } } } } } } } } } for (let paramkey in methodvalue.parameters) { const parameter = handleGetRef(methodvalue.parameters[paramkey], data); if (parameter.in === "query") { var tmpaction = { description: parameter.description, name: parameter.name, required: parameter.required, in: "query", }; if (parameter.example !== undefined && parameter.example !== null) { tmpaction.example = parameter.example } if (parameter.required === undefined) { tmpaction.required = false; } newaction.queries.push(tmpaction); } else if (parameter.in === "path") { // FIXME - parse this to the URL too newaction.paths.push(parameter.name); // FIXME: This doesn't follow OpenAPI3 exactly. // https://swagger.io/docs/specification/describing-request-body/ // https://swagger.io/docs/specification/describing-parameters/ // Need to split the data. } else if (parameter.in === "body") { // FIXME: Add tracking for components // E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml if (parameter.example !== undefined && parameter.example !== null) { if (newaction.body === undefined || newaction.body === null || newaction.body.length < 5) { newaction.body = parameter.example } } } else if (parameter.in === "header") { newaction.headers += `${parameter.name}=${parameter.example}\n`; } else { console.log( "WARNING: don't know how to handle this param: ", parameter ); } } // Check if body is valid JSON. if (newaction.body !== undefined && newaction.body !== null && newaction.body.length > 0) { // Trim starting / ending newlines, spaces and tabs newaction.body = newaction.body.trim() } if (newaction.name === "" || newaction.name === undefined) { // Find a unique part of the string // FIXME: Looks for length between /, find the one where they differ // Should find others with the same START to their path // Make a list of reserved names? Aka things that show up only once if (Object.getOwnPropertyNames(wordlist).length === 0) { for (let [newpath, pathvalue] of Object.entries(data.paths)) { const newpathsplit = newpath.split("/"); for (let splitkey in newpathsplit) { const pathitem = newpathsplit[splitkey].toLowerCase(); if (wordlist[pathitem] === undefined) { wordlist[pathitem] = 1; } else { wordlist[pathitem] += 1; } } } } //console.log("WORDLIST: ", wordlist) // Remove underscores and make it normal with upper case etc const urlsplit = path.split("/"); if (urlsplit.length > 0) { var curname = ""; for (let urlkey in urlsplit) { var subpath = urlsplit[urlkey]; if (wordlist[subpath] > 2 || subpath.length < 1) { continue; } curname = subpath; break; } // FIXME: If name exists, // FIXME: Check if first part of parsedname is verb, otherwise use method const parsedname = curname .split("_") .join(" ") .split("-") .join(" ") .split("{") .join(" ") .split("}") .join(" ") .trim(); if (parsedname.length === 0) { newaction.errors.push("Missing name"); } else { const newname = method.charAt(0).toUpperCase() + method.slice(1) + " " + parsedname; const searchactions = newActions.find( (data) => data.name === newname ); //console.log("SEARCH: ", searchactions); if (searchactions !== undefined) { newaction.errors.push("Missing name"); } else { newaction.name = newname; } } } else { newaction.errors.push("Missing name"); } } //newaction.action_label = "No Label" newActions.push(newaction); } } if (data.servers !== undefined && data.servers.length > 0) { var firstUrl = data.servers[0].url; if ( firstUrl.includes("{") && firstUrl.includes("}") && data.servers[0].variables !== undefined ) { const regex = /{\w+}/g; const found = firstUrl.match(regex); if (found !== null) { for (let foundkey in found) { const item = found[foundkey].slice(1, found[foundkey].length - 1); const foundVar = data.servers[0].variables[item]; if (foundVar["default"] !== undefined) { firstUrl = firstUrl.replace(found[foundkey], foundVar["default"]); } } } } if (firstUrl.endsWith("/")) { setBaseUrl(firstUrl.slice(0, firstUrl.length - 1)); parentUrl = firstUrl.slice(0, firstUrl.length - 1) } else { setBaseUrl(firstUrl) parentUrl = firstUrl } } } if (securitySchemes !== undefined) { //console.log("SECURITY: ", securitySchemes) var newauth = []; try { var optionset = false for (const [key, value] of Object.entries(securitySchemes)) { //console.log("AUTH: ", key, value); if (key === "jwt") { setAuthenticationOption("JWT"); setAuthenticationRequired(true); if ( value.in !== undefined && value.in !== null && value.in.length > 0 ) { setParameterName(value.in); optionset = true } } else if (value.scheme === "bearer") { setAuthenticationOption("Bearer auth"); setAuthenticationRequired(true); optionset = true } else if (key === "ApiKeyAuth" || key === "Token" || ((value.in === "header" || value.in === "query") && value.name !== undefined)) { //if (optionset === false) { // optionset = true //} if (optionset === false) { optionset = true value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1) setParameterLocation(value.in); if (!apikeySelection.includes(value.in)) { console.log("APIKEY SELECT: ", apikeySelection); toast("Might be error in setting up API key authentication"); } console.log("PARAM NAME: ", value.name); setAuthenticationOption("API key"); setParameterName(value.name); setAuthenticationRequired(true); newauth.push({ "name": key, "type": value.in.toLowerCase(), "in": value.in.toLowerCase(), "example": "", }) } else { newauth.push({ "name": key, "type": value.in.toLowerCase(), "in": value.in.toLowerCase(), "example": "", }) } if (value.description !== undefined && value.description !== null && value.description.length > 0) { // Don't want a real description - just the ones we're replacing with if ((value.description.split(" ").length - 1) <= 2) { setRefreshUrl(value.description) } } } else if (value.scheme === "basic") { setAuthenticationOption("Basic auth"); setAuthenticationRequired(true); optionset = true } else if (value.scheme === "oauth2") { setAuthenticationOption("Oauth2"); setAuthenticationRequired(true); optionset = true } else if (value.type === "oauth2" || key === "Oauth2" || key === "Oauth2c" || (key !== undefined && key !== null && key.toLowerCase().includes("oauth2"))) { //toast("Can't handle Oauth2 auth yet.") setAuthenticationOption("Oauth2"); setAuthenticationRequired(true); optionset = true //console.log("FLOW-1: ", value) const flowkey = value.flow === undefined ? "flows" : "flow"; //console.log("FLOW: ", value[flowkey]) // Doesn't seem to be used for now const basekey = value[flowkey].authorizationCode !== undefined ? "authorizationCode" : "implicit"; // Kind of fucked up, but it works for now? if (value["x-grant-type"] !== undefined && value["x-grant-type"] !== null && value["x-grant-type"].length !== 0) { setOauth2GrantType(value["x-grant-type"]) } //console.log("FLOW2: ", value[flowkey][basekey]) if (value[flowkey] !== undefined && value[flowkey][basekey] !== undefined ) { var newparamname = parameterName if (value[flowkey][basekey].authorizationUrl !== undefined && value[flowkey][basekey].authorizationUrl !== null && value[flowkey][basekey].authorizationUrl.length !== 0 && parameterName.length === 0) { setParameterName(value[flowkey][basekey].authorizationUrl); } else { setOauth2Type("application") } var tokenUrl = ""; if (value[flowkey][basekey].tokenUrl !== undefined) { setParameterLocation(value[flowkey][basekey].tokenUrl); tokenUrl = value[flowkey][basekey].tokenUrl; } else { setParameterLocation(""); } if (value[flowkey][basekey].refreshUrl !== undefined) { setRefreshUrl(value[flowkey][basekey].refreshUrl); } else if (tokenUrl.length > 0) { setRefreshUrl(tokenUrl); } if ( value[flowkey][basekey].scopes !== undefined && value[flowkey][basekey].scopes !== null ) { if (value[flowkey][basekey].scopes.length > 0) { setOauth2Scopes(value[flowkey][basekey].scopes); } else { var newscopes = []; for (let [scopekey, scopevalue] of Object.entries(value[flowkey][basekey].scopes)) { if (scopekey.startsWith("http")) { const scopekeysplit = scopekey.split("/"); if (scopekeysplit.length < 5) { console.log("Skipping scope: ", scopekey); toast("Skipping scope: " + scopekey); continue; } //console.log("Checking scope for: ", scopekey, scopekeysplit.length) } newscopes.push(scopekey); } setOauth2Scopes(newscopes); } } } else { console.log( "Bad flowkey and basekey for oauth2: ", flowkey, basekey ); } } else { toast("Couldn't handle AUTH type: ", key); //newauth.push({ // "name": key, // "type": value.in, // "example": "", //}) } } } catch (e) { toast("Failed to handle auth") console.log("Error: ", e) } if (newauth.length > 0) { newauth = newauth.filter(data => data.name != "ApiKeyAuth") setExtraAuth(newauth); } } console.log("PARent: ", parentUrl) var prefixCheck = "/v1" if (parentUrl.includes("/")) { const urlsplit = parentUrl.split("/") if (urlsplit.length > 2) { // Skip if http:// in it too prefixCheck = "/" + urlsplit.slice(3).join("/") } console.log("Prefix: ", prefixCheck) if (prefixCheck.length > 0 && prefixCheck !== "/" && prefixCheck.startsWith("/")) { for (var actionKey in newActions) { const action = newActions[actionKey] if (action.url !== undefined && action.url !== null && action.url.startsWith(prefixCheck)) { newActions[actionKey].url = action.url.slice(prefixCheck.length, action.url.length) } console.log("Action: ", newActions[actionKey].url) } } } console.log("Actions: ", newActions.length, " BaseURL: ", parentUrl) var newActions2 = [] // Remove with duplicate action URLs for (var actionKey in newActions) { const action = newActions[actionKey] if (action.url === undefined || action.url === null) { continue } var found = false for (var actionKey2 in newActions2) { const action2 = newActions2[actionKey2] if (action2.url === undefined || action2.url === null) { continue } if (action.url === action2.url) { found = true break } } if (!found) { newActions2.push(action) } else { //console.log("NOT skipping duplicate action: ", action.url, ". Should merge contents") newActions2.push(action) } } //console.log("Actions: ", newActions.length, " Actions2: ", newActions2.length) newActions = newActions2 if (newActions.length > increaseAmount - 1) { setActionAmount(increaseAmount); } else { setActionAmount(newActions.length); } //const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" if (newActions.length > 1000 && isCloud) { toast("Cut down actions from " + newActions.length + " to 999 because of limit"); newActions = newActions.slice(0, 999); } setProjectCategories(all_categories); // Rearrange them by which has action_label const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label") console.log("First actions: ", firstActions) const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label") newActions = firstActions.concat(secondActions) setActions(newActions); //data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label setFilteredActions(newActions); setIsAppLoaded(true); }; // Saving the app that's been configured. // Save SAVE app const submitApp = () => { toast("Uploading and building app " + name); setAppBuilding(true); setErrorCode(""); // Format the information const splitBase = baseUrl.split("/"); const host = splitBase[2]; const schemes = [splitBase[0]]; const basePath = "/" + splitBase.slice(3).join("/"); const data = { openapi: "3.0.0", info: { title: name, description: description, version: "1.0", "x-logo": fileBase64, }, servers: [{ url: baseUrl }], host: host, basePath: basePath, schemes: schemes, paths: {}, editing: isEditing, components: { securitySchemes: {}, }, id: props.match.params.appid, }; if (isEditing === false) { var urlParams = new URLSearchParams(window.location.search); if (urlParams !== undefined && urlParams !== null && urlParams.has("id")) { data.id = urlParams.get("id") } //id: props.match.params.appid, } if (basedata.info !== undefined && basedata.info.contact !== undefined) { data.info["contact"] = basedata.info.contact; } else if (contact === "") { data.info["contact"] = { name: "@Anonymous Shuffle User", url: "https://twitter.com/shuffleio", email: "support@shuffler.io", }; } else { data.info["contact"] = contact; } if (newWorkflowTags.length > 0) { var newtags = []; for (let tagkey in newWorkflowTags) { newtags.push({ name: newWorkflowTags[tagkey] }); } data["tags"] = newtags; } if (newWorkflowCategories.length > 0) { data["info"]["x-categories"] = newWorkflowCategories; } // Handles actions var handledPaths = [] for (let actionkey in actions) { var item = JSON.parse(JSON.stringify(actions[actionkey])) if (item.errors.length > 0) { toast("Saving with error in action " + item.name); } if (item.name === undefined && item.description !== undefined) { item.name = item.description; } // Basic way to allow multiple of the same path var pathjoin = item.url+"_"+item.method.toLowerCase() if (handledPaths.includes(pathjoin)) { // Max 1000 of same. Will it be ok for graphql longterm? const baseurl = item.url for (let i = 0; i < 1000; i++) { item.url = baseurl+"_shuffle_replace_"+i pathjoin = item.url+"_"+item.method.toLowerCase() if (handledPaths.includes(pathjoin)) { continue } break } } handledPaths.push(pathjoin) if (data.paths[item.url] === null || data.paths[item.url] === undefined) { data.paths[item.url] = {}; } const regex = /[A-Za-z0-9 _]/g; if (item.name === undefined) { console.log("Skipping action ", item); continue; } const found = item.name.match(regex); if (found !== null) { item.name = found.join(""); } // Workaround for proper responses. No default as JSON for now data.paths[item.url][item.method.toLowerCase()] = { responses: { default: { description: "default", content: { "text/plain": { schema: { type: "string", example: "", }, }, }, }, }, summary: item.name, operationId: item.name.split(" ").join("_"), description: item.description, parameters: [], requestBody: { content: {}, }, }; if (item.action_label !== undefined && item.action_label !== "" && item.action_label !== "No Label") { //console.log("Action label: ", item.action_label) data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label } if (item.required_bodyfields !== undefined && item.required_bodyfields !== null && item.required_bodyfields.length > 0) { console.log("Required bodyfields: ", item.required_bodyfields) data.paths[item.url][item.method.toLowerCase()]["x-required-fields"] = item.required_bodyfields } //console.log("ACTION: ", item) if (item.example_response !== undefined && item.example_response !== null && item.example_response.length > 0) { if (item["example_response"] === "shuffle_file_download") { data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.type = "string" data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.format = "binary" /* schema: type: object properties: username: type: string avatar: # <-- image embedded into JSON type: string format: byte description: Base64-encoded contents of the avatar image */ } else { // FIXME: Shallow copy of the string var showResult = Object.assign("", item.example_response).trim(); showResult = showResult.split(" None").join(' "None"'); showResult = showResult.split("'").join('"'); showResult = showResult.split(" False").join(" false"); showResult = showResult.split(" True").join(" true"); var jsonvalid = true; try { const tmp = String(JSON.parse(showResult)); if (!showResult.includes("{") && !showResult.includes("[")) { jsonvalid = false; } } catch (e) { jsonvalid = false; } data.paths[item.url][item.method.toLowerCase()].responses["default"][ "content" ]["text/plain"].schema.type = "string"; if (jsonvalid) { // FIXME: Add a JSON parser here - don't run it as a string. data.paths[item.url][item.method.toLowerCase()].responses["default"][ "content" ]["text/plain"].schema.example = showResult; } else { data.paths[item.url][item.method.toLowerCase()].responses["default"][ "content" ]["text/plain"].schema.example = item.example_response; } } } if (item.queries.length > 0) { var skipped = false; var querynames = [] for (let querykey in item.queries) { let queryitem = item.queries[querykey]; if (queryitem === undefined || queryitem === null || queryitem.name === undefined || queryitem.name === null || queryitem.name === "") { continue } // A fix for duplicate items if (querynames.includes(queryitem.name.toLowerCase())) { continue } querynames.push(queryitem.name.toLowerCase()) if (queryitem.name.toLowerCase() == "url") { console.log(item.name + " uses a bad query: url"); continue; //skipped = true //break } if (queryitem.name.toLowerCase() == "file_id") { item.queries[querykey].name = "fileid" continue; //skipped = true //break } if ( queryitem.name.toLowerCase() == "url" || queryitem.name.toLowerCase() == "body" || queryitem.name.toLowerCase() == "self" || queryitem.name.toLowerCase() == "query" || queryitem.name.toLowerCase() == "ssl_verify" || queryitem.name.toLowerCase() == "queries" || queryitem.name.toLowerCase() == "headers" || queryitem.name.toLowerCase() == "list" || queryitem.name.toLowerCase() == "dict" || queryitem.name.toLowerCase() == "str" || queryitem.name.toLowerCase() == "int" || queryitem.name.toLowerCase() == "access_token") { /* queryitem.name.includes("[") || queryitem.name.includes("]") || queryitem.name.includes("{") || queryitem.name.includes("}") || queryitem.name.includes("(") || queryitem.name.includes(")") || queryitem.name.includes("!") || queryitem.name.includes("@") || queryitem.name.includes("#") || queryitem.name.includes("$") || queryitem.name.includes("%") || queryitem.name.includes("^") || queryitem.name.includes("&") || queryitem.name.includes(":") || queryitem.name.includes(";") || queryitem.name.includes("<") || queryitem.name.includes(">") || queryitem.name.includes('"') || queryitem.name.includes("'") ) { */ console.log(item.name + " error: uses a bad query - not adding: ",queryitem.name) // Find a replacement for the invalid ones first. continue; } var newitem = { in: "query", name: queryitem.name, description: "Generated by shuffler.io OpenAPI", required: queryitem.required, schema: { type: "string", }, }; if (queryitem.example !== undefined) { newitem.example = queryitem.example } if (queryitem.description !== undefined) { newitem.description = queryitem.description; } data.paths[item.url][item.method.toLowerCase()].parameters.push( newitem ); //console.log(queryitem) } // Bad code as it doesn't allow for "anything". if (skipped) { toast( "Bad configuration of " + item.name + ". Skipping because queries are invalid." ); continue; } } //data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) if (item.paths.length > 0) { for (let querykey in item.paths) { let queryitem = item.paths[querykey]; if (queryitem.toLowerCase() == "url") { queryitem = "action_url"; } if (queryitem.toLowerCase() == "apikey") { queryitem = "action_apikey"; } newitem = { in: "path", name: queryitem, description: "Generated by shuffler.io OpenAPI", required: true, schema: { type: "string", }, }; if (queryitem.description !== undefined) { newitem.description = queryitem.description; } data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem); //console.log(queryitem) } } else { // Always goes here if they didn't click anything :/ const values = getCurrentPaths(item.url); const paths = values[0]; for (let querykey in paths) { let queryitem = paths[querykey]; newitem = { in: "path", name: queryitem, description: "Generated by shuffler.io OpenAPI", required: false, schema: { type: "string", }, }; if (queryitem.description !== undefined) { newitem.description = queryitem.description; } data.paths[item.url][item.method.toLowerCase()].parameters.push( newitem ); //console.log(queryitem) } } const methodname = item.method.toLowerCase() if (methodname === "post" || methodname === "put" || methodname === "patch" || methodname === "delete") { if ( item.body !== undefined && item.body !== null && item.body.length > 0 ) { //console.log("GOT BODY: ", item.url, item.method, item.body) // Replacing dollarsign insertions that aren't escaped // This is to stop it from messing with systems in Shuffle. // This MAY cause it to be a little weird in other systems however, // but it's the only way we can properly support e.g. GraphQL // with good examples var newbody = "" for (let bodykey in item.body) { if (item.body[bodykey] === "$") { if (bodykey > 0) { const newkey = parseInt(bodykey, 10) if (item.body[newkey-1] !== "\\") { if (item.body[newkey+1] !== "\{") { newbody += "\\" } } newbody += item.body[bodykey] } else { newbody += "\\" newbody += item.body[bodykey] } } else { newbody += item.body[bodykey] } } //console.log("New body: ", newbody) if (newbody !== item.body) { item.body = newbody } //var pathjoin = item.url+"_"+item.method.toLowerCase() const required = false; newitem = { in: "body", name: "body", multiline: true, description: "Generated by shuffler.io OpenAPI", required: required, example: item.body, schema: { type: "string", }, }; // FIXME - add application/json if JSON example? data.paths[item.url][item.method.toLowerCase()]["requestBody"] = { description: "Generated by Shuffler.io", required: required, content: { example: { example: item.body, }, }, }; data.paths[item.url][item.method.toLowerCase()].parameters.push( newitem ); } else if (actionBodyRequest.includes(item.method.toUpperCase())) { // Appending an empty field const required = false; newitem = { in: "body", name: "body", multiline: true, description: "Generated by shuffler.io OpenAPI", required: required, example: "", schema: { type: "string", }, }; // FIXME - add application/json if JSON example? data.paths[item.url][item.method.toLowerCase()]["requestBody"] = { description: "Generated by Shuffler.io", required: required, content: { example: { example: "", }, }, }; data.paths[item.url][item.method.toLowerCase()].parameters.push( newitem ); } else { //console.log("Nothing to append?") } } // https://swagger.io/docs/specification/describing-request-body/file-upload/ if (item.file_field !== undefined && item.file_field !== null && item.file_field.length > 0) { data.paths[item.url][item.method.toLowerCase()]["requestBody"][ "content" ]["multipart/form-data"] = { schema: { type: "object", properties: { fieldname: { type: "string", value: item.file_field, }, }, }, }; //console.log(data.paths[item.url][item.method.toLowerCase()]["requestBody"]["content"]["multipart/form-data"]) } if (item.headers.length > 0) { const required = false; const headersSplit = item.headers.split("\n"); for (let headerkey in headersSplit) { const header = headersSplit[headerkey]; var innerkey = "" var value = ""; if (header.length > 0 && header.includes("= ")) { const headersplit = header.split("= "); innerkey = headersplit[0]; value = headersplit[1]; } else if (header.length > 0 && header.includes(" =")) { const headersplit = header.split(" ="); innerkey = headersplit[0]; value = headersplit[1]; } else if (header.length > 0 && header.includes("=")) { const headersplit = header.split("="); innerkey = headersplit[0]; value = headersplit[1]; } else if (header.length > 0 && header.includes(": ")) { const headersplit = header.split(": "); innerkey = headersplit[0]; value = headersplit[1]; } else if (header.length > 0 && header.includes(" :")) { const headersplit = header.split(" :"); innerkey = headersplit[0]; value = headersplit[1]; } else if (header.length > 0 && header.includes(":")) { const headersplit = header.split(":"); innerkey = headersplit[0]; value = headersplit[1]; } else { continue; } if (innerkey.length > 0 && value.length > 0) { newitem = { in: "header", name: innerkey, multiline: false, description: "Header generated by shuffler.io OpenAPI", required: false, example: value, schema: { type: "string", }, }; data.paths[item.url][item.method.toLowerCase()].parameters.push( newitem ); } } } } if (authenticationOption === "API key") { if (parameterName.length === 0) { toast("A field name for the APIkey must be defined"); setAppBuilding(false); return; } console.log("Paramname: ", parameterName) var newparamName = parameterName.replaceAll('"', ""); newparamName = newparamName.replaceAll("'", ""); data.components.securitySchemes["ApiKeyAuth"] = { type: "apiKey", in: parameterLocation.toLowerCase(), name: newparamName, description: refreshUrl, } console.log("Full auth component: ", data.components.securitySchemes["ApiKeyAuth"]) } else if (authenticationOption === "Bearer auth") { data.components.securitySchemes["BearerAuth"] = { type: "http", scheme: "bearer", bearerFormat: "UUID", }; } else if (authenticationOption === "JWT") { data.components.securitySchemes["jwt"] = { type: "http", scheme: "bearer", bearerFormat: "JWT", in: parameterName, }; console.log("SECURITYSCHEMES: ", data.components); } else if (authenticationOption === "Basic auth") { data.components.securitySchemes["BasicAuth"] = { type: "http", scheme: "basic", }; } else if (authenticationOption === "Oauth2") { console.log("oauth2: ", parameterName) var newparamName = parameterName.replaceAll('"', ""); newparamName = newparamName.replaceAll("'", ""); // FIXME - this is a hack to get around the fact that the oauth2 // flow is not properly defined if (oauth2Type === "application") { newparamName = "" } //parameterName, parameterValue, revocationUrl data.components.securitySchemes["Oauth2"] = { type: "oauth2", description: "Oauth2.0 authorizationCode authentication", flow: { authorizationCode: { authorizationUrl: newparamName, tokenUrl: parameterLocation, refreshUrl: refreshUrl, scopes: oauth2Scopes === undefined || oauth2Scopes === null ? [] : oauth2Scopes, }, }, }; //if (value[flowkey][basekey]["x-grant-type"] !== undefined && value[flowkey][basekey]["x-grant-type"] !== null && value[flowkey][basekey]["x-grant-type"].length !== 0) { if (oauth2GrantType.length > 0) { data.components.securitySchemes["Oauth2"]["x-grant-type"] = oauth2GrantType; } console.log("SECURITYSCHEMES: ", data.components); } if (setExtraAuth.length > 0) { for (let authkey in extraAuth) { const curauth = extraAuth[authkey]; if (curauth.name.length === 0 || curauth.name.toLowerCase() == "url") { toast("Can't add extra auth with empty name or Name URL"); setAppBuilding(false); return; } data.components.securitySchemes[curauth.name] = { type: "apiKey", in: curauth.type, name: curauth.name, }; } } setAppDownloadData(JSON.stringify(data, null, 4)) fetch(globalUrl + "/api/v1/verify_openapi", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(data, null, 4), credentials: "include", }) .then((response) => { //if (response.status !== 200) { // setErrorCode("An error occurred during validation") // throw new Error("NOT 200 :O") //} setAppBuilding(false); return response.json(); }) .then((responseJson) => { if (!responseJson.success) { if (responseJson.extra !== undefined && responseJson.extra !== null) { toast("Failed building: " + responseJson.extra); } if (responseJson.reason !== undefined) { setErrorCode(responseJson.reason); if (responseJson.extra === undefined && responseJson.extra === null) { toast("Failed to verify: " + responseJson.reason); } } } else { toast("Successfully uploaded openapi"); if (window.location.pathname.includes("/new")) { if (responseJson.id !== undefined && responseJson.id !== null) { window.location = `/apps/edit/${responseJson.id}`; } } } }) .catch((error) => { setAppBuilding(false); setErrorCode(error.toString()); toast(error.toString()); }); }; const bearerAuth = authenticationOption === "Bearer auth" ? (