Compare commits

...

16 Commits

Author SHA1 Message Date
41e1c33e0e add package.json 2026-08-04 23:10:24 -07:00
aa2bf08255 read values from env 2026-08-04 23:07:10 -07:00
54af35299d fix errors not being reported using http proxies 2026-08-03 15:59:59 -04:00
1a327b4aaf fix network errors not being reported anymore 2026-08-03 15:37:53 -04:00
4f2b9b40f3 fix http proxies not being supported 2026-08-03 14:46:20 -04:00
9f4fb5a101 fix password not updating on server 2026-08-01 20:53:51 -04:00
afa4d78a0b stability fixes 2026-07-29 19:10:27 -04:00
d5d8c89280 fix open_tab() not returning false on failure 2026-07-28 02:25:18 -04:00
6b9ac26f82 request whitelist fixes + error handling unfuckery 2026-07-27 03:57:31 -04:00
567b1ade4a now uses npm package 2026-07-26 16:54:24 -04:00
4f21030841 bump version to 1.2 2026-07-26 15:16:47 -04:00
7356cd8afd fix navigation errors being misreported lol professional grade software 2026-07-26 15:12:47 -04:00
3aff7caf2e added extension link 2026-07-01 18:34:06 -04:00
7a645494f2 wording 2026-06-08 02:19:40 -04:00
e363d79bec readme tweak 2026-06-07 01:28:01 -04:00
91ab1d9e7e hello world cleanup 2026-06-06 19:53:27 -04:00
8 changed files with 371 additions and 117 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,3 @@
node_modules/ node_modules/
web-ext-artifacts/ web-ext-artifacts/
*.swp *.swp
package.json
package-lock.json

View File

