Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save adamziel/a93297e21f37612751a2904c193d44fa to your computer and use it in GitHub Desktop.

Select an option

Save adamziel/a93297e21f37612751a2904c193d44fa to your computer and use it in GitHub Desktop.
Requesting WordPress Playground from the internet

A simple demo of how a WordPress Playground instance running in the web browser could be reached from the outside world.

The data flow:

  1. The browser receives a unique session ID
  2. The browser connects via a long-lived connection to gateway.php (SSE)
  3. An external caller sends a HTTP request to the gateway (with session ID).
  4. Gateway stalls, stores the request details on the disk
  5. The browser receives request details via the long-lived connection
  6. Browser processes the request and POSTs the response back to the gateway
  7. The gateway feeds the response back to the original caller
version: "3.9"
services:
php:
image: php:8.3-apache
container_name: inverse-proxy
volumes:
- ./:/var/www/html
ports:
- "4280:80"
environment:
APACHE_RUN_USER: www-data
APACHE_RUN_GROUP: www-data
<?php
/**
* gateway.php
*
* Purpose
* -------
* Acts as an HTTP reverse proxy that delivers any request reaching
* http://localhost:4280/gateway.php?session=<TOKEN>
* straight into the browser that owns <TOKEN>.
*
*/
define('MSG_DIR', __DIR__ . '/msg_queue');
define('POLL_US', 10000); // 10 ms
define('PING_SEC', 10);
define('WAIT_SEC', 30);
if (!is_dir(MSG_DIR)) mkdir(MSG_DIR, 0775, true);
@apache_setenv('no-gzip', '1');
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', '0');
header('X-Accel-Buffering: no');
function sid(): string {
$raw = $_GET['session'] ?? $_POST['session'] ?? '';
if (!$raw) { http_response_code(400); exit('no session'); }
return preg_replace('/[^A-Za-z0-9_-]/', '', $raw);
}
function path(string $sid, string $rid, string $ext): string {
$dir = MSG_DIR."/{$sid}";
if (!is_dir($dir)) mkdir($dir, 0775, true);
return "{$dir}/{$rid}.{$ext}";
}
$sid = sid();
/* ----- server→browser stream (SSE) ----- */
if (isset($_GET['stream'])) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
@ob_end_flush(); flush();
$log = path($sid, 'events', 'log');
if (!file_exists($log)) touch($log);
$pos = 0; $nextPing = time() + PING_SEC;
ignore_user_abort(true);
while (true) {
clearstatcache(false, $log);
$size = filesize($log);
if ($size > $pos) {
$h = fopen($log, 'r'); fseek($h, $pos);
while (($l = fgets($h)) !== false) {
echo "data: $l\n\n"; flush();
}
fclose($h);
$pos = $size;
}
if (time() >= $nextPing) {
echo ": ping\n\n"; flush();
$nextPing = time() + PING_SEC;
}
if (connection_aborted()) break;
usleep(POLL_US);
}
exit;
}
/* ----- browser → gateway (POST response) ----- */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['respond'])) {
$in = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$rid = $in['req_id'] ?? '';
$resp = $in['response']?? null;
if (!$rid || !$resp) { http_response_code(400); exit('bad'); }
file_put_contents(path($sid, $rid, 'res'), json_encode($resp), LOCK_EX);
exit('stored');
}
/* ----- external caller → gateway (new request) ----- */
$rid = bin2hex(random_bytes(8));
$reqData = [
'req_id' => $rid,
'method' => $_SERVER['REQUEST_METHOD'],
'path' => $_SERVER['REQUEST_URI'],
'headers' => getallheaders(),
'body' => file_get_contents('php://input')
];
file_put_contents(path($sid, $rid, 'req'), json_encode($reqData), LOCK_EX);
file_put_contents(path($sid, 'events', 'log'),
json_encode($reqData)."\n",
FILE_APPEND | LOCK_EX);
/* wait synchronously for browser reply */
$resFile = path($sid, $rid, 'res');
$start = microtime(true);
while (!file_exists($resFile) &&
microtime(true) - $start < WAIT_SEC) {
usleep(POLL_US);
}
if (!file_exists($resFile)) { http_response_code(504); exit('timeout'); }
/* ----- gateway → external caller (response) ----- */
$resp = json_decode(file_get_contents($resFile), true) ?? [];
$resp = json_decode($resp, true);
http_response_code($resp['status'] ?? 200);
foreach (($resp['headers'] ?? []) as $k => $v) header("$k: $v");
echo $resp['body'] ?? '';
unlink($resFile);
unlink(path($sid, $rid, 'req'));
<!--
index.html
-----------
Single-page demo client that
* generates a 16-char random session token with makeid()
* opens an EventSource to /gateway.php?session=<token>&stream=1
* logs every incoming request and echoes a plain-text response
* sends its response back via multipart/form-data POST
* once per second fires a sample POST to the gateway to prove the round-trip
-->
<!doctype html><meta charset="utf-8">
<pre id="log"></pre>
<script>
function makeid(len){
let r='', c='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for(let i=0;i<len;i++) r+=c.charAt(Math.floor(Math.random()*c.length));
return r;
}
const log = m => { document.getElementById('log').textContent += m + '\n'; };
const session = makeid(16); // unique token for this tab
const gate = `http://localhost:4280/gateway.php?session=${session}`;
log('session ' + session);
const es = new EventSource(gate + '&stream=1'); // real-time stream from server
es.onmessage = async ev => {
const req = JSON.parse(ev.data);
log('← ' + req.method + ' ' + req.path + ' ' + req.body);
// Minimal echo response – adjust as your app requires
const reply = {
status: 200,
headers: { 'Content-Type': 'text/plain' },
body: 'echo: ' + JSON.stringify(req.body)
};
// Multipart POST back to gateway carrying the response
const fd = new FormData();
fd.append('session', session);
fd.append('respond', 1);
fd.append('req_id', req.req_id);
fd.append('response', JSON.stringify(reply));
await fetch('http://localhost:4280/gateway.php', { method: 'POST', body: fd });
log('→ responded ' + req.req_id);
};
// Self-test: every second send a POST to the gateway so we see the loop close
setTimeout(async () => {
while (true) {
const res = await fetch(`http://localhost:4280/gateway.php?session=${session}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hello: 'world' })
});
const data = await res.text();
console.log({data});
await new Promise(r => setTimeout(r, 1000));
}
}, 1000);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment