123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
- // Licensed to the public under the Apache License 2.0.
- import {
- urlencode as _urlencode,
- urldecode as _urldecode,
- urlencoded_parser, multipart_parser, header_attribute,
- ENCODE_IF_NEEDED, ENCODE_FULL, DECODE_IF_NEEDED, DECODE_PLUS
- } from 'lucihttp';
- import {
- error as fserror,
- stdin, stdout, mkstemp
- } from 'fs';
- // luci.http module scope
- export let HTTP_MAX_CONTENT = 1024*100; // 100 kB maximum content size
- // Decode a mime encoded http message body with multipart/form-data
- // Content-Type. Stores all extracted data associated with its parameter name
- // in the params table within the given message object. Multiple parameter
- // values are stored as tables, ordinary ones as strings.
- // If an optional file callback function is given then it is fed with the
- // file contents chunk by chunk and only the extracted file name is stored
- // within the params table. The callback function will be called subsequently
- // with three arguments:
- // o Table containing decoded (name, file) and raw (headers) mime header data
- // o String value containing a chunk of the file data
- // o Boolean which indicates whether the current chunk is the last one (eof)
- export function mimedecode_message_body(src, msg, file_cb) {
- let len = 0, maxlen = +msg.env.CONTENT_LENGTH;
- let err, header, field, parser;
- parser = multipart_parser(msg.env.CONTENT_TYPE, function(what, buffer, length) {
- if (what == parser.PART_INIT) {
- field = {};
- }
- else if (what == parser.HEADER_NAME) {
- header = lc(buffer);
- }
- else if (what == parser.HEADER_VALUE && header) {
- if (lc(header) == 'content-disposition' &&
- header_attribute(buffer, null) == 'form-data') {
- field.name = header_attribute(buffer, 'name');
- field.file = header_attribute(buffer, 'filename');
- field[1] = field.file;
- }
- field.headers = field.headers || {};
- field.headers[header] = buffer;
- }
- else if (what == parser.PART_BEGIN) {
- return !field.file;
- }
- else if (what == parser.PART_DATA && field.name && length > 0) {
- if (field.file) {
- if (file_cb) {
- file_cb(field, buffer, false);
- msg.params[field.name] = msg.params[field.name] || field;
- }
- else {
- if (!field.fd)
- field.fd = mkstemp(field.name);
- if (field.fd) {
- field.fd.write(buffer);
- msg.params[field.name] = msg.params[field.name] || field;
- }
- }
- }
- else {
- field.value = buffer;
- }
- }
- else if (what == parser.PART_END && field.name) {
- if (field.file && msg.params[field.name]) {
- if (file_cb)
- file_cb(field, '', true);
- else if (field.fd)
- field.fd.seek(0);
- }
- else {
- let val = msg.params[field.name];
- if (type(val) == 'array')
- push(val, field.value || '');
- else if (val != null)
- msg.params[field.name] = [ val, field.value || '' ];
- else
- msg.params[field.name] = field.value || '';
- }
- field = null;
- }
- else if (what == parser.ERROR) {
- err = buffer;
- }
- return true;
- }, HTTP_MAX_CONTENT);
- while (true) {
- let chunk = src();
- len += length(chunk);
- if (maxlen && len > maxlen + 2)
- die('Message body size exceeds Content-Length');
- if (!parser.parse(chunk))
- die(err);
- if (chunk == null)
- break;
- }
- };
- // Decode an urlencoded http message body with application/x-www-urlencoded
- // Content-Type. Stores all extracted data associated with its parameter name
- // in the params table within the given message object. Multiple parameter
- // values are stored as tables, ordinary ones as strings.
- export function urldecode_message_body(src, msg) {
- let len = 0, maxlen = +msg.env.CONTENT_LENGTH;
- let err, name, value, parser;
- parser = urlencoded_parser(function (what, buffer, length) {
- if (what == parser.TUPLE) {
- name = null;
- value = null;
- }
- else if (what == parser.NAME) {
- name = _urldecode(buffer, DECODE_PLUS);
- }
- else if (what == parser.VALUE && name) {
- let val = msg.params[name];
- if (type(val) == 'array')
- push(val, _urldecode(buffer, DECODE_PLUS) || '');
- else if (val != null)
- msg.params[name] = [ val, _urldecode(buffer, DECODE_PLUS) || '' ];
- else
- msg.params[name] = _urldecode(buffer, DECODE_PLUS) || '';
- }
- else if (what == parser.ERROR) {
- err = buffer;
- }
- return true;
- }, HTTP_MAX_CONTENT);
- while (true) {
- let chunk = src();
- len += length(chunk);
- if (maxlen && len > maxlen + 2)
- die('Message body size exceeds Content-Length');
- if (!parser.parse(chunk))
- die(err);
- if (chunk == null)
- break;
- }
- };
- // This function will examine the Content-Type within the given message object
- // to select the appropriate content decoder.
- // Currently the application/x-www-urlencoded and application/form-data
- // mime types are supported. If the encountered content encoding can't be
- // handled then the whole message body will be stored unaltered as 'content'
- // property within the given message object.
- export function parse_message_body(src, msg, filecb) {
- if (msg.env.CONTENT_LENGTH || msg.env.REQUEST_METHOD == 'POST') {
- let ctype = header_attribute(msg.env.CONTENT_TYPE, null);
- // Is it multipart/mime ?
- if (ctype == 'multipart/form-data')
- return mimedecode_message_body(src, msg, filecb);
- // Is it application/x-www-form-urlencoded ?
- else if (ctype == 'application/x-www-form-urlencoded')
- return urldecode_message_body(src, msg);
- // Unhandled encoding
- // If a file callback is given then feed it chunk by chunk, else
- // store whole buffer in message.content
- let sink;
- // If we have a file callback then feed it
- if (type(filecb) == 'function') {
- let meta = {
- name: 'raw',
- encoding: msg.env.CONTENT_TYPE
- };
- sink = (chunk) => {
- if (chunk != null)
- return filecb(meta, chunk, false);
- else
- return filecb(meta, null, true);
- };
- }
- // ... else append to .content
- else {
- let chunks = [], len = 0;
- sink = (chunk) => {
- len += length(chunk);
- if (len > HTTP_MAX_CONTENT)
- die('POST data exceeds maximum allowed length');
- if (chunk != null) {
- push(chunks, chunk);
- }
- else {
- msg.content = join('', chunks);
- msg.content_length = len;
- }
- };
- }
- // Pump data...
- while (true) {
- let chunk = src();
- sink(chunk);
- if (chunk == null)
- break;
- }
- return true;
- }
- return false;
- };
- export function build_querystring(q) {
- let s = [];
- for (let k, v in q) {
- push(s,
- length(s) ? '&' : '?',
- _urlencode(k, ENCODE_IF_NEEDED | ENCODE_FULL) || k,
- '=',
- _urlencode(v, ENCODE_IF_NEEDED | ENCODE_FULL) || v
- );
- }
- return join('', s);
- };
- export function urlencode(value) {
- if (value == null)
- return null;
- value = '' + value;
- return _urlencode(value, ENCODE_IF_NEEDED | ENCODE_FULL) || value;
- };
- export function urldecode(value, decode_plus) {
- if (value == null)
- return null;
- value = '' + value;
- return _urldecode(value, DECODE_IF_NEEDED | (decode_plus ? DECODE_PLUS : 0)) || value;
- };
- // Extract and split urlencoded data pairs, separated bei either "&" or ";"
- // from given url or string. Returns a table with urldecoded values.
- // Simple parameters are stored as string values associated with the parameter
- // name within the table. Parameters with multiple values are stored as array
- // containing the corresponding values.
- export function urldecode_params(url, tbl) {
- let parser, name, value;
- let params = tbl || {};
- parser = urlencoded_parser(function(what, buffer, length) {
- if (what == parser.TUPLE) {
- name = null;
- value = null;
- }
- else if (what == parser.NAME) {
- name = _urldecode(buffer);
- }
- else if (what == parser.VALUE && name) {
- params[name] = _urldecode(buffer) || '';
- }
- return true;
- });
- if (parser) {
- let m = match(('' + (url || '')), /[^?]*$/);
- parser.parse(m ? m[0] : '');
- parser.parse(null);
- }
- return params;
- };
- // Encode each key-value-pair in given table to x-www-urlencoded format,
- // separated by '&'. Tables are encoded as parameters with multiple values by
- // repeating the parameter name with each value.
- export function urlencode_params(tbl) {
- let enc = [];
- for (let k, v in tbl) {
- if (type(v) == 'array') {
- for (let v2 in v) {
- if (length(enc))
- push(enc, '&');
- push(enc,
- _urlencode(k),
- '=',
- _urlencode('' + v2));
- }
- }
- else {
- if (length(enc))
- push(enc, '&');
- push(enc,
- _urlencode(k),
- '=',
- _urlencode('' + v));
- }
- }
- return join(enc, '');
- };
- // Default IO routines suitable for CGI invocation
- let avail_len = +getenv('CONTENT_LENGTH');
- const default_source = () => {
- let rlen = min(avail_len, 4096);
- if (rlen == 0) {
- stdin.close();
- return null;
- }
- let chunk = stdin.read(rlen);
- if (chunk == null)
- die(`Input read error: ${fserror()}`);
- avail_len -= length(chunk);
- return chunk;
- };
- const default_sink = (...chunks) => {
- for (let chunk in chunks)
- stdout.write(chunk);
- stdout.flush();
- };
- const Class = {
- formvalue: function(name, noparse) {
- if (!noparse && !this.parsed_input)
- this._parse_input();
- if (name != null)
- return this.message.params[name];
- else
- return this.message.params;
- },
- formvaluetable: function(prefix) {
- let vals = {};
- prefix = (prefix || '') + '.';
- if (!this.parsed_input)
- this._parse_input();
- for (let k, v in this.message.params)
- if (index(k, prefix) == 0)
- vals[substr(k, length(prefix))] = '' + v;
- return vals;
- },
- content: function() {
- if (!this.parsed_input)
- this._parse_input();
- return this.message.content;
- },
- getcookie: function(name) {
- return header_attribute(`cookie; ${this.getenv('HTTP_COOKIE') ?? ''}`, name);
- },
- getenv: function(name) {
- if (name != null)
- return this.message.env[name];
- else
- return this.message.env;
- },
- setfilehandler: function(callback) {
- if (type(callback) == 'resource' && type(callback.call) == 'function')
- this.filehandler = (...args) => callback.call(...args);
- else if (type(callback) == 'function')
- this.filehandler = callback;
- else
- die('Invalid callback argument for setfilehandler()');
- if (!this.parsed_input)
- return;
- // If input has already been parsed then uploads are stored as unlinked
- // temporary files pointed to by open file handles in the parameter
- // value table. Loop all params, and invoke the file callback for any
- // param with an open file handle.
- for (let name, value in this.message.params) {
- while (value?.fd) {
- let data = value.fd.read(1024);
- let eof = (length(data) == 0);
- this.filehandler(value, data, eof);
- if (eof) {
- value.fd.close();
- value.fd = null;
- }
- }
- }
- },
- _parse_input: function() {
- parse_message_body(
- this.input,
- this.message,
- this.filehandler
- );
- this.parsed_input = true;
- },
- close: function() {
- this.write_headers();
- this.closed = true;
- },
- header: function(key, value) {
- this.headers ??= {};
- this.headers[lc(key)] = value;
- },
- prepare_content: function(mime) {
- if (!this.headers?.['content-type']) {
- if (mime == 'application/xhtml+xml') {
- if (index(this.getenv('HTTP_ACCEPT'), mime) == -1) {
- mime = 'text/html; charset=UTF-8';
- this.header('Vary', 'Accept');
- }
- }
- this.header('Content-Type', mime);
- }
- },
- status: function(code, message) {
- this.status_code = code ?? 200;
- this.status_message = message ?? 'OK';
- },
- write_headers: function() {
- if (this.eoh)
- return;
- if (!this.status_code)
- this.status();
- if (!this.headers?.['content-type'])
- this.header('Content-Type', 'text/html; charset=UTF-8');
- if (!this.headers?.['cache-control']) {
- this.header('Cache-Control', 'no-cache');
- this.header('Expires', '0');
- }
- if (!this.headers?.['x-frame-options'])
- this.header('X-Frame-Options', 'SAMEORIGIN');
- if (!this.headers?.['x-xss-protection'])
- this.header('X-XSS-Protection', '1; mode=block');
- if (!this.headers?.['x-content-type-options'])
- this.header('X-Content-Type-Options', 'nosniff');
- this.output('Status: ');
- this.output(this.status_code);
- this.output(' ');
- this.output(this.status_message);
- this.output('\r\n');
- for (let k, v in this.headers) {
- this.output(k);
- this.output(': ');
- this.output(v);
- this.output('\r\n');
- }
- this.output('\r\n');
- this.eoh = true;
- },
- // If the content chunk is nil this function will automatically invoke close.
- write: function(content) {
- if (content != null && !this.closed) {
- this.write_headers();
- this.output(content);
- return true;
- }
- else {
- this.close();
- }
- },
- redirect: function(url) {
- this.status(302, 'Found');
- this.header('Location', url ?? '/');
- this.close();
- },
- write_json: function(value) {
- this.write(sprintf('%.J', value));
- },
- urlencode,
- urlencode_params,
- urldecode,
- urldecode_params,
- build_querystring
- };
- export default function(env, sourcein, sinkout) {
- return proto({
- input: sourcein ?? default_source,
- output: sinkout ?? default_sink,
- // File handler nil by default to let .content() work
- file: null,
- // HTTP-Message table
- message: {
- env,
- headers: {},
- params: urldecode_params(env?.QUERY_STRING ?? '')
- },
- parsed_input: false
- }, Class);
- };
|