@@ -17,17 +17,25 @@ I'm so fucking tired of retards recommending libraries that are so easily detect
## Installation ## 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. 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.
Just go get the extension on the [Firefox store](https://addons.mozilla.org/en-US/firefox/addon/4play/), or run it for development like so:
```sh
npm install --global web-ext
cd ext
web-ext run # this will launch a new firefox instance
```
Then, start the server: Then, start the server:
```bash ```bash
cd server mkdir server && cd server
npm install http ws npm install @lawlers/4play
node hello-world.js node hello-world.js
``` ```
## Example server script ## Example server script
```js ```js
const fplay = require("./fplay.js"); const fplay = require("@lawlers/4play");
var port = 3030; var port = 3030;
var timeout = 30000; var timeout = 30000;
@@ -103,26 +111,9 @@ Gets the browser's user agent string.
**Returns:** `string` on success, `false` on failure. **Returns:** `string` on success, `false` on failure.
## `fplay.web_response_whitelist(sources)` ### `fplay.web_response_whitelist(sources)`
Outputs the response body for specific data sources. `sources` can is an array that can contain: Outputs the raw body of incoming pages for `fplay.event.on("web_response", ...)`. `sources` defines what data sources will be returned: `main_frame`, `sub_frame`, `stylesheet`, `script`, `image`, `object`, `xmlhttprequest`, `ping`, `font`, `media`, `websocket`, `csp_report`, `imageset`, `web_manifest`, `speculative` and `other`
- `main_frame`
- `sub_frame`
- `stylesheet`
- `script`
- `image`
- `object`
- `xmlhttprequest`
- `ping`
- `font`
- `media`
- `websocket`
- `csp_report`
- `imageset`
- `web_manifest`
- `speculative`
- `other`
Default is `main_frame`, `xmlhttprequest`. Default is `main_frame`, `xmlhttprequest`.
@@ -305,7 +296,7 @@ An `EventEmitter` instance that emits the following events:
| `browser_connect` | `ws` | Browser connected | | `browser_connect` | `ws` | Browser connected |
| `browser_disconnect` | `{}` | Browser disconnected | | `browser_disconnect` | `{}` | Browser disconnected |
| `dom_ready` | `{ id, index, status, active, title, url, container }` | A tab finished loading | | `dom_ready` | `{ id, index, status, active, title, url, container }` | A tab finished loading |
| `dom_load_fail` | `{ id }` | A tab failed to load | | `dom_load_fail` | `{ id, url, error }` | A tab failed to load (Network error, `<=400` code) |
| `web_request` | `{ id, url, status, origin, type, method, container, headers }` | A request was sent | | `web_request` | `{ id, url, status, origin, type, method, container, headers }` | A request was sent |
| `web_response` | `{ id, url, status, origin, type, method, container, body }` | A response was received | | `web_response` | `{ id, url, status, origin, type, method, container, body }` | A response was received |

231
ext/bg.js
View File

@@ -15,6 +15,8 @@ var proxy_map = {};
var web_response_whitelist = ["main_frame", "xmlhttprequest"]; var web_response_whitelist = ["main_frame", "xmlhttprequest"];
var webreq_completed = new Map();
const log_debug = true; const log_debug = true;
browser.browserAction.setBadgeBackgroundColor({ browser.browserAction.setBadgeBackgroundColor({
@@ -239,11 +241,25 @@ function attach_ws_events(ws){
break; break;
case "tab_open": case "tab_open":
var tab =
// sometimes opening tabs like about:blank with a
// container will fail.
var tab = null;
try{
tab =
await browser.tabs.create({ await browser.tabs.create({
url: msg.url, url: msg.url,
...(typeof msg.container == "string" && { cookieStoreId: msg.container }) ...(typeof msg.container == "string" && { cookieStoreId: msg.container })
}); });
}catch(err){
// send error: we attempted to open tab
send(ws, seqid, {
data: false
});
break;
}
// immediately return even if its not loaded yet // immediately return even if its not loaded yet
send(ws, seqid, { send(ws, seqid, {
@@ -322,7 +338,7 @@ function attach_ws_events(ws){
send(ws, seqid, {"status": true, "result": result}); send(ws, seqid, {"status": true, "result": result});
}catch(err){ }catch(err){
send(ws, seqid, {"status": err.name + ": " + err.message}); send(ws, seqid, {"status": err.name + ": " + err.message, "result": null});
} }
break; break;
@@ -443,6 +459,22 @@ function attach_ws_events(ws){
if(status){ if(status){
if(
msg.proxy.type == "http" ||
msg.proxy.type == "https"
){
if(typeof msg.proxy.proxyDNS == "boolean"){
delete msg.proxy.proxyDNS;
}
}
if(typeof msg.proxy.port == "string"){
msg.proxy.port = parseInt(msg.proxy.port);
}
proxy_map[msg.id] = msg.proxy; proxy_map[msg.id] = msg.proxy;
send(ws, seqid, {status: true}); send(ws, seqid, {status: true});
return; return;
@@ -473,6 +505,7 @@ function attach_ws_events(ws){
ws.addEventListener("close", async function(e){ ws.addEventListener("close", async function(e){
console.log("ws: offline"); console.log("ws: offline");
webreq_completed = new Map();
await set_status(STATUS_OFFLINE); await set_status(STATUS_OFFLINE);
}); });
} }
@@ -530,21 +563,53 @@ async function buff2b64(uint8Array) {
}); });
} }
// store incoming data
browser.webRequest.onBeforeRequest.addListener( browser.webRequest.onBeforeRequest.addListener(
async function(details){ function(details){
if(global_ws === null){ return; }
if(!web_response_whitelist.includes(details.type)){
return;
}
if( if(
web_response_whitelist.length === 0 || web_response_whitelist.length === 0 ||
global_ws === null global_ws === null
){ return; } ){
return;
}
var resolve_func = null;
webreq_completed.set(
details.requestId,
new Promise(
function(resolve){
resolve_func = resolve;
}
)
const filter = browser.webRequest.filterResponseData(
details.requestId
); );
var filter = null;
try{
filter = browser.webRequest.filterResponseData(details.requestId);
}catch(err){
resolve_func({success: false});
webreq_completed.delete(details.requestId);
return;
}
var chunks = []; var chunks = [];
filter.ondata = async function(event){ filter.ondata = function(event){
chunks.push(event.data); chunks.push(event.data);
@@ -552,7 +617,7 @@ browser.webRequest.onBeforeRequest.addListener(
filter.write(event.data); filter.write(event.data);
}; };
filter.onstop = async function(){ filter.onstop = function(){
// we got the full response data // we got the full response data
var len = 0; var len = 0;
@@ -570,7 +635,41 @@ browser.webRequest.onBeforeRequest.addListener(
offset += c.byteLength; offset += c.byteLength;
} }
var b64 = await buff2b64(merged); resolve_func({success: true, data: merged});
filter.close();
};
filter.onerror = function(){
resolve_func({success: false});
webreq_completed.delete(details.requestId);
}
},
{ urls: ["<all_urls>"] },
["blocking"]
);
// detect page complete and send payloads
browser.webRequest.onCompleted.addListener(
async function(details){
if(global_ws === null){ return; }
var pending_req = null;
pending_req = await webreq_completed.get(details.requestId);
if(
// not in capture group
!pending_req ||
// some fuckass error occured
pending_req.success === false
){
return;
}
var b64 = await buff2b64(pending_req.data);
send_event( send_event(
global_ws, global_ws,
@@ -584,17 +683,44 @@ browser.webRequest.onBeforeRequest.addListener(
type: details.type, type: details.type,
method: details.method, method: details.method,
container: details.cookieStoreId, container: details.cookieStoreId,
url: details.url,
body: b64 body: b64
} }
} }
); );
filter.close(); // clean up
}; webreq_completed.delete(details.requestId);
}, },
{ urls: ["<all_urls>"], types: web_response_whitelist }, { urls: ["<all_urls>"] }
["blocking"] );
// shit out an error when the request fails
browser.webRequest.onErrorOccurred.addListener(
async function(page){
if(global_ws === null){ return; }
if(!web_response_whitelist.includes(page.type)){
webreq_completed.delete(page.requestId);
return;
}
send_event(
global_ws,
{
"action": "dom_load_fail",
"data": {
id: page.tabId,
url: page.url,
error: page.error
}
}
);
webreq_completed.delete(page.requestId);
},
{ urls: ["<all_urls>"] }
); );
browser.proxy.onRequest.addListener(function(request){ browser.proxy.onRequest.addListener(function(request){
@@ -614,9 +740,47 @@ browser.proxy.onRequest.addListener(function(request){
urls: ["<all_urls>"] urls: ["<all_urls>"]
}); });
// Automatically handle and detect all Proxy Authentication challenges
browser.webRequest.onAuthRequired.addListener(
function(details){
// Only intercept proxy auth requests, ignore normal website auth
if(!details.isProxy){
return {};
}
const proxy_config = proxy_map[details.cookieStoreId];
// Check if the assigned proxy configuration for this container includes credentials
if(
proxy_config &&
(
proxy_config.type == "http" ||
proxy_config.type == "https"
) &&
proxy_config.username &&
proxy_config.password
){
return {
authCredentials: {
username: proxy_config.username,
password: proxy_config.password
}
};
}
// If the proxy requires auth and we have no credentials, silently cancel
// to prevent hanging the browser with auth popup dialogs.
return { cancel: true };
},
{ urls: ["<all_urls>"] },
["blocking"]
);
browser.tabs.onUpdated.addListener(function(tabid, event, tab){ browser.tabs.onUpdated.addListener(function(tabid, event, tab){
if(connected === false){ return; } if(global_ws === null){ return; }
if(event.status === "complete"){ if(event.status === "complete"){
@@ -638,20 +802,49 @@ browser.tabs.onUpdated.addListener(function(tabid, event, tab){
} }
}); });
browser.webNavigation.onErrorOccurred.addListener(function(page){ browser.webRequest.onHeadersReceived.addListener(
function(page){
if(connected === false){ return; } if(global_ws === null){ return; }
if(!web_response_whitelist.includes(page.type)){
return
};
// send error on 4xx code
if(page.statusCode >= 400){
const proxy_config = proxy_map[page.cookieStoreId];
if(
proxy_config &&
(
proxy_config.type == "http" ||
proxy_config.type == "https"
) &&
page.statusCode === 407
){
// ignore http proxy auth error
return;
}
send_event( send_event(
global_ws, global_ws,
{ {
"action": "dom_load_fail", "action": "dom_load_fail",
"data": { "data": {
id: page.tabId id: page.tabId,
url: page.url,
error: page.statusLine.trim()
} }
} }
); );
}); }
},
{ urls: ["<all_urls>"] }
);
(async function(){ (async function(){
await ws_init(); await ws_init();

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "4play", "name": "4play",
"version": "1.0", "version": "1.8",
"description": "4play & dominate", "description": "4play & dominate",
"icons": { "icons": {
"32": "icon.png" "32": "icon.png"

View File

@@ -2,7 +2,7 @@ const fplay = require("./lib/fplay.js");
var port = 3030; var port = 3030;
var timeout = 30000; var timeout = 30000;
var password = "cnc"; var password = process.env.FOURPLAY_PASSWORD || "cnc";
fplay.event.on("server_ready", function(){ fplay.event.on("server_ready", function(){
@@ -52,12 +52,12 @@ fplay.event.on("browser_connect", async function(ws){
fplay.event.on("web_request", async function(request){ fplay.event.on("web_request", async function(request){
console.log(request); //console.log(request);
}); });
fplay.event.on("web_response", async function(request){ fplay.event.on("web_response", async function(response){
console.log(response);
}); });
fplay.init(port, password, timeout); fplay.init(port, password, timeout);

View File

@@ -1,11 +1,13 @@
const http = require("http"); const http = require("http");
const { WebSocketServer } = require("ws"); const { WebSocketServer } = require("ws");
const {FOURPLAY_PORT = 3000, FOURPLAY_COMMAND_TIMEOUT = 5000, FOURPLAY_PASSWORD = "cnc"} = process.env;
var fplay = {}; var fplay = {};
fplay.PORT = 3030; fplay.PORT = FOURPLAY_PORT;
fplay.COMMAND_TIMEOUT = 5000; fplay.COMMAND_TIMEOUT = FOURPLAY_COMMAND_TIMEOUT;
fplay.PASSWORD = "cnc"; fplay.PASSWORD = FOURPLAY_PASSWORD;
const { EventEmitter } = require("events"); const { EventEmitter } = require("events");
fplay.event = new EventEmitter(); fplay.event = new EventEmitter();
@@ -25,8 +27,6 @@ fplay.server = http.createServer(function(req, res){
} }
}); });
fplay.wss = new WebSocketServer({ server: fplay.server, path: "/" + fplay.PASSWORD });
fplay.seqid = 0; fplay.seqid = 0;
fplay.pending = new Map(); // promise map fplay.pending = new Map(); // promise map
fplay.pending_dom_ready = new Map(); fplay.pending_dom_ready = new Map();
@@ -509,7 +509,17 @@ fplay.event.on("web_request", function(page){
fplay.browser_connected = false; fplay.browser_connected = false;
fplay.wss.on("connection", async function(ws){ fplay.init = function(port = 3030, password = "cnc", command_timeout = 5000){
fplay.PORT = port;
fplay.COMMAND_TIMEOUT = command_timeout;
fplay.PASSWORD = password;
fplay.server.listen(fplay.PORT, function(){
fplay.wss = new WebSocketServer({ server: fplay.server, path: "/" + fplay.PASSWORD });
fplay.wss.on("connection", async function(ws){
fplay.browser_connected = true; fplay.browser_connected = true;
fplay.event.emit("browser_connect", ws); fplay.event.emit("browser_connect", ws);
@@ -544,18 +554,14 @@ fplay.wss.on("connection", async function(ws){
fplay.browser_connected = false; fplay.browser_connected = false;
fplay.event.emit("browser_disconnect", {}); fplay.event.emit("browser_disconnect", {});
}); });
}); });
fplay.init = function(port = 3030, password = "cnc", command_timeout = 5000){
fplay.PORT = port;
fplay.COMMAND_TIMEOUT = command_timeout;
fplay.PASSWORD = password;
fplay.server.listen(fplay.PORT, function(){
fplay.event.emit("server_ready", {}); fplay.event.emit("server_ready", {});
}); });
} }
process.on('SIGINT', function() {
process.exit();
});
module.exports = fplay; module.exports = fplay;

37
server/package-lock.json generated Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "@lawlers/4play",
"version": "1.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@lawlers/4play",
"version": "1.2.5",
"license": "AGPL-3.0-only",
"dependencies": {
"ws": "^8.21.1"
}
},
"node_modules/ws": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

29
server/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "@lawlers/4play",
"version": "1.2.5",
"description": "4play and dominate",
"keywords": [
"4play",
"fplay",
"foreplay",
"browser-automation",
"puppeteer",
"selenium",
"playwright",
"firefox"
],
"repository": {
"type": "git",
"url": "https://git.lolcat.ca/lolcat/4play"
},
"license": "AGPL-3.0-only",
"author": "lolcat",
"type": "commonjs",
"main": "fplay.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"ws": "^8.21.1"
}
}