commit bedf56ba8c6c38f3bb45f4ecb506151aa07dff09 Author: lolcat Date: Mon Jun 1 03:31:51 2026 -0400 datadome, cloudflare, google and akamai on suicide watch diff --git a/README.md b/README.md new file mode 100644 index 0000000..0615154 --- /dev/null +++ b/README.md @@ -0,0 +1,298 @@ +# 4play +4play is a Firefox extension that lets you bot shit. + +## Features +- Does not expose any `webdriver.*` variables, it's just an extension +- Environment is undetectable. Inject JavaScript through JS isolated worlds. +- Simple as fuck + +## How it works +4play makes Firefox connects to a websocket server in the background. The server then sends commands to the browser. You can clean up containers, navigate to pages, inject javascript or extract raw page content & request headers. + +# Fucking why? +It's a complete replacement for dogshit libraries like Puppeteer/Selenium/Playwright, which are developed by Google engineers. These libraries purposely leak bot signals even with the usage of stealth scripts. With this library, a single Firefox instance can use multiple proxies at the same time across multiple containers and completely avoid detection. Fuck your cat & mouse game, im tired of your shit. + +I'm so fucking tired of retards recommending libraries that are so easily detectable. Just give me something that works you fucks. + +## Installation +Install the 4play extension on a **CLEAN** Firefox install. Do **NOT** use your main profile, it will mess up your tabs and containers. Enter credentials in the extension and connect. + +Then, start the server: +```bash +cd server +npm install http ws +node hello-world.js +``` + +## Example server script + +```js +const fplay = require("./fplay.js"); + +var port = 3030; +var timeout = 30000; + +fplay.event.on("server_ready", function(){ + + console.log("listening on port " + port + " (timeout=" + timeout + ")"); +}); + +fplay.event.on("browser_connect", async function(ws){ + + const ua = await fplay.get_ua(ws); + console.log("Connection from " + ua); + + // clean up + const blanktab = await fplay.close_all_tabs(ws); + await fplay.delete_all_containers(ws); + + // create container + const container = await fplay.container_create(ws); + console.log(container); + + // assign proxy + await fplay.container_attach_proxy( + ws, + container, + { + type: "socks", // socks(is socks5) http, https, socks4 + host: "whatever-proxy-host-you-want.io", + port: 1339, + username: "admin", + password: "1234", + proxyDNS: true, + } + ); + + // open tab + const newtab = await fplay.tab_open(ws, "https://lolcat.ca", true, container); + console.log(newtab); + + // get page's title + var result = await fplay.tab_inject_js(ws, newtab, "document.title", true); + + console.log(result); +}); + +fplay.init(port, timeout); +``` + +# 4play API Reference + +## Initialization + +### `fplay.init(port?, command_timeout?)` + +Starts the HTTP/WebSocket server. + +- `port` (`number`, default: `3030`) - Port to listen on. +- `password` (`string`, default: `cnc`) - Websocket path (acts as password) +- `command_timeout` (`number`, default: `5000`) - Timeout in ms for commands sent to the browser. + +**Returns:** `void` + +--- + +## Misc + +### `fplay.get_ua(ws)` + +Gets the browser's user agent string. + +- `ws` (`WebSocket`) - Active browser connection. + +**Returns:** `string` on success, `false` on failure. + +### `fplay.wait_random(min, max)` + +Waits for an inclusive amount of time in miliseconds. + +**Returns:** `boolean` `true`. + +### `fplay.parse_cookies(headers)` + +Extracts a `key => value` cookie pair from raw headers. + +- `headers` (`Array`) - List of header strings + +**Returns:** `Array: key => value`. + +--- + +## Tabs + +### `fplay.get_tab_list(ws)` + +Gets a list of all open tabs. + +- `ws` (`WebSocket`) - Active browser connection. + +**Returns:** `Array` on success (each object contains `id`, `index`, `status`, `active`, `title`, `url`, `container`), `false` on failure. + +### `fplay.tab_open(ws, url, await_dom_ready?, container?)` + +Opens a new tab. + +- `ws` (`WebSocket`) - Active browser connection. +- `url` (`string`) - URL to open. +- `await_dom_ready` (`boolean`, default: `false`) - If `true`, waits for the page to fully load before resolving. +- `container` (`object | string | null`, default: `null`) - Container to open the tab in. Accepts a container object (with `.id`) or a raw container object. + +**Returns:** `object` (tab data: `id`, `index`, `status`, `active`, `title`, `url`, `container`) on success, `false` on failure. + +### `fplay.tab_close(ws, tab_ids)` + +Closes one or more tabs. + +- `ws` (`WebSocket`) - Active browser connection. +- `tab_ids` (`number | number[] | object`) - Tab ID, array of tab IDs, or a tab object (with `.id`). + +**Returns:** `number` (count of closed tabs) on success, `false` on failure. + +### `fplay.close_all_tabs(ws)` + +Closes all tabs except a newly opened blank tab. + +- `ws` (`WebSocket`) - Active browser connection. + +**Returns:** `object` - The new blank tab's data. + +### `fplay.tab_focus(ws, tabid)` + +Focuses (activates) a tab. + +- `ws` (`WebSocket`) - Active browser connection. +- `tabid` (`number | object`) - Tab ID or tab object (with `.id`). + +**Returns:** `boolean` - `true` if focused successfully, `false` otherwise. + +### `fplay.tab_exists(ws, tabid)` + +Checks whether a tab exists. + +- `ws` (`WebSocket`) - Active browser connection. +- `tabid` (`number | object`) - Tab ID or tab object (with `.id`). + +**Returns:** `boolean` - `true` if the tab exists, `false` otherwise. + +### `fplay.tab_inject_js(ws, tabid, js, isolated?)` + +Injects and executes JavaScript in a tab. + +- `ws` (`WebSocket`) - Active browser connection. +- `tabid` (`number | object`) - Tab ID or tab object (with `.id`). +- `js` (`string`) - JavaScript code to execute. +- `isolated` (`boolean`, default: `false`) - If `true`, runs in an isolated world. See MDN for more info. + +**Returns:** `object` - `{ status: true, result: any }` on success, `{ status: string, result: null }` on error (status contains the error message), `{ status: false, result: null }` on failure. + +--- + +## Containers + +### `fplay.container_create(ws, name?)` + +Creates a new container with a random color and icon. + +- `ws` (`WebSocket`) - Active browser connection. +- `name` (`string | null`, default: `null`) - Container name. If `null`, auto-generates a name (`sesh1`, `sesh2`, etc.). + +**Returns:** `object` (`id`, `name`, `color`, `icon`, `proxy`) on success, `false` on failure. + +### `fplay.get_container_list(ws)` + +Gets a list of all containers. + +- `ws` (`WebSocket`) - Active browser connection. + +**Returns:** `Array` on success (each object contains `id`, `name`, `icon`, `color`, `proxy`), `false` on failure. + +### `fplay.container_delete(ws, id)` + +Deletes one or more containers. + +- `ws` (`WebSocket`) - Active browser connection. +- `id` (`string | string[] | object`) - Container ID, array of container IDs, or a container object (with `.id`). + +**Returns:** `number` (count of deleted containers) on success, `false` on failure. + +### `fplay.container_exists(ws, id)` + +Checks whether a container exists. + +- `ws` (`WebSocket`) - Active browser connection. +- `id` (`string | object`) - Container ID or container object (with `.id`). + +**Returns:** `boolean` - `true` if the container exists, `false` otherwise. + +### `fplay.delete_all_containers(ws)` + +Deletes all containers. + +- `ws` (`WebSocket`) - Active browser connection. + +**Returns:** `number` - Count of deleted containers. + +### `fplay.container_attach_proxy(ws, id, proxy)` + +Attaches a proxy configuration to a container. + +- `ws` (`WebSocket`) - Active browser connection. +- `id` (`string | object`) - Container ID or container object (with `.id`). +- `proxy` (`object`) - Proxy configuration object: + - `type` (`string`) - `"socks"`, `"socks4"`, `"http"`, or `"https"` + - `host` (`string`) - Proxy host + - `port` (`number`) - Proxy port + - `username` (`string`, optional) - Proxy username + - `password` (`string`, optional) - Proxy password + - `proxyDNS` (`boolean`, optional) - Whether to proxy DNS lookups + +**Returns:** `boolean` - `true` if attached successfully, `false` otherwise. + +### `fplay.container_detach_proxy(ws, id)` + +Removes the proxy from a container, reverting to direct connection. + +- `ws` (`WebSocket`) - Active browser connection. +- `id` (`string | object`) - Container ID or container object (with `.id`). + +**Returns:** `boolean` - `true` if detached successfully, `false` otherwise. + +--- + +## Events + +### `fplay.wait_for_dom_ready(tabid)` + +Returns a promise that resolves when a tab's DOM finishes loading. + +- `tabid` (`number`) - Tab ID to wait on. + +**Returns:** `Promise` - Resolves with tab data on success, `false` on load failure. + +### `fplay.event` + +An `EventEmitter` instance that emits the following events: + +| Event | Data | Description | +|---|---|---| +| `server_ready` | `{}` | Server started listening | +| `browser_connect` | `ws` | Browser connected | +| `browser_disconnect` | `{}` | Browser disconnected | +| `dom_ready` | `{ id, index, status, active, title, url, container }` | A tab finished loading | +| `dom_load_fail` | `{ id }` | A tab failed to load | +| `web_request` | `{ id, url, status, origin, type, method, container, headers }` | A main-frame request was sent | + +--- + +## Properties + +| Property | Type | Description | +|---|---|---| +| `fplay.browser_connected` | `boolean` | Whether the browser extension is currently connected | +| `fplay.PORT` | `number` | Current server port | +| `fplay.COMMAND_TIMEOUT` | `number` | Current command timeout in ms | +| `fplay.PASSWORD` | `string` | Server password | + +# License +AGPLv3 diff --git a/ext/bg.js b/ext/bg.js new file mode 100644 index 0000000..0b06a39 --- /dev/null +++ b/ext/bg.js @@ -0,0 +1,559 @@ + +const STATUS_CONNECTING = 0; +const STATUS_OFFLINE = 1; +const STATUS_CONNECTED = 2; + +var conn_attempts = 0; + +var connected = false; +var timeoutId; +var global_ws = null; + +var container_count = 0; + +var proxy_map = {}; + +browser.browserAction.setBadgeBackgroundColor({ + color: [0, 0, 0, 0] +}); + +// helper functions +async function get_tabs(){ + + var tabs = await browser.tabs.query({}); + + return tabs.map(tab => ({ + id: tab.id, + index: tab.index, + status: tab.status, + active: tab.active, + title: tab.title, + url: tab.url, + container: tab.cookieStoreId + })); +} + +async function tab_exists(id){ + + try{ + + await browser.tabs.get(id); + return true; + }catch{ + + return false; + } +} + +async function get_container_list(){ + + const containers = await browser.contextualIdentities.query({}); + + var list = []; + for(var i=0; i " + msg); + ws.send(msg); +} + +function send_event(ws, msg = {}){ + + var msg = JSON.stringify(msg); + console.log("-> " + msg); + ws.send(msg); +} + +async function set_status(status){ + + switch(status){ + case STATUS_CONNECTING: + conn_attempts++; + + browser.browserAction.setBadgeText({text: "🔵"}); + await browser.storage.local.set({"status": STATUS_CONNECTING, "attempts": conn_attempts}); + break; + + case STATUS_CONNECTED: + conn_attempts = 0; + + browser.browserAction.setBadgeText({text: "🟢"}); + await browser.storage.local.set({"status": STATUS_CONNECTED, "attempts": 0}); + break; + + case STATUS_OFFLINE: + browser.browserAction.setBadgeText({text: "🔴"}); + await browser.storage.local.set({"status": STATUS_OFFLINE}); + break; + } +} + +function ws_connect_timeout(url, timeoutMs = 5000){ + + timeoutMs = parseInt(timeoutMs); + + return new Promise(function(resolve, reject){ + + var ws = null; + global_ws = null; + + try{ + + ws = new WebSocket(url); + + }catch(error){ + + setTimeout(async function(){ + + ws_init(); + reject(new Error("Bad URL")); + }, timeoutMs); + return; + } + + attach_ws_events(ws); + /* + ws.addEventListener("open", function(){console.log("open!");}); + ws.addEventListener("message", function(){console.log("message!");}); + ws.addEventListener("close", function(){console.log("close!");});*/ + + connected = false; + + // Set up the timeout + timeoutId = setTimeout(function(){ + if(!connected){ + + ws.close(); + reject(new Error("Timeout")); + } + }, timeoutMs); + + ws.addEventListener("open", function(){ + connected = true; + clearTimeout(timeoutId); + resolve(ws); + }); + + ws.addEventListener("close", function(){ + + clearTimeout(timeoutId); + setTimeout(async function(){ + + ws_init(); + reject(new Error("Close")); + }, timeoutMs); + }); + }); +} + +async function ws_init(){ + + await set_status(STATUS_CONNECTING); + var config = await browser.storage.local.get(); + + try{ + + global_ws = await ws_connect_timeout(config.ws_url, config.ws_timeout); + }catch(error){ + + console.log("ws: " + error); + return; + } + + // online +} + +function attach_ws_events(ws){ + + ws.addEventListener("open", async function(){ + + console.log("ws: connected"); + await set_status(STATUS_CONNECTED); + }); + + ws.addEventListener("message", async function(e){ + + console.log("<- " + e.data); + + var msg = JSON.parse(e.data); + var seqid = msg.seqid; + + switch(msg.action){ + + // + // Misc + // + case "get_ua": + send(ws, seqid, { + "ua": navigator.userAgent + }); + break; + + // + // Tabs + // + case "get_tabs": + send(ws, seqid, { + "tabs": await get_tabs() + }); + break; + + case "tab_open": + var tab = + await browser.tabs.create({ + url: msg.url, + ...(typeof msg.container == "string" && { cookieStoreId: msg.container }) + }); + + // immediately return even if its not loaded yet + send(ws, seqid, { + data: { + id: tab.id, + index: tab.index, + status: tab.status, + active: tab.active, + title: tab.title, + url: tab.url, + container: tab.cookieStoreId + } + }); + break; + + case "tab_close": + var closed_tabs = 0; + + switch(typeof msg.tabid){ + + case "number": + var exists = await tab_exists(msg.tabid); + + if(exists){ + await browser.tabs.remove(msg.tabid); + closed_tabs = 1; + } + break; + + case "object": + var exists = false; + + for(var i=0; i eval(code), + args: [msg.js], + ...(msg.isolated === true && { world: "ISOLATED" }) + }); + + send(ws, seqid, {"status": true, "result": result}); + }catch(err){ + + send(ws, seqid, {"status": err.name + ": " + err.message}); + } + break; + + // + // Containers + // + case "get_container_list": + var containers = await get_container_list(); + + send(ws, seqid, { + "containers": containers + }); + break; + + case "container_create": + + // generate random container attributes + container_count++; + + var name = null; + + if(typeof msg.name != "undefined"){ + + name = msg.name; + }else{ + + name = "sesh" + container_count; + } + + const color = [ + "blue", + "turquoise", + "green", + "yellow", + "orange", + "red", + "pink", + "purple" + ][Math.floor(Math.random() * 8)]; + + const icon = [ + "fingerprint", + "briefcase", + "dollar", + "cart", + "circle", + "gift", + "vacation", + "food", + "fruit", + "pet", + "tree", + "chill", + "fence" + ][Math.floor(Math.random() * 13)]; + + const container = await browser.contextualIdentities.create({ + name: name, + color: color, + icon: icon + }); + + proxy_map[container.cookieStoreId] = { + type: "direct" + }; + + send(ws, seqid, { + id: container.cookieStoreId, + name: name, + color: color, + icon: icon, + proxy: proxy_map[container.cookieStoreId] + }); + + break; + + case "container_exists": + var exists = await container_exists(msg.id); + send(ws, seqid, {"exists": exists}); + break; + + case "container_delete": + var deleted_containers = 0; + + switch(typeof msg.id){ + + case "number": + var exists = await container_exists(msg.id); + + if(exists){ + await browser.contextualIdentities.remove(msg.id); + delete(proxy_map[msg.id]); + deleted_containers = 1; + } + break; + + case "object": + var exists = false; + + for(var i=0; i"], types: ["main_frame"]}, + ["requestHeaders"] +); + +browser.proxy.onRequest.addListener(function(request){ + + const proxy_config = proxy_map[request.cookieStoreId]; + if(proxy_config){ + + return proxy_config; + } + + // fallback, should not happen + return { + type: "direct" + } +}, +{ + urls: [""] +}); + +browser.tabs.onUpdated.addListener(function(tabid, event, tab){ + + if(connected === false){ return; } + + if(event.status === "complete"){ + + send_event( + global_ws, + { + "action": "dom_ready", + "data": { + id: tab.id, + index: tab.index, + status: tab.status, + active: tab.active, + title: tab.title, + url: tab.url, + container: tab.cookieStoreId + } + } + ); + } +}); + +browser.webNavigation.onErrorOccurred.addListener(function(page){ + + if(connected === false){ return; } + + send_event( + global_ws, + { + "action": "dom_load_fail", + "data": { + id: page.tabId + } + } + ); +}); + +(async function(){ + await ws_init(); +})(); diff --git a/ext/icon.png b/ext/icon.png new file mode 100644 index 0000000..e74b86a Binary files /dev/null and b/ext/icon.png differ diff --git a/ext/icons/connected.png b/ext/icons/connected.png new file mode 100755 index 0000000..8b183f5 Binary files /dev/null and b/ext/icons/connected.png differ diff --git a/ext/icons/connecting.png b/ext/icons/connecting.png new file mode 100644 index 0000000..07f371b Binary files /dev/null and b/ext/icons/connecting.png differ diff --git a/ext/icons/offline.png b/ext/icons/offline.png new file mode 100755 index 0000000..d57801e Binary files /dev/null and b/ext/icons/offline.png differ diff --git a/ext/manifest.json b/ext/manifest.json new file mode 100644 index 0000000..1d71722 --- /dev/null +++ b/ext/manifest.json @@ -0,0 +1,30 @@ +{ + "manifest_version": 2, + "name": "4play", + "version": "1.0", + "description": "4play & dominate", + "icons": { + "48": "icon.png" + }, + "browser_action": { + "default_icon": "icon.png", + "default_title": "4play", + "default_popup": "popup.html" + }, + "permissions": [ + "tabs", + "contextualIdentities", + "cookies", + "proxy", + "webNavigation", + "webRequest", + "webRequestBlocking", + "scripting", + "", + "activeTab", + "storage" + ], + "background": { + "scripts": ["bg.js"] + } +} diff --git a/ext/popup-style.css b/ext/popup-style.css new file mode 100644 index 0000000..6f88acd --- /dev/null +++ b/ext/popup-style.css @@ -0,0 +1,80 @@ +body{ + padding:0; + margin:0; + font-family:sans-serif; + font-size:14px; + color:#bdae93; + background:#282828; + min-width:350px; +} + +.header{ + padding:4px 10px; + font-weight:bold; + font-size:24px; + border-bottom:1px solid #282828; + background:#3c3836; + overflow:hidden; + line-height:32px; + font-family:monospace; + color:#d5c4a1; +} + +.header img{ + float:left; + margin-right:7px; +} + +.content{ + margin:10px 10px 27px 10px; +} + +.title{ + font-size:18px; + font-weight:bold; + display:block; + margin-bottom:10px; +} + +.title::before{ + content:"#"; + color:#a89984; +} + +label{ + display:block; + color:#a89984; +} + +.option-wrapper{ + margin-bottom:10px; +} + +input{ + all:unset; + display:block; + width:100%; + background:#1d2021; + border:1px solid #504545; + padding:4px 7px; + box-sizing:border-box; +} + +input:focus{ + border-color:#928374; +} + +#status{ + font-size:14px; + overflow:hidden; + line-height:18px; + padding:4px 10px; + border-bottom:1px solid #504545; + background:#1d2021; + font-family:monospace; +} + +#status img{ + float:left; + margin-right:4px; +} diff --git a/ext/popup.html b/ext/popup.html new file mode 100644 index 0000000..8dea7de --- /dev/null +++ b/ext/popup.html @@ -0,0 +1,27 @@ + + + + + + + +
+ + 4play +
+
+
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/ext/popup.js b/ext/popup.js new file mode 100644 index 0000000..a303314 --- /dev/null +++ b/ext/popup.js @@ -0,0 +1,89 @@ + +// this code triggers on popup load + +const STATUS_CONNECTING = 0; +const STATUS_OFFLINE = 1; +const STATUS_CONNECTED = 2; + +document.addEventListener('DOMContentLoaded', async function(){ + + // generate initial config + var config = await browser.storage.local.get(); + + if(typeof config.ws_url == "undefined"){ + + await browser.storage.local.set({"ws_url": "ws://localhost:3030/cnc"}); + } + + if(typeof config.ws_timeout == "undefined"){ + + await browser.storage.local.set({"ws_timeout": 5000}); + } + + // update form + config = await browser.storage.local.get(); // refresh first + + var form = document.getElementsByClassName("content")[0]; + + var form_ws_url = document.getElementsByName("ws_url")[0]; + var form_ws_timeout = document.getElementsByName("ws_timeout")[0]; + + form_ws_url.value = config.ws_url; + form_ws_timeout.value = config.ws_timeout; + + // attach events to both form inputs to update config + form_ws_url.addEventListener("input", async function(){ + + await browser.storage.local.set({"ws_url": form_ws_url.value}); + }); + + form_ws_timeout.addEventListener("input", async function(e){ + + form_ws_timeout.value = form_ws_timeout.value.replaceAll(/[^0-9]/g, ""); + var timeout = form_ws_timeout.value; + + if(form_ws_timeout.value == ""){ + + timeout = 5000; + } + + await browser.storage.local.set({"ws_timeout": timeout}); + }); + + // show the websocket status + await html_gen_status(); + + setInterval(async function(){ + + await html_gen_status(); + }, 1000); +}); + +async function html_gen_status(){ + + var dom_status = document.getElementById("status"); + var icon_path = null; + var text = null; + + var config = await browser.storage.local.get(); + + switch(config.status){ + + case STATUS_CONNECTING: + icon_path = "connecting"; + text = `Connecting... Attempt #${config.attempts}`; + break; + + case STATUS_CONNECTED: + icon_path = "connected"; + text = `Connected & ready to kick ass`; + break; + + case STATUS_OFFLINE: + icon_path = "offline"; + text = `C&C unreachable. Attempt #${config.attempts}`; + break; + } + + dom_status.innerHTML = `${text}`; +} diff --git a/server/hello-world.js b/server/hello-world.js new file mode 100644 index 0000000..d0d6310 --- /dev/null +++ b/server/hello-world.js @@ -0,0 +1,50 @@ +const fplay = require("./lib/fplay.js"); + +var port = 3030; +var timeout = 30000; +var password = "cnc"; + +fplay.event.on("server_ready", function(){ + + console.log("listening on port " + port + " (timeout=" + timeout + ")"); +}); + +fplay.event.on("browser_connect", async function(ws){ + + const ua = await fplay.get_ua(ws); + console.log("Connection from " + ua); + + // clean up + const blanktab = await fplay.close_all_tabs(ws); + await fplay.delete_all_containers(ws); + + // create container + const container = await fplay.container_create(ws); + console.log(container); + + // assign proxy + /* + await fplay.container_attach_proxy( + ws, + container, + { + type: "socks", // socks(is socks5) http, https, socks4 + host: "whatever-proxy-host-you-want.io", + port: 1339, + username: "admin", + password: "1234", + proxyDNS: true, + } + );*/ + + // open tab + const newtab = await fplay.tab_open(ws, "https://lolcat.ca", true, container); + console.log(newtab); + + // get page's title + var result = await fplay.tab_inject_js(ws, newtab, "document.title", true); + + console.log(result); +}); + +fplay.init(port, password, timeout); diff --git a/server/lib/fplay.js b/server/lib/fplay.js new file mode 100644 index 0000000..91f5f19 --- /dev/null +++ b/server/lib/fplay.js @@ -0,0 +1,538 @@ +const http = require("http"); +const { WebSocketServer } = require("ws"); + +var fplay = {}; + +fplay.PORT = 3030; +fplay.COMMAND_TIMEOUT = 5000; +fplay.PASSWORD = "cnc"; + +const { EventEmitter } = require("events"); +fplay.event = new EventEmitter(); + +fplay.server = http.createServer(function(req, res){ + + switch(req.url){ + + case "/" + fplay.PASSWORD: + res.writeHead(426, { "Content-Type": "application/json" }); + res.end(JSON.stringify({"status": "No websocket upgrade received"})); + break; + + default: + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ "status": "Invalid endpoint" })); + } +}); + +fplay.wss = new WebSocketServer({ server: fplay.server, path: "/" + fplay.PASSWORD }); + +fplay.seqid = 0; +fplay.pending = new Map(); // promise map +fplay.pending_dom_ready = new Map(); + +// helper functions +fplay.send = async function(ws, action, msg = {}){ + + const seqid = ++fplay.seqid; + msg.action = action; + msg.seqid = seqid; + + return new Promise(function(resolve){ + + const timer = setTimeout(function(){ + fplay.pending.delete(seqid); + resolve(false); + }, fplay.COMMAND_TIMEOUT); + + fplay.pending.set(seqid, { + resolve: function(data){ clearTimeout(timer); resolve(data); }, + reject: function(){ clearTimeout(timer); resolve(false); } + }); + + ws.send(JSON.stringify(msg)); + }); +} + +fplay.wait_random = async function(min, max){ + + return new Promise(function(resolve){ + + setTimeout(function(){ + + resolve(true); + }, min + Math.round(Math.random() * (max - min))) + }); +} + +fplay.parse_cookies = function(headers){ + + var cookies = {}; + + headers.forEach(function(header){ + + var cookie = header.split(":") + var header_name = cookie.shift(); + + if(header_name.toLowerCase() != "cookie"){ return; } // continue; + + cookie = cookie.join(":").split(";"); + + cookie.forEach(function(c){ + + c = c.split("="); + var key = c.shift().trim(); + var value = c.join("="); + + cookies[key] = value; + }); + }); + + return cookies; +} + +// +// Misc protocol functions +// +fplay.get_ua = async function(ws){ + + var ua = await fplay.send(ws, "get_ua"); + + if(typeof ua.ua == "string"){ + + return ua.ua; + } + + return false; +} + + + +// +// Tab functions +// +fplay.get_tab_list = async function(ws){ + + var tabs = await fplay.send(ws, "get_tabs"); + + if(typeof tabs.tabs == "object"){ + + return tabs.tabs; + } + + return false; +} + +fplay.tab_open = async function(ws, url, await_dom_ready = false, container = null){ + + var data = { + url: url + }; + + if(container !== null){ + + if( + typeof container == "object" && + typeof container.id != "undefined" + ){ + + data.container = container.id; + } + + if(typeof container == "string"){ + + data.container = container; + } + } + + var newtab = await fplay.send(ws, "tab_open", data); + + if(typeof newtab.data == "object"){ + + if(await_dom_ready){ + + var data = await fplay.wait_for_dom_ready(newtab.data.id); + return data; + } + + return newtab.data; + } + + return false; +} + +// @ tab_ids: number, array or tab object +fplay.tab_close = async function(ws, tab_ids){ + + if( + typeof tab_ids == "object" && + typeof tab_ids.id == "number" + ){ + + tab_ids = tab_ids.id; + } + + var closed_tab_count = await fplay.send(ws, "tab_close", {"tabid": tab_ids}); + + if(typeof closed_tab_count.closed_tab_count == "number"){ + + return closed_tab_count.closed_tab_count; + } + + return false; +} + +fplay.close_all_tabs = async function(ws){ + + const tabs = await fplay.get_tab_list(ws); + const newtab = await fplay.tab_open(ws, "about:blank"); + + // clean up useless tabs + var tabs_to_close = []; + + for(var i=0; i