http.uc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
  2. // Licensed to the public under the Apache License 2.0.
  3. import {
  4. urlencode as _urlencode,
  5. urldecode as _urldecode,
  6. urlencoded_parser, multipart_parser, header_attribute,
  7. ENCODE_IF_NEEDED, ENCODE_FULL, DECODE_IF_NEEDED, DECODE_PLUS
  8. } from 'lucihttp';
  9. import {
  10. error as fserror,
  11. stdin, stdout, mkstemp
  12. } from 'fs';
  13. // luci.http module scope
  14. export let HTTP_MAX_CONTENT = 1024*100; // 100 kB maximum content size
  15. // Decode a mime encoded http message body with multipart/form-data
  16. // Content-Type. Stores all extracted data associated with its parameter name
  17. // in the params table within the given message object. Multiple parameter
  18. // values are stored as tables, ordinary ones as strings.
  19. // If an optional file callback function is given then it is fed with the
  20. // file contents chunk by chunk and only the extracted file name is stored
  21. // within the params table. The callback function will be called subsequently
  22. // with three arguments:
  23. // o Table containing decoded (name, file) and raw (headers) mime header data
  24. // o String value containing a chunk of the file data
  25. // o Boolean which indicates whether the current chunk is the last one (eof)
  26. export function mimedecode_message_body(src, msg, file_cb) {
  27. let len = 0, maxlen = +msg.env.CONTENT_LENGTH;
  28. let err, header, field, parser;
  29. parser = multipart_parser(msg.env.CONTENT_TYPE, function(what, buffer, length) {
  30. if (what == parser.PART_INIT) {
  31. field = {};
  32. }
  33. else if (what == parser.HEADER_NAME) {
  34. header = lc(buffer);
  35. }
  36. else if (what == parser.HEADER_VALUE && header) {
  37. if (lc(header) == 'content-disposition' &&
  38. header_attribute(buffer, null) == 'form-data') {
  39. field.name = header_attribute(buffer, 'name');
  40. field.file = header_attribute(buffer, 'filename');
  41. field[1] = field.file;
  42. }
  43. field.headers = field.headers || {};
  44. field.headers[header] = buffer;
  45. }
  46. else if (what == parser.PART_BEGIN) {
  47. return !field.file;
  48. }
  49. else if (what == parser.PART_DATA && field.name && length > 0) {
  50. if (field.file) {
  51. if (file_cb) {
  52. file_cb(field, buffer, false);
  53. msg.params[field.name] = msg.params[field.name] || field;
  54. }
  55. else {
  56. if (!field.fd)
  57. field.fd = mkstemp(field.name);
  58. if (field.fd) {
  59. field.fd.write(buffer);
  60. msg.params[field.name] = msg.params[field.name] || field;
  61. }
  62. }
  63. }
  64. else {
  65. field.value = buffer;
  66. }
  67. }
  68. else if (what == parser.PART_END && field.name) {
  69. if (field.file && msg.params[field.name]) {
  70. if (file_cb)
  71. file_cb(field, '', true);
  72. else if (field.fd)
  73. field.fd.seek(0);
  74. }
  75. else {
  76. let val = msg.params[field.name];
  77. if (type(val) == 'array')
  78. push(val, field.value || '');
  79. else if (val != null)
  80. msg.params[field.name] = [ val, field.value || '' ];
  81. else
  82. msg.params[field.name] = field.value || '';
  83. }
  84. field = null;
  85. }
  86. else if (what == parser.ERROR) {
  87. err = buffer;
  88. }
  89. return true;
  90. }, HTTP_MAX_CONTENT);
  91. while (true) {
  92. let chunk = src();
  93. len += length(chunk);
  94. if (maxlen && len > maxlen + 2)
  95. die('Message body size exceeds Content-Length');
  96. if (!parser.parse(chunk))
  97. die(err);
  98. if (chunk == null)
  99. break;
  100. }
  101. };
  102. // Decode an urlencoded http message body with application/x-www-urlencoded
  103. // Content-Type. Stores all extracted data associated with its parameter name
  104. // in the params table within the given message object. Multiple parameter
  105. // values are stored as tables, ordinary ones as strings.
  106. export function urldecode_message_body(src, msg) {
  107. let len = 0, maxlen = +msg.env.CONTENT_LENGTH;
  108. let err, name, value, parser;
  109. parser = urlencoded_parser(function (what, buffer, length) {
  110. if (what == parser.TUPLE) {
  111. name = null;
  112. value = null;
  113. }
  114. else if (what == parser.NAME) {
  115. name = _urldecode(buffer, DECODE_PLUS);
  116. }
  117. else if (what == parser.VALUE && name) {
  118. let val = msg.params[name];
  119. if (type(val) == 'array')
  120. push(val, _urldecode(buffer, DECODE_PLUS) || '');
  121. else if (val != null)
  122. msg.params[name] = [ val, _urldecode(buffer, DECODE_PLUS) || '' ];
  123. else
  124. msg.params[name] = _urldecode(buffer, DECODE_PLUS) || '';
  125. }
  126. else if (what == parser.ERROR) {
  127. err = buffer;
  128. }
  129. return true;
  130. }, HTTP_MAX_CONTENT);
  131. while (true) {
  132. let chunk = src();
  133. len += length(chunk);
  134. if (maxlen && len > maxlen + 2)
  135. die('Message body size exceeds Content-Length');
  136. if (!parser.parse(chunk))
  137. die(err);
  138. if (chunk == null)
  139. break;
  140. }
  141. };
  142. // This function will examine the Content-Type within the given message object
  143. // to select the appropriate content decoder.
  144. // Currently the application/x-www-urlencoded and application/form-data
  145. // mime types are supported. If the encountered content encoding can't be
  146. // handled then the whole message body will be stored unaltered as 'content'
  147. // property within the given message object.
  148. export function parse_message_body(src, msg, filecb) {
  149. if (msg.env.CONTENT_LENGTH || msg.env.REQUEST_METHOD == 'POST') {
  150. let ctype = header_attribute(msg.env.CONTENT_TYPE, null);
  151. // Is it multipart/mime ?
  152. if (ctype == 'multipart/form-data')
  153. return mimedecode_message_body(src, msg, filecb);
  154. // Is it application/x-www-form-urlencoded ?
  155. else if (ctype == 'application/x-www-form-urlencoded')
  156. return urldecode_message_body(src, msg);
  157. // Unhandled encoding
  158. // If a file callback is given then feed it chunk by chunk, else
  159. // store whole buffer in message.content
  160. let sink;
  161. // If we have a file callback then feed it
  162. if (type(filecb) == 'function') {
  163. let meta = {
  164. name: 'raw',
  165. encoding: msg.env.CONTENT_TYPE
  166. };
  167. sink = (chunk) => {
  168. if (chunk != null)
  169. return filecb(meta, chunk, false);
  170. else
  171. return filecb(meta, null, true);
  172. };
  173. }
  174. // ... else append to .content
  175. else {
  176. let chunks = [], len = 0;
  177. sink = (chunk) => {
  178. len += length(chunk);
  179. if (len > HTTP_MAX_CONTENT)
  180. die('POST data exceeds maximum allowed length');
  181. if (chunk != null) {
  182. push(chunks, chunk);
  183. }
  184. else {
  185. msg.content = join('', chunks);
  186. msg.content_length = len;
  187. }
  188. };
  189. }
  190. // Pump data...
  191. while (true) {
  192. let chunk = src();
  193. sink(chunk);
  194. if (chunk == null)
  195. break;
  196. }
  197. return true;
  198. }
  199. return false;
  200. };
  201. export function build_querystring(q) {
  202. let s = [];
  203. for (let k, v in q) {
  204. push(s,
  205. length(s) ? '&' : '?',
  206. _urlencode(k, ENCODE_IF_NEEDED | ENCODE_FULL) || k,
  207. '=',
  208. _urlencode(v, ENCODE_IF_NEEDED | ENCODE_FULL) || v
  209. );
  210. }
  211. return join('', s);
  212. };
  213. export function urlencode(value) {
  214. if (value == null)
  215. return null;
  216. value = '' + value;
  217. return _urlencode(value, ENCODE_IF_NEEDED | ENCODE_FULL) || value;
  218. };
  219. export function urldecode(value, decode_plus) {
  220. if (value == null)
  221. return null;
  222. value = '' + value;
  223. return _urldecode(value, DECODE_IF_NEEDED | (decode_plus ? DECODE_PLUS : 0)) || value;
  224. };
  225. // Extract and split urlencoded data pairs, separated bei either "&" or ";"
  226. // from given url or string. Returns a table with urldecoded values.
  227. // Simple parameters are stored as string values associated with the parameter
  228. // name within the table. Parameters with multiple values are stored as array
  229. // containing the corresponding values.
  230. export function urldecode_params(url, tbl) {
  231. let parser, name, value;
  232. let params = tbl || {};
  233. parser = urlencoded_parser(function(what, buffer, length) {
  234. if (what == parser.TUPLE) {
  235. name = null;
  236. value = null;
  237. }
  238. else if (what == parser.NAME) {
  239. name = _urldecode(buffer);
  240. }
  241. else if (what == parser.VALUE && name) {
  242. params[name] = _urldecode(buffer) || '';
  243. }
  244. return true;
  245. });
  246. if (parser) {
  247. let m = match(('' + (url || '')), /[^?]*$/);
  248. parser.parse(m ? m[0] : '');
  249. parser.parse(null);
  250. }
  251. return params;
  252. };
  253. // Encode each key-value-pair in given table to x-www-urlencoded format,
  254. // separated by '&'. Tables are encoded as parameters with multiple values by
  255. // repeating the parameter name with each value.
  256. export function urlencode_params(tbl) {
  257. let enc = [];
  258. for (let k, v in tbl) {
  259. if (type(v) == 'array') {
  260. for (let v2 in v) {
  261. if (length(enc))
  262. push(enc, '&');
  263. push(enc,
  264. _urlencode(k),
  265. '=',
  266. _urlencode('' + v2));
  267. }
  268. }
  269. else {
  270. if (length(enc))
  271. push(enc, '&');
  272. push(enc,
  273. _urlencode(k),
  274. '=',
  275. _urlencode('' + v));
  276. }
  277. }
  278. return join(enc, '');
  279. };
  280. // Default IO routines suitable for CGI invocation
  281. let avail_len = +getenv('CONTENT_LENGTH');
  282. const default_source = () => {
  283. let rlen = min(avail_len, 4096);
  284. if (rlen == 0) {
  285. stdin.close();
  286. return null;
  287. }
  288. let chunk = stdin.read(rlen);
  289. if (chunk == null)
  290. die(`Input read error: ${fserror()}`);
  291. avail_len -= length(chunk);
  292. return chunk;
  293. };
  294. const default_sink = (...chunks) => {
  295. for (let chunk in chunks)
  296. stdout.write(chunk);
  297. stdout.flush();
  298. };
  299. const Class = {
  300. formvalue: function(name, noparse) {
  301. if (!noparse && !this.parsed_input)
  302. this._parse_input();
  303. if (name != null)
  304. return this.message.params[name];
  305. else
  306. return this.message.params;
  307. },
  308. formvaluetable: function(prefix) {
  309. let vals = {};
  310. prefix = (prefix || '') + '.';
  311. if (!this.parsed_input)
  312. this._parse_input();
  313. for (let k, v in this.message.params)
  314. if (index(k, prefix) == 0)
  315. vals[substr(k, length(prefix))] = '' + v;
  316. return vals;
  317. },
  318. content: function() {
  319. if (!this.parsed_input)
  320. this._parse_input();
  321. return this.message.content;
  322. },
  323. getcookie: function(name) {
  324. return header_attribute(`cookie; ${this.getenv('HTTP_COOKIE') ?? ''}`, name);
  325. },
  326. getenv: function(name) {
  327. if (name != null)
  328. return this.message.env[name];
  329. else
  330. return this.message.env;
  331. },
  332. setfilehandler: function(callback) {
  333. if (type(callback) == 'resource' && type(callback.call) == 'function')
  334. this.filehandler = (...args) => callback.call(...args);
  335. else if (type(callback) == 'function')
  336. this.filehandler = callback;
  337. else
  338. die('Invalid callback argument for setfilehandler()');
  339. if (!this.parsed_input)
  340. return;
  341. // If input has already been parsed then uploads are stored as unlinked
  342. // temporary files pointed to by open file handles in the parameter
  343. // value table. Loop all params, and invoke the file callback for any
  344. // param with an open file handle.
  345. for (let name, value in this.message.params) {
  346. while (value?.fd) {
  347. let data = value.fd.read(1024);
  348. let eof = (length(data) == 0);
  349. this.filehandler(value, data, eof);
  350. if (eof) {
  351. value.fd.close();
  352. value.fd = null;
  353. }
  354. }
  355. }
  356. },
  357. _parse_input: function() {
  358. parse_message_body(
  359. this.input,
  360. this.message,
  361. this.filehandler
  362. );
  363. this.parsed_input = true;
  364. },
  365. close: function() {
  366. this.write_headers();
  367. this.closed = true;
  368. },
  369. header: function(key, value) {
  370. this.headers ??= {};
  371. this.headers[lc(key)] = value;
  372. },
  373. prepare_content: function(mime) {
  374. if (!this.headers?.['content-type']) {
  375. if (mime == 'application/xhtml+xml') {
  376. if (index(this.getenv('HTTP_ACCEPT'), mime) == -1) {
  377. mime = 'text/html; charset=UTF-8';
  378. this.header('Vary', 'Accept');
  379. }
  380. }
  381. this.header('Content-Type', mime);
  382. }
  383. },
  384. status: function(code, message) {
  385. this.status_code = code ?? 200;
  386. this.status_message = message ?? 'OK';
  387. },
  388. write_headers: function() {
  389. if (this.eoh)
  390. return;
  391. if (!this.status_code)
  392. this.status();
  393. if (!this.headers?.['content-type'])
  394. this.header('Content-Type', 'text/html; charset=UTF-8');
  395. if (!this.headers?.['cache-control']) {
  396. this.header('Cache-Control', 'no-cache');
  397. this.header('Expires', '0');
  398. }
  399. if (!this.headers?.['x-frame-options'])
  400. this.header('X-Frame-Options', 'SAMEORIGIN');
  401. if (!this.headers?.['x-xss-protection'])
  402. this.header('X-XSS-Protection', '1; mode=block');
  403. if (!this.headers?.['x-content-type-options'])
  404. this.header('X-Content-Type-Options', 'nosniff');
  405. this.output('Status: ');
  406. this.output(this.status_code);
  407. this.output(' ');
  408. this.output(this.status_message);
  409. this.output('\r\n');
  410. for (let k, v in this.headers) {
  411. this.output(k);
  412. this.output(': ');
  413. this.output(v);
  414. this.output('\r\n');
  415. }
  416. this.output('\r\n');
  417. this.eoh = true;
  418. },
  419. // If the content chunk is nil this function will automatically invoke close.
  420. write: function(content) {
  421. if (content != null && !this.closed) {
  422. this.write_headers();
  423. this.output(content);
  424. return true;
  425. }
  426. else {
  427. this.close();
  428. }
  429. },
  430. redirect: function(url) {
  431. this.status(302, 'Found');
  432. this.header('Location', url ?? '/');
  433. this.close();
  434. },
  435. write_json: function(value) {
  436. this.write(sprintf('%.J', value));
  437. },
  438. urlencode,
  439. urlencode_params,
  440. urldecode,
  441. urldecode_params,
  442. build_querystring
  443. };
  444. export default function(env, sourcein, sinkout) {
  445. return proto({
  446. input: sourcein ?? default_source,
  447. output: sinkout ?? default_sink,
  448. // File handler nil by default to let .content() work
  449. file: null,
  450. // HTTP-Message table
  451. message: {
  452. env,
  453. headers: {},
  454. params: urldecode_params(env?.QUERY_STRING ?? '')
  455. },
  456. parsed_input: false
  457. }, Class);
  458. };