-
Notifications
You must be signed in to change notification settings - Fork 8
/
ScriptRunner.js
161 lines (137 loc) · 5.23 KB
/
ScriptRunner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
const { spawn } = require("child_process")
const fsp = require("fs").promises
const path = require("path")
// Map of file extensions to their interpreters
const interpreterMap = {
mjs: "node",
php: "php",
py: "python3",
sh: "bash",
rb: "ruby",
pl: "perl"
}
const TIMEOUT = 20000
class ScriptRunner {
constructor(hub) {
this.hub = hub
}
init() {
const { hub } = this
const { app, rootFolder, folderCache } = hub
const scriptExtensions = Object.keys(interpreterMap).join("|")
const scriptPattern = new RegExp(`.*\.(${scriptExtensions})$`)
// Handle both GET and POST requests
app.all(scriptPattern, async (req, res) => {
const folderName = hub.getFolderName(req)
// Check if folder exists
if (!folderCache[folderName]) return res.status(404).send("Folder not found")
const folderPath = path.join(rootFolder, folderName)
const filePath = path.join(folderPath, path.basename(req.path))
// Ensure the file path is within the folder (prevent directory traversal)
if (!filePath.startsWith(folderPath)) return res.status(403).send("Access denied")
const ext = path.extname(filePath).slice(1)
const interpreter = interpreterMap[ext]
try {
// Check if file exists
const fileExists = await fsp
.access(filePath)
.then(() => true)
.catch(() => false)
if (!fileExists) return res.status(404).send("Script not found")
// Prepare environment variables with request data
const env = {
...process.env,
FOLDER_NAME: folderName,
REQUEST_METHOD: req.method,
REQUEST_BODY: JSON.stringify(req.body),
QUERY_STRING: new URLSearchParams(req.query).toString(),
CONTENT_TYPE: req.get("content-type") || "",
CONTENT_LENGTH: req.get("content-length") || "0",
HTTP_HOST: req.get("host") || "",
HTTP_USER_AGENT: req.get("user-agent") || "",
REMOTE_ADDR: req.ip || req.connection.remoteAddress,
SCRIPT_FILENAME: filePath,
SCRIPT_NAME: req.path,
DOCUMENT_ROOT: folderPath,
// Add more CGI-like environment variables
PATH_INFO: filePath,
SERVER_NAME: req.hostname,
SERVER_PORT: req.protocol === "https" ? "443" : "80",
SERVER_PROTOCOL: "HTTP/" + req.httpVersion,
SERVER_SOFTWARE: "ScrollHub"
}
// Add all HTTP headers as environment variables
Object.entries(req.headers).forEach(([key, value]) => {
env["HTTP_" + key.toUpperCase().replace("-", "_")] = value
})
// Spawn the interpreter process
const spawnedProcess = spawn(interpreter, [filePath], {
env,
cwd: folderPath, // Set working directory to the folder
stdio: ["pipe", "pipe", "pipe"]
})
// Send POST data to script's stdin if present
if (req.method === "POST") {
const postData = req.body
if (typeof postData === "string") {
spawnedProcess.stdin.write(postData)
} else if (Buffer.isBuffer(postData)) {
spawnedProcess.stdin.write(postData)
} else {
spawnedProcess.stdin.write(JSON.stringify(postData))
}
spawnedProcess.stdin.end()
}
let output = ""
let errorOutput = ""
spawnedProcess.stdout.on("data", data => {
output += data.toString()
})
spawnedProcess.stderr.on("data", data => {
errorOutput += data.toString()
})
// Set timeout for script execution (e.g., 30 seconds)
const timeout = setTimeout(() => {
spawnedProcess.kill()
res.status(504).send("Script execution timed out")
}, TIMEOUT)
spawnedProcess.on("close", code => {
clearTimeout(timeout)
if (code === 0) {
// Parse output for headers and body
const parts = output.split("\r\n\r\n")
if (parts.length > 1) {
// Script provided headers
const headers = parts[0].split("\r\n")
const body = parts.slice(1).join("\r\n\r\n")
headers.forEach(header => {
const [name, ...value] = header.split(":")
if (name && value) {
res.setHeader(name.trim(), value.join(":").trim())
}
})
res.send(body)
} else {
// No headers provided, send as plain text
res.setHeader("Content-Type", "text/plain")
res.send(output)
}
} else {
console.error(`Script execution error in ${filePath} (${code}):`, errorOutput)
res.status(500).send(`Script execution failed: ${errorOutput}`)
}
})
// Handle process errors
spawnedProcess.on("error", err => {
clearTimeout(timeout)
console.error(`Failed to start script process in ${folderName}:`, err)
res.status(500).send("Failed to execute script")
})
} catch (error) {
console.error(`Script execution error in ${folderName}:`, error)
res.status(500).send("Internal server error")
}
})
}
}
module.exports = { ScriptRunner }