1 // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
2 // Licensed to the public under the Apache License 2.0.
5 urlencode as _urlencode,
6 urldecode as _urldecode,
7 urlencoded_parser, multipart_parser, header_attribute,
8 ENCODE_IF_NEEDED, ENCODE_FULL, DECODE_IF_NEEDED, DECODE_PLUS
13 stdin, stdout, mkstemp
16 // luci.http module scope
17 export let HTTP_MAX_CONTENT = 1024*100; // 100 kB maximum content size
19 // Decode a mime encoded http message body with multipart/form-data
20 // Content-Type. Stores all extracted data associated with its parameter name
21 // in the params table within the given message object. Multiple parameter
22 // values are stored as tables, ordinary ones as strings.
23 // If an optional file callback function is given then it is fed with the
24 // file contents chunk by chunk and only the extracted file name is stored
25 // within the params table. The callback function will be called subsequently
26 // with three arguments:
27 // o Table containing decoded (name, file) and raw (headers) mime header data
28 // o String value containing a chunk of the file data
29 // o Boolean which indicates whether the current chunk is the last one (eof)
30 export function mimedecode_message_body(src, msg, file_cb) {
31 let len = 0, maxlen = +msg.env.CONTENT_LENGTH;
32 let err, header, field, parser;
34 parser = multipart_parser(msg.env.CONTENT_TYPE, function(what, buffer, length) {
35 if (what == parser.PART_INIT) {
38 else if (what == parser.HEADER_NAME) {
41 else if (what == parser.HEADER_VALUE && header) {
42 if (lc(header) == 'content-disposition' &&
43 header_attribute(buffer, null) == 'form-data') {
44 field.name = header_attribute(buffer, 'name');
45 field.file = header_attribute(buffer, 'filename');
46 field[1] = field.file;
49 field.headers = field.headers || {};
50 field.headers[header] = buffer;
52 else if (what == parser.PART_BEGIN) {
55 else if (what == parser.PART_DATA && field.name && length > 0) {
58 file_cb(field, buffer, false);
60 msg.params[field.name] = msg.params[field.name] || field;
64 field.fd = mkstemp(field.name);
67 field.fd.write(buffer);
68 msg.params[field.name] = msg.params[field.name] || field;
76 else if (what == parser.PART_END && field.name) {
77 if (field.file && msg.params[field.name]) {
79 file_cb(field, '', true);
84 let val = msg.params[field.name];
86 if (type(val) == 'array')
87 push(val, field.value || '');
89 msg.params[field.name] = [ val, field.value || '' ];
91 msg.params[field.name] = field.value || '';
96 else if (what == parser.ERROR) {
101 }, HTTP_MAX_CONTENT);
106 len += length(chunk);
108 if (maxlen && len > maxlen + 2)
109 die('Message body size exceeds Content-Length');
111 if (!parser.parse(chunk))
119 // Decode an urlencoded http message body with application/x-www-urlencoded
120 // Content-Type. Stores all extracted data associated with its parameter name
121 // in the params table within the given message object. Multiple parameter
122 // values are stored as tables, ordinary ones as strings.
123 export function urldecode_message_body(src, msg) {
124 let len = 0, maxlen = +msg.env.CONTENT_LENGTH;
125 let err, name, value, parser;
127 parser = urlencoded_parser(function (what, buffer, length) {
128 if (what == parser.TUPLE) {
132 else if (what == parser.NAME) {
133 name = _urldecode(buffer, DECODE_PLUS);
135 else if (what == parser.VALUE && name) {
136 let val = msg.params[name];
138 if (type(val) == 'array')
139 push(val, _urldecode(buffer, DECODE_PLUS) || '');
140 else if (val != null)
141 msg.params[name] = [ val, _urldecode(buffer, DECODE_PLUS) || '' ];
143 msg.params[name] = _urldecode(buffer, DECODE_PLUS) || '';
145 else if (what == parser.ERROR) {
150 }, HTTP_MAX_CONTENT);
155 len += length(chunk);
157 if (maxlen && len > maxlen + 2)
158 die('Message body size exceeds Content-Length');
160 if (!parser.parse(chunk))
168 // This function will examine the Content-Type within the given message object
169 // to select the appropriate content decoder.
170 // Currently the application/x-www-urlencoded and application/form-data
171 // mime types are supported. If the encountered content encoding can't be
172 // handled then the whole message body will be stored unaltered as 'content'
173 // property within the given message object.
174 export function parse_message_body(src, msg, filecb) {
175 if (msg.env.CONTENT_LENGTH || msg.env.REQUEST_METHOD == 'POST') {
176 let ctype = header_attribute(msg.env.CONTENT_TYPE, null);
178 // Is it multipart/mime ?
179 if (ctype == 'multipart/form-data')
180 return mimedecode_message_body(src, msg, filecb);
182 // Is it application/x-www-form-urlencoded ?
183 else if (ctype == 'application/x-www-form-urlencoded')
184 return urldecode_message_body(src, msg);
186 // Unhandled encoding
187 // If a file callback is given then feed it chunk by chunk, else
188 // store whole buffer in message.content
191 // If we have a file callback then feed it
192 if (type(filecb) == 'function') {
195 encoding: msg.env.CONTENT_TYPE
200 return filecb(meta, chunk, false);
202 return filecb(meta, null, true);
206 // ... else append to .content
208 let chunks = [], len = 0;
211 len += length(chunk);
213 if (len > HTTP_MAX_CONTENT)
214 die('POST data exceeds maximum allowed length');
220 msg.content = join('', chunks);
221 msg.content_length = len;
242 export function build_querystring(q) {
245 for (let k, v in q) {
247 length(s) ? '&' : '?',
248 _urlencode(k, ENCODE_IF_NEEDED | ENCODE_FULL) || k,
250 _urlencode(v, ENCODE_IF_NEEDED | ENCODE_FULL) || v
257 export function urlencode(value) {
263 return _urlencode(value, ENCODE_IF_NEEDED | ENCODE_FULL) || value;
266 export function urldecode(value, decode_plus) {
272 return _urldecode(value, DECODE_IF_NEEDED | (decode_plus ? DECODE_PLUS : 0)) || value;
275 // Extract and split urlencoded data pairs, separated bei either "&" or ";"
276 // from given url or string. Returns a table with urldecoded values.
277 // Simple parameters are stored as string values associated with the parameter
278 // name within the table. Parameters with multiple values are stored as array
279 // containing the corresponding values.
280 export function urldecode_params(url, tbl) {
281 let parser, name, value;
282 let params = tbl || {};
284 parser = urlencoded_parser(function(what, buffer, length) {
285 if (what == parser.TUPLE) {
289 else if (what == parser.NAME) {
290 name = _urldecode(buffer);
292 else if (what == parser.VALUE && name) {
293 params[name] = _urldecode(buffer) || '';
300 let m = match(('' + (url || '')), /[^?]*$/);
302 parser.parse(m ? m[0] : '');
309 // Encode each key-value-pair in given table to x-www-urlencoded format,
310 // separated by '&'. Tables are encoded as parameters with multiple values by
311 // repeating the parameter name with each value.
312 export function urlencode_params(tbl) {
315 for (let k, v in tbl) {
316 if (type(v) == 'array') {
324 _urlencode('' + v2));
338 return join(enc, '');
342 // Default IO routines suitable for CGI invocation
343 let avail_len = +getenv('CONTENT_LENGTH');
345 const default_source = () => {
346 let rlen = min(avail_len, 4096);
354 let chunk = stdin.read(rlen);
357 die(`Input read error: ${fserror()}`);
359 avail_len -= length(chunk);
364 const default_sink = (...chunks) => {
365 for (let chunk in chunks)
372 formvalue: function(name, noparse) {
373 if (!noparse && !this.parsed_input)
377 return this.message.params[name];
379 return this.message.params;
382 formvaluetable: function(prefix) {
385 prefix = (prefix || '') + '.';
387 if (!this.parsed_input)
390 for (let k, v in this.message.params)
391 if (index(k, prefix) == 0)
392 vals[substr(k, length(prefix))] = '' + v;
397 content: function() {
398 if (!this.parsed_input)
401 return this.message.content;
404 getcookie: function(name) {
405 return header_attribute(`cookie; ${this.getenv('HTTP_COOKIE') ?? ''}`, name);
408 getenv: function(name) {
410 return this.message.env[name];
412 return this.message.env;
415 setfilehandler: function(callback) {
416 if (type(callback) == 'resource' && type(callback.call) == 'function')
417 this.filehandler = (...args) => callback.call(...args);
418 else if (type(callback) == 'function')
419 this.filehandler = callback;
421 die('Invalid callback argument for setfilehandler()');
423 if (!this.parsed_input)
426 // If input has already been parsed then uploads are stored as unlinked
427 // temporary files pointed to by open file handles in the parameter
428 // value table. Loop all params, and invoke the file callback for any
429 // param with an open file handle.
430 for (let name, value in this.message.params) {
432 let data = value.fd.read(1024);
433 let eof = (data == null || data == '');
435 this.filehandler(value, data, eof);
445 _parse_input: function() {
452 this.parsed_input = true;
456 this.write_headers();
460 header: function(key, value) {
462 this.headers[lc(key)] = value;
465 prepare_content: function(mime) {
466 if (!this.headers?.['content-type']) {
467 if (mime == 'application/xhtml+xml') {
468 if (index(this.getenv('HTTP_ACCEPT'), mime) == -1) {
469 mime = 'text/html; charset=UTF-8';
470 this.header('Vary', 'Accept');
474 this.header('Content-Type', mime);
478 status: function(code, message) {
479 this.status_code = code ?? 200;
480 this.status_message = message ?? 'OK';
483 write_headers: function() {
487 if (!this.status_code)
490 if (!this.headers?.['content-type'])
491 this.header('Content-Type', 'text/html; charset=UTF-8');
493 if (!this.headers?.['cache-control']) {
494 this.header('Cache-Control', 'no-cache');
495 this.header('Expires', '0');
498 if (!this.headers?.['x-frame-options'])
499 this.header('X-Frame-Options', 'SAMEORIGIN');
501 if (!this.headers?.['x-xss-protection'])
502 this.header('X-XSS-Protection', '1; mode=block');
504 if (!this.headers?.['x-content-type-options'])
505 this.header('X-Content-Type-Options', 'nosniff');
507 this.output('Status: ');
508 this.output(this.status_code);
510 this.output(this.status_message);
513 for (let k, v in this.headers) {
525 // If the content chunk is nil this function will automatically invoke close.
526 write: function(content) {
527 if (content != null && !this.closed) {
528 this.write_headers();
529 this.output(content);
538 redirect: function(url) {
539 this.status(302, 'Found');
540 this.header('Location', url ?? '/');
544 write_json: function(value) {
545 this.write(sprintf('%.J', value));
557 export default function(env, sourcein, sinkout) {
559 input: sourcein ?? default_source,
560 output: sinkout ?? default_sink,
562 // File handler nil by default to let .content() work
565 // HTTP-Message table
569 params: urldecode_params(env?.QUERY_STRING ?? '')