@php-wasm/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +18 -0
- package/README.md +11 -0
- package/jest.config.ts +13 -0
- package/package.json +21 -25
- package/project.json +78 -0
- package/src/lib/inbound-tcp-to-ws-proxy.ts +89 -0
- package/src/lib/outbound-ws-to-tcp-proxy.ts +231 -0
- package/src/lib/utils.ts +33 -0
- package/src/main.ts +74 -0
- package/tsconfig.json +21 -0
- package/tsconfig.lib.json +10 -0
- package/tsconfig.spec.json +14 -0
- package/vite.config.ts +50 -0
- package/assets/php-e1f9edd4.js +0 -1
- package/main.js +0 -5
- /package/{cli.js → public/cli.js} +0 -0
- /package/{assets/php-9094d88a.ini → src/lib/php.ini} +0 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../../../.eslintrc.json"],
|
|
3
|
+
"ignorePatterns": ["!**/*"],
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
7
|
+
"rules": {}
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"files": ["*.ts", "*.tsx"],
|
|
11
|
+
"rules": {}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"files": ["*.js", "*.jsx"],
|
|
15
|
+
"rules": {}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/README.md
ADDED
package/jest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: 'nx-extensions',
|
|
4
|
+
preset: '../../../jest.preset.js',
|
|
5
|
+
transform: {
|
|
6
|
+
'^.+\\.[tj]s$': [
|
|
7
|
+
'ts-jest',
|
|
8
|
+
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
|
|
9
|
+
],
|
|
10
|
+
},
|
|
11
|
+
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
12
|
+
coverageDirectory: '../../../coverage/packages/nx-extensions',
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"comlink": "4.4.1",
|
|
24
|
-
"ws": "8.13.0",
|
|
25
|
-
"@php-wasm/node": "0.1.0"
|
|
26
|
-
}
|
|
2
|
+
"name": "@php-wasm/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PHP.wasm CLI for node.js",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/WordPress/wordpress-playground"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://developer.wordpress.org/playground",
|
|
10
|
+
"author": "The WordPress contributors",
|
|
11
|
+
"contributors": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Adam Zielinski",
|
|
14
|
+
"email": "adam@adamziel.com",
|
|
15
|
+
"url": "https://github.com/adamziel"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"license": "GPL-2.0-or-later",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "main.js",
|
|
21
|
+
"bin": "cli.js",
|
|
22
|
+
"gitHead": "eb0404cb59d670e4c0eb769021e510412b6232e9"
|
|
27
23
|
}
|
package/project.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "php-wasm-cli",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/php-wasm/cli/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "@wp-playground/nx-extensions:package-json",
|
|
9
|
+
"options": {
|
|
10
|
+
"tsConfig": "packages/php-wasm/cli/tsconfig.lib.json",
|
|
11
|
+
"outputPath": "dist/packages/php-wasm/cli",
|
|
12
|
+
"buildTarget": "php-wasm-cli:build:bundle:production"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"build:bundle": {
|
|
16
|
+
"executor": "@nrwl/vite:build",
|
|
17
|
+
"outputs": ["{options.outputPath}"],
|
|
18
|
+
"options": {
|
|
19
|
+
"main": "dist/packages/php-wasm/cli/main.js",
|
|
20
|
+
"outputPath": "dist/packages/php-wasm/cli"
|
|
21
|
+
},
|
|
22
|
+
"defaultConfiguration": "production",
|
|
23
|
+
"configurations": {
|
|
24
|
+
"development": {
|
|
25
|
+
"minify": false
|
|
26
|
+
},
|
|
27
|
+
"production": {
|
|
28
|
+
"minify": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"start": {
|
|
33
|
+
"executor": "@wp-playground/nx-extensions:built-script",
|
|
34
|
+
"options": {
|
|
35
|
+
"scriptPath": "dist/packages/php-wasm/cli/main.js"
|
|
36
|
+
},
|
|
37
|
+
"dependsOn": ["build", "^build-for-cli-run"]
|
|
38
|
+
},
|
|
39
|
+
"publish": {
|
|
40
|
+
"executor": "nx:run-commands",
|
|
41
|
+
"options": {
|
|
42
|
+
"command": "node tools/scripts/publish.mjs php-wasm-cli {args.ver} {args.tag}"
|
|
43
|
+
},
|
|
44
|
+
"dependsOn": ["build"]
|
|
45
|
+
},
|
|
46
|
+
"lint": {
|
|
47
|
+
"executor": "@nrwl/linter:eslint",
|
|
48
|
+
"outputs": ["{options.outputFile}"],
|
|
49
|
+
"options": {
|
|
50
|
+
"lintFilePatterns": ["packages/php-wasm/cli/**/*.ts"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"test": {
|
|
54
|
+
"executor": "@nrwl/jest:jest",
|
|
55
|
+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
56
|
+
"options": {
|
|
57
|
+
"jestConfig": "packages/php-wasm/cli/jest.config.ts",
|
|
58
|
+
"passWithNoTests": true
|
|
59
|
+
},
|
|
60
|
+
"configurations": {
|
|
61
|
+
"ci": {
|
|
62
|
+
"ci": true,
|
|
63
|
+
"codeCoverage": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"typecheck": {
|
|
68
|
+
"executor": "@nrwl/workspace:run-commands",
|
|
69
|
+
"options": {
|
|
70
|
+
"commands": [
|
|
71
|
+
"yarn tsc -p packages/php-wasm/cli/tsconfig.lib.json --noEmit",
|
|
72
|
+
"yarn tsc -p packages/php-wasm/cli/tsconfig.spec.json --noEmit"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"tags": ["scope:php-wasm-public"]
|
|
78
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createServer } from 'net';
|
|
2
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
+
import { debugLog } from './utils.js';
|
|
4
|
+
function log(...args: any[]) {
|
|
5
|
+
debugLog('[TCP Server]', ...args);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function addTCPServerToWebSocketServerClass(
|
|
9
|
+
wsListenPort: number,
|
|
10
|
+
WSServer: typeof WebSocketServer
|
|
11
|
+
): any {
|
|
12
|
+
return class PHPWasmWebSocketServer extends WSServer {
|
|
13
|
+
constructor(options: any, callback: any) {
|
|
14
|
+
const requestedPort = options.port;
|
|
15
|
+
options.port = wsListenPort;
|
|
16
|
+
listenTCPToWSProxy({
|
|
17
|
+
tcpListenPort: requestedPort,
|
|
18
|
+
wsConnectPort: wsListenPort,
|
|
19
|
+
});
|
|
20
|
+
super(options, callback);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface InboundTcpToWsProxyOptions {
|
|
26
|
+
tcpListenPort: number;
|
|
27
|
+
wsConnectHost?: string;
|
|
28
|
+
wsConnectPort: number;
|
|
29
|
+
}
|
|
30
|
+
export function listenTCPToWSProxy(options: InboundTcpToWsProxyOptions) {
|
|
31
|
+
options = {
|
|
32
|
+
wsConnectHost: '127.0.0.1',
|
|
33
|
+
...options,
|
|
34
|
+
};
|
|
35
|
+
const { tcpListenPort, wsConnectHost, wsConnectPort } = options;
|
|
36
|
+
const server = createServer();
|
|
37
|
+
server.on('connection', function handleConnection(tcpSource) {
|
|
38
|
+
const inBuffer: Buffer[] = [];
|
|
39
|
+
|
|
40
|
+
const wsTarget = new WebSocket(
|
|
41
|
+
`ws://${wsConnectHost}:${wsConnectPort}/`
|
|
42
|
+
);
|
|
43
|
+
wsTarget.binaryType = 'arraybuffer';
|
|
44
|
+
function wsSend(data: Buffer) {
|
|
45
|
+
wsTarget.send(new Uint8Array(data));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
wsTarget.addEventListener('open', function () {
|
|
49
|
+
log('Outbound WebSocket connection established');
|
|
50
|
+
while (inBuffer.length > 0) {
|
|
51
|
+
wsSend(inBuffer.shift()!);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
wsTarget.addEventListener('message', (e) => {
|
|
55
|
+
log(
|
|
56
|
+
'WS->TCP message:',
|
|
57
|
+
new TextDecoder().decode(e.data as ArrayBuffer)
|
|
58
|
+
);
|
|
59
|
+
tcpSource.write(Buffer.from(e.data as ArrayBuffer));
|
|
60
|
+
});
|
|
61
|
+
wsTarget.addEventListener('close', () => {
|
|
62
|
+
log('WebSocket connection closed');
|
|
63
|
+
tcpSource.end();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
tcpSource.on('data', function (data) {
|
|
67
|
+
log('TCP->WS message:', data);
|
|
68
|
+
if (wsTarget.readyState === WebSocket.OPEN) {
|
|
69
|
+
while (inBuffer.length > 0) {
|
|
70
|
+
wsSend(inBuffer.shift()!);
|
|
71
|
+
}
|
|
72
|
+
wsSend(data);
|
|
73
|
+
} else {
|
|
74
|
+
inBuffer.push(data);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
tcpSource.once('close', function () {
|
|
78
|
+
log('TCP connection closed');
|
|
79
|
+
wsTarget.close();
|
|
80
|
+
});
|
|
81
|
+
tcpSource.on('error', function () {
|
|
82
|
+
log('TCP connection error');
|
|
83
|
+
wsTarget.close();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
server.listen(tcpListenPort, function () {
|
|
87
|
+
log('TCP server listening');
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is a simple TCP proxy server that allows PHP to connect to a remote
|
|
3
|
+
* server via WebSockets. This is necessary because WebAssembly has no access
|
|
4
|
+
* to the network.
|
|
5
|
+
*
|
|
6
|
+
* This module was forked from the @maximegris/node-websockify npm package.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import * as dns from 'dns';
|
|
11
|
+
import * as util from 'util';
|
|
12
|
+
import * as net from 'net';
|
|
13
|
+
import * as http from 'http';
|
|
14
|
+
import { WebSocketServer } from 'ws';
|
|
15
|
+
import { debugLog } from './utils.js';
|
|
16
|
+
|
|
17
|
+
function log(...args: any[]) {
|
|
18
|
+
debugLog('[WS Server]', ...args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lookup = util.promisify(dns.lookup);
|
|
22
|
+
|
|
23
|
+
function prependByte(
|
|
24
|
+
chunk: string | ArrayBuffer | ArrayLike<number>,
|
|
25
|
+
byte: number
|
|
26
|
+
) {
|
|
27
|
+
if (typeof chunk === 'string') {
|
|
28
|
+
chunk = String.fromCharCode(byte) + chunk;
|
|
29
|
+
} else if (chunk instanceof ArrayBuffer) {
|
|
30
|
+
const buffer = new Uint8Array(chunk.byteLength + 1);
|
|
31
|
+
buffer[0] = byte;
|
|
32
|
+
buffer.set(new Uint8Array(chunk), 1);
|
|
33
|
+
chunk = buffer.buffer;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error('Unsupported chunk type');
|
|
36
|
+
}
|
|
37
|
+
return chunk;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Send a chunk of data to the remote server.
|
|
42
|
+
*/
|
|
43
|
+
export const COMMAND_CHUNK = 0x01;
|
|
44
|
+
/**
|
|
45
|
+
* Set a TCP socket option.
|
|
46
|
+
*/
|
|
47
|
+
export const COMMAND_SET_SOCKETOPT = 0x02;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adds support for TCP socket options to WebSocket class.
|
|
51
|
+
*
|
|
52
|
+
* Socket options are implemented by adopting a specific data transmission
|
|
53
|
+
* protocol between WS client and WS server The first byte
|
|
54
|
+
* of every message is a command type, and the remaining bytes
|
|
55
|
+
* are the actual data.
|
|
56
|
+
*
|
|
57
|
+
* @param WebSocketConstructor
|
|
58
|
+
* @returns Decorated constructor
|
|
59
|
+
*/
|
|
60
|
+
export function addSocketOptionsSupportToWebSocketClass(
|
|
61
|
+
WebSocketConstructor: typeof WebSocket
|
|
62
|
+
) {
|
|
63
|
+
return class PHPWasmWebSocketConstructor extends WebSocketConstructor {
|
|
64
|
+
override CONNECTING = 0;
|
|
65
|
+
override OPEN = 1;
|
|
66
|
+
override CLOSING = 2;
|
|
67
|
+
override CLOSED = 3;
|
|
68
|
+
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
send(chunk: any, callback: any) {
|
|
71
|
+
return this.sendCommand(COMMAND_CHUNK, chunk, callback);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setSocketOpt(
|
|
75
|
+
optionClass: number,
|
|
76
|
+
optionName: number,
|
|
77
|
+
optionValue: number
|
|
78
|
+
) {
|
|
79
|
+
return this.sendCommand(
|
|
80
|
+
COMMAND_SET_SOCKETOPT,
|
|
81
|
+
new Uint8Array([optionClass, optionName, optionValue]).buffer,
|
|
82
|
+
() => undefined
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
sendCommand(
|
|
86
|
+
commandType: number,
|
|
87
|
+
chunk: string | ArrayBuffer | ArrayLike<number>,
|
|
88
|
+
callback: any
|
|
89
|
+
) {
|
|
90
|
+
return (WebSocketConstructor.prototype.send as any).call(
|
|
91
|
+
this,
|
|
92
|
+
prependByte(chunk, commandType),
|
|
93
|
+
callback
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function initOutboundWebsocketProxyServer(
|
|
100
|
+
listenPort: number,
|
|
101
|
+
listenHost = '127.0.0.1'
|
|
102
|
+
): Promise<http.Server> {
|
|
103
|
+
log(`Binding the WebSockets server to ${listenHost}:${listenPort}...`);
|
|
104
|
+
const webServer = http.createServer((request, response) => {
|
|
105
|
+
response.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
106
|
+
response.write(
|
|
107
|
+
'403 Permission Denied\nOnly websockets are allowed here.\n'
|
|
108
|
+
);
|
|
109
|
+
response.end();
|
|
110
|
+
});
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
webServer.listen(listenPort, listenHost, function () {
|
|
113
|
+
const wsServer = new WebSocketServer({ server: webServer });
|
|
114
|
+
wsServer.on('connection', onWsConnect);
|
|
115
|
+
resolve(webServer);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle new WebSocket client
|
|
121
|
+
async function onWsConnect(client: any, request: http.IncomingMessage) {
|
|
122
|
+
const clientAddr = client._socket.remoteAddress;
|
|
123
|
+
const clientLog = function (...args: any[]) {
|
|
124
|
+
log(' ' + clientAddr + ': ', ...args);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
clientLog(
|
|
128
|
+
'WebSocket connection from : ' +
|
|
129
|
+
clientAddr +
|
|
130
|
+
' at URL ' +
|
|
131
|
+
(request ? request.url : client.upgradeReq.url)
|
|
132
|
+
);
|
|
133
|
+
clientLog(
|
|
134
|
+
'Version ' +
|
|
135
|
+
client.protocolVersion +
|
|
136
|
+
', subprotocol: ' +
|
|
137
|
+
client.protocol
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Parse the search params (the host doesn't matter):
|
|
141
|
+
const reqUrl = new URL(`ws://0.0.0.0` + request.url);
|
|
142
|
+
const reqTargetPort = Number(reqUrl.searchParams.get('port'));
|
|
143
|
+
const reqTargetHost = reqUrl.searchParams.get('host');
|
|
144
|
+
if (!reqTargetPort || !reqTargetHost) {
|
|
145
|
+
clientLog('Missing host or port information');
|
|
146
|
+
client.close(3000);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// eslint-disable-next-line prefer-const
|
|
151
|
+
let target: any;
|
|
152
|
+
const recvQueue: Buffer[] = [];
|
|
153
|
+
function flushMessagesQueue() {
|
|
154
|
+
while (recvQueue.length > 0) {
|
|
155
|
+
const msg = recvQueue.pop()! as Buffer;
|
|
156
|
+
const commandType = msg[0];
|
|
157
|
+
clientLog('flushing', { commandType }, msg);
|
|
158
|
+
if (commandType === COMMAND_CHUNK) {
|
|
159
|
+
target.write(msg.slice(1));
|
|
160
|
+
} else if (commandType === COMMAND_SET_SOCKETOPT) {
|
|
161
|
+
const SOL_SOCKET = 1;
|
|
162
|
+
const SO_KEEPALIVE = 9;
|
|
163
|
+
|
|
164
|
+
const IPPROTO_TCP = 6;
|
|
165
|
+
const TCP_NODELAY = 1;
|
|
166
|
+
if (msg[1] === SOL_SOCKET && msg[2] === SO_KEEPALIVE) {
|
|
167
|
+
target.setKeepAlive(msg[3]);
|
|
168
|
+
} else if (msg[1] === IPPROTO_TCP && msg[2] === TCP_NODELAY) {
|
|
169
|
+
target.setNoDelay(msg[3]);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
clientLog('Unknown command type: ' + commandType);
|
|
173
|
+
process.exit();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
client.on('message', function (msg: Buffer) {
|
|
179
|
+
// clientLog('PHP -> network buffer:', msg);
|
|
180
|
+
recvQueue.unshift(msg);
|
|
181
|
+
if (target) {
|
|
182
|
+
flushMessagesQueue();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
client.on('close', function (code: any, reason: any) {
|
|
186
|
+
clientLog(
|
|
187
|
+
'WebSocket client disconnected: ' + code + ' [' + reason + ']'
|
|
188
|
+
);
|
|
189
|
+
target.end();
|
|
190
|
+
} as any);
|
|
191
|
+
client.on('error', function (a: string | Buffer) {
|
|
192
|
+
clientLog('WebSocket client error: ' + a);
|
|
193
|
+
target.end();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Resolve the target host to an IP address if it isn't one already
|
|
197
|
+
let reqTargetIp;
|
|
198
|
+
if (net.isIP(reqTargetHost) === 0) {
|
|
199
|
+
clientLog('resolving ' + reqTargetHost + '... ');
|
|
200
|
+
const resolution = await lookup(reqTargetHost);
|
|
201
|
+
reqTargetIp = resolution.address;
|
|
202
|
+
clientLog('resolved ' + reqTargetHost + ' -> ' + reqTargetIp);
|
|
203
|
+
} else {
|
|
204
|
+
reqTargetIp = reqTargetHost;
|
|
205
|
+
}
|
|
206
|
+
clientLog(
|
|
207
|
+
'Opening a socket connection to ' + reqTargetIp + ':' + reqTargetPort
|
|
208
|
+
);
|
|
209
|
+
target = net.createConnection(reqTargetPort, reqTargetIp, function () {
|
|
210
|
+
clientLog('Connected to target');
|
|
211
|
+
flushMessagesQueue();
|
|
212
|
+
});
|
|
213
|
+
target.on('data', function (data: any) {
|
|
214
|
+
// clientLog('network -> PHP buffer:', [...data.slice(0, 100)].join(', ') + '...');
|
|
215
|
+
try {
|
|
216
|
+
client.send(data);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
clientLog('Client closed, cleaning up target');
|
|
219
|
+
target.end();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
target.on('end', function () {
|
|
223
|
+
clientLog('target disconnected');
|
|
224
|
+
client.close();
|
|
225
|
+
});
|
|
226
|
+
target.on('error', function (e: any) {
|
|
227
|
+
clientLog('target connection error', e);
|
|
228
|
+
target.end();
|
|
229
|
+
client.close(3000);
|
|
230
|
+
});
|
|
231
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
|
|
3
|
+
export function debugLog(...args: any[]) {
|
|
4
|
+
if (process.env['DEV']) {
|
|
5
|
+
console.log(...args);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function findFreePorts(n: number) {
|
|
10
|
+
const serversPromises: Promise<net.Server>[] = [];
|
|
11
|
+
for (let i = 0; i < n; i++) {
|
|
12
|
+
serversPromises.push(listenOnRandomPort());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const servers = await Promise.all(serversPromises);
|
|
16
|
+
const ports: number[] = [];
|
|
17
|
+
for (const server of servers) {
|
|
18
|
+
const address = server.address()! as net.AddressInfo;
|
|
19
|
+
ports.push(address.port);
|
|
20
|
+
server.close();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ports;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function listenOnRandomPort(): Promise<net.Server> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const server = net.createServer();
|
|
29
|
+
server.listen(0, () => {
|
|
30
|
+
resolve(server);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CLI script that runs PHP CLI via the WebAssembly build.
|
|
3
|
+
*/
|
|
4
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
5
|
+
import { rootCertificates } from 'tls';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
initOutboundWebsocketProxyServer,
|
|
9
|
+
addSocketOptionsSupportToWebSocketClass,
|
|
10
|
+
} from './lib/outbound-ws-to-tcp-proxy.js';
|
|
11
|
+
import { addTCPServerToWebSocketServerClass } from './lib/inbound-tcp-to-ws-proxy.js';
|
|
12
|
+
import { findFreePorts } from './lib/utils.js';
|
|
13
|
+
import { PHP, loadPHPRuntime, getPHPLoaderModule } from '@php-wasm/node';
|
|
14
|
+
|
|
15
|
+
let args = process.argv.slice(2);
|
|
16
|
+
if (!args.length) {
|
|
17
|
+
args = ['--help'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Write the ca-bundle.crt file to disk so that PHP can find it.
|
|
21
|
+
const caBundlePath = new URL('ca-bundle.crt', (import.meta || {}).url).pathname;
|
|
22
|
+
if (!existsSync(caBundlePath)) {
|
|
23
|
+
writeFileSync(caBundlePath, rootCertificates.join('\n'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
const defaultPhpIniPath = await import('./lib/php.ini');
|
|
29
|
+
|
|
30
|
+
const phpVersion = process.env['PHP'] || '8.2';
|
|
31
|
+
|
|
32
|
+
const [inboundProxyWsServerPort, outboundProxyWsServerPort] =
|
|
33
|
+
await findFreePorts(2);
|
|
34
|
+
|
|
35
|
+
await initOutboundWebsocketProxyServer(outboundProxyWsServerPort);
|
|
36
|
+
|
|
37
|
+
// This dynamic import only works after the build step
|
|
38
|
+
// when the PHP files are present in the same directory
|
|
39
|
+
// as this script.
|
|
40
|
+
const phpLoaderModule = await getPHPLoaderModule(phpVersion);
|
|
41
|
+
const loaderId = await loadPHPRuntime(phpLoaderModule, {
|
|
42
|
+
ENV: {
|
|
43
|
+
...process.env,
|
|
44
|
+
TERM: 'xterm',
|
|
45
|
+
},
|
|
46
|
+
websocket: {
|
|
47
|
+
url: (_: any, host: string, port: string) => {
|
|
48
|
+
const query = new URLSearchParams({ host, port }).toString();
|
|
49
|
+
return `ws://127.0.0.1:${outboundProxyWsServerPort}/?${query}`;
|
|
50
|
+
},
|
|
51
|
+
subprotocol: 'binary',
|
|
52
|
+
decorator: addSocketOptionsSupportToWebSocketClass,
|
|
53
|
+
serverDecorator: addTCPServerToWebSocketServerClass.bind(
|
|
54
|
+
null,
|
|
55
|
+
inboundProxyWsServerPort
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const hasMinusCOption = args.some((arg) => arg.startsWith('-c'));
|
|
60
|
+
if (!hasMinusCOption) {
|
|
61
|
+
args.unshift('-c', defaultPhpIniPath);
|
|
62
|
+
}
|
|
63
|
+
const php = new PHP(loaderId);
|
|
64
|
+
php.writeFile(caBundlePath, rootCertificates.join('\n'));
|
|
65
|
+
args.unshift('-d', `openssl.cafile=${caBundlePath}`);
|
|
66
|
+
php.cli(['php', ...args]).catch((result) => {
|
|
67
|
+
if (result.name === 'ExitStatus') {
|
|
68
|
+
process.exit(result.status === undefined ? 1 : result.status);
|
|
69
|
+
}
|
|
70
|
+
throw result;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"forceConsistentCasingInFileNames": true,
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noImplicitOverride": true,
|
|
7
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
8
|
+
"noImplicitReturns": true,
|
|
9
|
+
"noFallthroughCasesInSwitch": true
|
|
10
|
+
},
|
|
11
|
+
"files": [],
|
|
12
|
+
"include": [],
|
|
13
|
+
"references": [
|
|
14
|
+
{
|
|
15
|
+
"path": "./tsconfig.lib.json"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "./tsconfig.spec.json"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"types": ["jest", "node"]
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"jest.config.ts",
|
|
10
|
+
"src/**/*.test.ts",
|
|
11
|
+
"src/**/*.spec.ts",
|
|
12
|
+
"src/**/*.d.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
|
4
|
+
|
|
5
|
+
export default defineConfig(() => {
|
|
6
|
+
return {
|
|
7
|
+
assetsInclude: ['**/*.ini'],
|
|
8
|
+
cacheDir: '../../../node_modules/.vite/php-cli',
|
|
9
|
+
|
|
10
|
+
plugins: [
|
|
11
|
+
viteTsConfigPaths({
|
|
12
|
+
root: '../../../',
|
|
13
|
+
}),
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
// Configuration for building your library.
|
|
17
|
+
// See: https://vitejs.dev/guide/build.html#library-mode
|
|
18
|
+
build: {
|
|
19
|
+
assetsInlineLimit: 0,
|
|
20
|
+
target: 'es2020',
|
|
21
|
+
rollupOptions: {
|
|
22
|
+
external: [
|
|
23
|
+
'@php-wasm/node',
|
|
24
|
+
'net',
|
|
25
|
+
'fs',
|
|
26
|
+
'path',
|
|
27
|
+
'http',
|
|
28
|
+
'tls',
|
|
29
|
+
'util',
|
|
30
|
+
'dns',
|
|
31
|
+
'ws',
|
|
32
|
+
],
|
|
33
|
+
input: 'packages/php-wasm/cli/src/main.ts',
|
|
34
|
+
output: {
|
|
35
|
+
format: 'esm',
|
|
36
|
+
entryFileNames: '[name].js',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
test: {
|
|
42
|
+
globals: true,
|
|
43
|
+
cache: {
|
|
44
|
+
dir: '../../../node_modules/.vitest',
|
|
45
|
+
},
|
|
46
|
+
environment: 'jsdom',
|
|
47
|
+
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
});
|
package/assets/php-e1f9edd4.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const p="/assets/php-9094d88a.ini";export{p as default};
|
package/main.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{existsSync as b,writeFileSync as O}from"fs";import{rootCertificates as g}from"tls";import*as E from"dns";import*as L from"util";import*as h from"net";import{createServer as W}from"net";import*as k from"http";import{WebSocketServer as _,WebSocket as S}from"ws";import{getPHPLoaderModule as A,loadPHPRuntime as N,PHP as R}from"@php-wasm/node";const U="modulepreload",x=function(e){return"/"+e},w={},D=function(s,o,t){if(!o||o.length===0)return s();const r=document.getElementsByTagName("link");return Promise.all(o.map(l=>{if(l=x(l),l in w)return;w[l]=!0;const u=l.endsWith(".css"),i=u?'[rel="stylesheet"]':"";if(!!t)for(let c=r.length-1;c>=0;c--){const n=r[c];if(n.href===l&&(!u||n.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${l}"]${i}`))return;const f=document.createElement("link");if(f.rel=u?"stylesheet":U,u||(f.as="script",f.crossOrigin=""),f.href=l,document.head.appendChild(f),u)return new Promise((c,n)=>{f.addEventListener("load",c),f.addEventListener("error",()=>n(new Error(`Unable to preload CSS for ${l}`)))})})).then(()=>s())};function C(...e){process.env.DEV&&console.log(...e)}async function I(e){const s=[];for(let r=0;r<e;r++)s.push($());const o=await Promise.all(s),t=[];for(const r of o){const l=r.address();t.push(l.port),r.close()}return t}function $(){return new Promise(e=>{const s=h.createServer();s.listen(0,()=>{e(s)})})}function v(...e){C("[WS Server]",...e)}const H=L.promisify(E.lookup);function M(e,s){if(typeof e=="string")e=String.fromCharCode(s)+e;else if(e instanceof ArrayBuffer){const o=new Uint8Array(e.byteLength+1);o[0]=s,o.set(new Uint8Array(e),1),e=o.buffer}else throw new Error("Unsupported chunk type");return e}const y=1,T=2;function B(e){return class extends e{constructor(){super(...arguments),this.CONNECTING=0,this.OPEN=1,this.CLOSING=2,this.CLOSED=3}send(o,t){return this.sendCommand(y,o,t)}setSocketOpt(o,t,r){return this.sendCommand(T,new Uint8Array([o,t,r]).buffer,()=>{})}sendCommand(o,t,r){return e.prototype.send.call(this,M(t,o),r)}}}function V(e,s="127.0.0.1"){v(`Binding the WebSockets server to ${s}:${e}...`);const o=k.createServer((t,r)=>{r.writeHead(403,{"Content-Type":"text/plain"}),r.write(`403 Permission Denied
|
|
2
|
-
Only websockets are allowed here.
|
|
3
|
-
`),r.end()});return new Promise(t=>{o.listen(e,s,function(){new _({server:o}).on("connection",q),t(o)})})}async function q(e,s){const o=e._socket.remoteAddress,t=function(...n){v(" "+o+": ",...n)};t("WebSocket connection from : "+o+" at URL "+(s?s.url:e.upgradeReq.url)),t("Version "+e.protocolVersion+", subprotocol: "+e.protocol);const r=new URL("ws://0.0.0.0"+s.url),l=Number(r.searchParams.get("port")),u=r.searchParams.get("host");if(!l||!u){t("Missing host or port information"),e.close(3e3);return}let i;const a=[];function f(){for(;a.length>0;){const n=a.pop(),d=n[0];t("flushing",{commandType:d},n),d===y?i.write(n.slice(1)):d===T?n[1]===1&&n[2]===9?i.setKeepAlive(n[3]):n[1]===6&&n[2]===1&&i.setNoDelay(n[3]):(t("Unknown command type: "+d),process.exit())}}e.on("message",function(n){a.unshift(n),i&&f()}),e.on("close",function(n,d){t("WebSocket client disconnected: "+n+" ["+d+"]"),i.end()}),e.on("error",function(n){t("WebSocket client error: "+n),i.end()});let c;h.isIP(u)===0?(t("resolving "+u+"... "),c=(await H(u)).address,t("resolved "+u+" -> "+c)):c=u,t("Opening a socket connection to "+c+":"+l),i=h.createConnection(l,c,function(){t("Connected to target"),f()}),i.on("data",function(n){try{e.send(n)}catch{t("Client closed, cleaning up target"),i.end()}}),i.on("end",function(){t("target disconnected"),e.close()}),i.on("error",function(n){t("target connection error",n),i.end(),e.close(3e3)})}function m(...e){C("[TCP Server]",...e)}function K(e,s){return class extends s{constructor(t,r){const l=t.port;t.port=e,F({tcpListenPort:l,wsConnectPort:e}),super(t,r)}}}function F(e){e={wsConnectHost:"127.0.0.1",...e};const{tcpListenPort:s,wsConnectHost:o,wsConnectPort:t}=e,r=W();r.on("connection",function(u){const i=[],a=new S(`ws://${o}:${t}/`);a.binaryType="arraybuffer";function f(c){a.send(new Uint8Array(c))}a.addEventListener("open",function(){for(m("Outbound WebSocket connection established");i.length>0;)f(i.shift())}),a.addEventListener("message",c=>{m("WS->TCP message:",new TextDecoder().decode(c.data)),u.write(Buffer.from(c.data))}),a.addEventListener("close",()=>{m("WebSocket connection closed"),u.end()}),u.on("data",function(c){if(m("TCP->WS message:",c),a.readyState===S.OPEN){for(;i.length>0;)f(i.shift());f(c)}else i.push(c)}),u.once("close",function(){m("TCP connection closed"),a.close()}),u.on("error",function(){m("TCP connection error"),a.close()})}),r.listen(s,function(){m("TCP server listening")})}let p=process.argv.slice(2);p.length||(p=["--help"]);const P=new URL("ca-bundle.crt",(import.meta||{}).url).pathname;b(P)||O(P,g.join(`
|
|
4
|
-
`));async function j(){const e=await D(()=>import("./assets/php-e1f9edd4.js"),[]),s=process.env.PHP||"8.2",[o,t]=await I(2);await V(t);const r=await A(s),l=await N(r,{ENV:{...process.env,TERM:"xterm"},websocket:{url:(a,f,c)=>{const n=new URLSearchParams({host:f,port:c}).toString();return`ws://127.0.0.1:${t}/?${n}`},subprotocol:"binary",decorator:B,serverDecorator:K.bind(null,o)}});p.some(a=>a.startsWith("-c"))||p.unshift("-c",e);const i=new R(l);i.writeFile(P,g.join(`
|
|
5
|
-
`)),p.unshift("-d",`openssl.cafile=${P}`),i.cli(["php",...p]).catch(a=>{throw a.name==="ExitStatus"&&process.exit(a.status===void 0?1:a.status),a})}j();
|
|
File without changes
|
|
File without changes
|