Luci.http request ucode Documentation Not Found

Luci uses uhttpd to interpret cgi-bin requests made over http. The option cgi_prefix '/cgi-bin' setting in /etc/config/uhttpd defines the file location for the luci interpretation script.

The /www/cgi-bin/luci script is as follows:

#!/usr/bin/env ucode

'use strict';

import { stdin, stdout } from 'fs';

import dispatch from 'luci.dispatcher';
import request from 'luci.http';

const input_bufsize = 4096;
let input_available = +getenv('CONTENT_LENGTH') || 0;

function read(len) {
	if (input_available == 0) {
		stdin.close();

		return null;
	}

	let chunk = stdin.read(min(input_available, len ?? input_bufsize, input_bufsize));

	if (chunk == null) {
		input_available = 0;
		stdin.close();
	}
	else {
		input_available -= length(chunk);
	}

	return chunk;
}

function write(data) {
	return stdout.write(data);
}

let req = request(getenv(), read, write);

dispatch(req);

req.close();

Example http request:

GET /cgi-bin/luci/admin/menu?1709065406341 HTTP/1.1
Host: ip
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36
Accept: */*
Referer: http://ip/cgi-bin/luci/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: sysauth_http=2ab91d1f2672560abcdef0692ebexxxx
Connection: close

Example http response:

HTTP/1.1 200 OK
Connection: close
content-type: application/json; charset=UTF-8
cache-control: no-cache
expires: 0
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
Content-Length: 24047

{
	"action": {
		"type": "firstchild"
	},
	"children": {
		"admin": {
			"satisfied": true,
			"children": {
				"network": {
					"satisfied": true,
					"children": {
						"firewall": {
							"satisfied": true,
							"action": {
								"type": "alias",
								"path": "admin/network/firewall/zones"
							},
							"depends": {
								"acl": [
									"luci-app-firewall"
								],
								"fs": {
									"/sbin/fw3": "executable"
								},
								"uci": {
									"firewall": true
								}
							},
							"order": 60,
							"title": "Firewall",
							"children": {
								"zones": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "firewall/zones"
									},
									"order": 10,
									"title": "General Settings"
								},
								"forwards": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "firewall/forwards"
									},
									"order": 20,
									"title": "Port Forwards"
								},
								"rules": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "firewall/rules"
									},
									"order": 30,
									"title": "Traffic Rules"
								},
								"snats": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "firewall/snats"
									},
									"order": 40,
									"title": "NAT Rules"
								},
								"ipsets": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "firewall/ipsets"
									},
									"order": 45,
									"title": "IP Sets"
								},
								"custom": {
									"satisfied": false,
									"action": {
										"type": "view",
										"path": "firewall/custom"
									},
									"depends": {
										"fs": {
											"/usr/share/fw3/helpers.conf": "file"
										}
									},
									"order": 50,
									"title": "Custom Rules"
								}
							}
						},
						"switch": {
							"satisfied": false,
							"action": {
								"type": "view",
								"path": "network/switch"
							},
							"depends": {
								"acl": [
									"luci-mod-network-config"
								],
								"fs": {
									"/sbin/swconfig": "executable"
								},
								"uci": {
									"network": {
										"@switch": true
									}
								}
							},
							"order": 20,
							"title": "Switch"
						},
						"wireless": {
							"satisfied": false,
							"action": {
								"type": "view",
								"path": "network/wireless"
							},
							"depends": {
								"acl": [
									"luci-mod-network-config"
								],
								"uci": {
									"wireless": {
										"@wifi-device": true
									}
								}
							},
							"order": 15,
							"title": "Wireless"
						},
						"remote_addr": {
							"satisfied": true,
							"wildcard": true,
							"action": {
								"type": "call",
								"module": "luci.controller.admin.network",
								"function": "remote_addr"
							}
						},
						"network": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "network/interfaces"
							},
							"depends": {
								"acl": [
									"luci-mod-network-config"
								]
							},
							"order": 10,
							"title": "Interfaces"
						},
						"routes": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "network/routes"
							},
							"depends": {
								"acl": [
									"luci-mod-network-config"
								]
							},
							"order": 30,
							"title": "Routing"
						},
						"dhcp": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "network/dhcp"
							},
							"depends": {
								"acl": [
									"luci-mod-network-dhcp"
								],
								"fs": {
									"/usr/sbin/dnsmasq": "executable"
								},
								"uci": {
									"dhcp": true
								}
							},
							"order": 40,
							"title": "DHCP and DNS"
						},
						"diagnostics": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "network/diagnostics"
							},
							"depends": {
								"acl": [
									"luci-mod-network-diagnostics"
								]
							},
							"order": 50,
							"title": "Diagnostics",
							"readonly": true
						}
					},
					"action": {
						"type": "firstchild",
						"recurse": true
					},
					"order": 50,
					"title": "Network"
				},
				"system": {
					"satisfied": true,
					"children": {
						"opkg": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "opkg"
							},
							"depends": {
								"acl": [
									"luci-app-opkg"
								]
							},
							"order": 30,
							"title": "Software"
						},
						"system": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "system/system"
							},
							"depends": {
								"acl": [
									"luci-mod-system-config"
								]
							},
							"order": 1,
							"title": "System"
						},
						"admin": {
							"satisfied": true,
							"action": {
								"type": "firstchild"
							},
							"depends": {
								"acl": [
									"luci-mod-system-config",
									"luci-mod-system-ssh"
								]
							},
							"order": 2,
							"title": "Administration",
							"children": {
								"password": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "system/password"
									},
									"depends": {
										"acl": [
											"luci-mod-system-config"
										]
									},
									"order": 1,
									"title": "Router Password"
								},
								"dropbear": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "system/dropbear"
									},
									"depends": {
										"acl": [
											"luci-mod-system-ssh"
										],
										"fs": {
											"/usr/sbin/dropbear": "executable"
										}
									},
									"order": 2,
									"title": "SSH Access"
								},
								"sshkeys": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "system/sshkeys"
									},
									"depends": {
										"acl": [
											"luci-mod-system-ssh"
										],
										"fs": {
											"/usr/sbin/dropbear": "executable"
										}
									},
									"order": 3,
									"title": "SSH-Keys"
								},
								"uhttpd": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "system/uhttpd"
									},
									"depends": {
										"acl": [
											"luci-mod-system-uhttpd"
										],
										"fs": {
											"/usr/sbin/uhttpd": "executable"
										}
									},
									"order": 4,
									"title": "HTTP(S) Access"
								}
							}
						},
						"startup": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "system/startup"
							},
							"depends": {
								"acl": [
									"luci-mod-system-init"
								]
							},
							"order": 45,
							"title": "Startup"
						},
						"crontab": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "system/crontab"
							},
							"depends": {
								"acl": [
									"luci-mod-system-cron"
								]
							},
							"order": 46,
							"title": "Scheduled Tasks"
						},
						"mounts": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "system/mounts"
							},
							"depends": {
								"acl": [
									"luci-mod-system-mounts"
								],
								"fs": {
									"/sbin/block": "executable"
								}
							},
							"order": 50,
							"title": "Mount Points"
						},
						"leds": {
							"satisfied": false,
							"action": {
								"type": "view",
								"path": "system/leds"
							},
							"depends": {
								"acl": [
									"luci-mod-system-config"
								],
								"fs": {
									"/sys/class/leds": "directory"
								}
							},
							"order": 60,
							"title": "LED Configuration"
						},
						"flash": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "system/flash"
							},
							"depends": {
								"acl": [
									"luci-mod-system-flash"
								]
							},
							"order": 70,
							"title": "Backup / Flash Firmware"
						},
						"reboot": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "system/reboot"
							},
							"depends": {
								"acl": [
									"luci-mod-system-reboot"
								]
							},
							"order": 90,
							"title": "Reboot"
						}
					},
					"action": {
						"type": "firstchild",
						"preferred": "system",
						"recurse": true
					},
					"order": 20,
					"title": "System"
				},
				"status": {
					"satisfied": true,
					"action": {
						"type": "firstchild",
						"preferred": "overview",
						"recurse": true
					},
					"order": 10,
					"title": "Status",
					"children": {
						"overview": {
							"satisfied": true,
							"action": {
								"type": "template",
								"path": "admin_status/index"
							},
							"depends": {
								"acl": [
									"luci-mod-status-index"
								]
							},
							"order": 1,
							"title": "Overview"
						},
						"routes": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "status/routes"
							},
							"depends": {
								"acl": [
									"luci-mod-status-routes"
								]
							},
							"order": 2,
							"title": "Routing",
							"readonly": true
						},
						"iptables": {
							"satisfied": false,
							"action": {
								"type": "view",
								"path": "status/iptables"
							},
							"depends": {
								"acl": [
									"luci-mod-status-firewall"
								],
								"fs": [
									{
										"/usr/sbin/nft": "absent",
										"/usr/sbin/iptables": "executable"
									},
									{
										"/usr/sbin/nft": "absent",
										"/usr/sbin/ip6tables": "executable"
									}
								]
							},
							"order": 3,
							"title": "Firewall"
						},
						"nftables": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "status/nftables"
							},
							"depends": {
								"acl": [
									"luci-mod-status-firewall"
								],
								"fs": {
									"/usr/sbin/nft": "executable"
								}
							},
							"order": 3,
							"title": "Firewall",
							"children": {
								"iptables": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "status/iptables"
									}
								}
							}
						},
						"logs": {
							"satisfied": true,
							"action": {
								"type": "alias",
								"path": "admin/status/logs/syslog"
							},
							"depends": {
								"acl": [
									"luci-mod-status-logs"
								]
							},
							"order": 4,
							"title": "System Log",
							"children": {
								"syslog": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "status/syslog"
									},
									"order": 1,
									"title": "System Log"
								},
								"dmesg": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "status/dmesg"
									},
									"order": 2,
									"title": "Kernel Log"
								}
							},
							"readonly": true
						},
						"processes": {
							"satisfied": true,
							"action": {
								"type": "view",
								"path": "status/processes"
							},
							"depends": {
								"acl": [
									"luci-mod-status-processes"
								]
							},
							"order": 6,
							"title": "Processes"
						},
						"channel_analysis": {
							"satisfied": false,
							"action": {
								"type": "view",
								"path": "status/channel_analysis"
							},
							"depends": {
								"acl": [
									"luci-mod-status-channel_analysis"
								],
								"uci": {
									"wireless": {
										"@wifi-device": true
									}
								}
							},
							"order": 7,
							"title": "Channel Analysis",
							"readonly": true
						},
						"realtime": {
							"satisfied": true,
							"action": {
								"type": "alias",
								"path": "admin/status/realtime/load"
							},
							"depends": {
								"acl": [
									"luci-mod-status-realtime"
								]
							},
							"order": 7,
							"title": "Realtime Graphs",
							"children": {
								"load": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "status/load"
									},
									"order": 1,
									"title": "Load"
								},
								"bandwidth": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "status/bandwidth"
									},
									"order": 2,
									"title": "Traffic"
								},
								"wireless": {
									"satisfied": false,
									"action": {
										"type": "view",
										"path": "status/wireless"
									},
									"depends": {
										"uci": {
											"wireless": {
												"@wifi-device": true
											}
										}
									},
									"order": 3,
									"title": "Wireless"
								},
								"connections": {
									"satisfied": true,
									"action": {
										"type": "view",
										"path": "status/connections"
									},
									"order": 4,
									"title": "Connections"
								}
							},
							"readonly": true
						}
					}
				},
				"services": {
					"satisfied": true,
					"action": {
						"type": "firstchild",
						"recurse": true
					},
					"order": 40,
					"title": "Services"
				},
				"vpn": {
					"satisfied": true,
					"action": {
						"type": "firstchild",
						"recurse": true
					},
					"order": 70,
					"title": "VPN",
					"children": {
						"openvpn": {
							"satisfied": true,
							"children": {
								"advanced": {
									"satisfied": true,
									"action": {
										"module": "luci.dispatcher",
										"type": "call",
										"post": {
											"cbi.submit": true
										},
										"function": "invoke_cbi_action",
										"parameters": [
											"openvpn-advanced",
											{
											}
										]
									},
									"wildcard": true
								},
								"file": {
									"satisfied": true,
									"action": {
										"module": "luci.dispatcher",
										"type": "call",
										"post": {
											"cbi.submit": true
										},
										"function": "invoke_form_action",
										"parameters": [
											"openvpn-file"
										]
									},
									"wildcard": true
								},
								"basic": {
									"satisfied": true,
									"action": {
										"module": "luci.dispatcher",
										"type": "call",
										"post": {
											"cbi.submit": true
										},
										"function": "invoke_cbi_action",
										"parameters": [
											"openvpn-basic",
											{
											}
										]
									},
									"wildcard": true
								},
								"upload": {
									"satisfied": true,
									"action": {
										"module": "luci.controller.openvpn",
										"type": "call",
										"function": "ovpn_upload"
									}
								}
							},
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_cbi_action",
								"parameters": [
									"openvpn",
									{
									}
								]
							},
							"depends": {
								"acl": [
									"luci-app-openvpn"
								]
							},
							"title": "OpenVPN"
						}
					}
				},
				"translations": {
					"satisfied": true,
					"wildcard": true,
					"action": {
						"type": "function",
						"module": "luci.controller.admin.index",
						"function": "action_translations"
					},
					"auth": {
					}
				},
				"ubus": {
					"satisfied": true,
					"wildcard": true,
					"action": {
						"type": "function",
						"module": "luci.controller.admin.index",
						"function": "action_ubus"
					},
					"auth": {
					}
				},
				"logout": {
					"satisfied": true,
					"action": {
						"type": "function",
						"module": "luci.controller.admin.index",
						"function": "action_logout"
					},
					"depends": {
						"acl": [
							"luci-base"
						]
					},
					"order": 999,
					"title": "Log out",
					"firstchild_ineligible": true
				},
				"uci": {
					"satisfied": true,
					"action": {
						"type": "firstchild"
					},
					"children": {
						"revert": {
							"satisfied": true,
							"action": {
								"type": "function",
								"module": "luci.controller.admin.uci",
								"function": "action_revert",
								"post": true
							}
						},
						"apply_rollback": {
							"satisfied": true,
							"action": {
								"type": "function",
								"module": "luci.controller.admin.uci",
								"function": "action_apply_rollback",
								"post": true
							},
							"auth": {
								"methods": [
									"cookie:sysauth_https",
									"cookie:sysauth_http"
								]
							},
							"cors": true
						},
						"apply_unchecked": {
							"satisfied": true,
							"action": {
								"type": "function",
								"module": "luci.controller.admin.uci",
								"function": "action_apply_unchecked",
								"post": true
							},
							"auth": {
								"methods": [
									"cookie:sysauth_https",
									"cookie:sysauth_http"
								]
							},
							"cors": true
						},
						"confirm": {
							"satisfied": true,
							"action": {
								"type": "function",
								"module": "luci.controller.admin.uci",
								"function": "action_confirm"
							},
							"auth": {
							},
							"cors": true
						}
					}
				},
				"menu": {
					"satisfied": true,
					"action": {
						"type": "function",
						"module": "luci.controller.admin.index",
						"function": "action_menu"
					},
					"auth": {
					}
				},
				"docker": {
					"satisfied": true,
					"children": {
						"images_tag": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "tag_image"
							},
							"wildcard": true
						},
						"newnetwork": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/newnetwork"
								]
							},
							"wildcard": true
						},
						"networks": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/networks"
								]
							},
							"order": 5,
							"title": "Networks",
							"wildcard": true
						},
						"config": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_cbi_action",
								"parameters": [
									"dockerman/configuration",
									{
									}
								]
							},
							"order": 1,
							"title": "Configuration",
							"wildcard": true
						},
						"containers": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/containers"
								]
							},
							"order": 3,
							"title": "Containers",
							"wildcard": true
						},
						"volumes": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/volumes"
								]
							},
							"order": 6,
							"title": "Volumes",
							"wildcard": true
						},
						"overview": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/overview"
								]
							},
							"order": 2,
							"title": "Overview",
							"wildcard": true
						},
						"container_put_archive": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "upload_archive"
							},
							"wildcard": true
						},
						"images_import": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "import_images"
							},
							"wildcard": true
						},
						"confirm": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "action_confirm"
							},
							"wildcard": true
						},
						"events": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "action_events"
							},
							"order": 7,
							"title": "Events"
						},
						"images_untag": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "untag_image"
							},
							"wildcard": true
						},
						"container": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/container"
								]
							},
							"wildcard": true
						},
						"images_load": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "load_images"
							},
							"wildcard": true
						},
						"newcontainer": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/newcontainer"
								]
							},
							"wildcard": true
						},
						"images_get_tags": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "get_image_tags"
							},
							"wildcard": true
						},
						"container_stats": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "action_get_container_stats"
							},
							"wildcard": true
						},
						"images": {
							"satisfied": true,
							"action": {
								"module": "luci.dispatcher",
								"type": "call",
								"post": {
									"cbi.submit": true
								},
								"function": "invoke_form_action",
								"parameters": [
									"dockerman/images"
								]
							},
							"order": 4,
							"title": "Images",
							"wildcard": true
						},
						"container_get_archive": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "download_archive"
							},
							"wildcard": true
						},
						"images_save": {
							"satisfied": true,
							"action": {
								"module": "luci.controller.dockerman",
								"type": "call",
								"function": "save_images"
							},
							"wildcard": true
						}
					},
					"action": {
						"type": "firstchild"
					},
					"depends": {
						"acl": [
							"luci-app-dockerman"
						]
					},
					"order": 40,
					"title": "Docker"
				}
			},
			"action": {
				"type": "firstchild",
				"recurse": true
			},
			"auth": {
				"methods": [
					"cookie:sysauth_https",
					"cookie:sysauth_http"
				],
				"login": true
			},
			"order": 10,
			"title": "Administration"
		}
	}
}

The luci.http package does not mention a request function.
Is there any ucode specific documentation that I've missed?

See the last function in /usr/share/ucode/luci/http.uc, I think that's it...

Edit: if you dig through https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export#using_the_default_export you can sort of see what's happening here with the export default function()... syntax.

1 Like

Im particularly confused about the following import in the /www/cgi-cin/luci script:
import request from 'luci.http';

cat /usr/share/ucode/luci/http.uc returns the following:

// 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);
};


Perhaps a better question is: What package is the following line (in /www/cgi-bin/uhttpd) importing? Is it NOT the api detailed here? If not, how is it importing the /usr/share/ucode/luci/http.uc code? Greater insight into ucode is appreciated.
import request from 'luci.http';

It imports the export default function(…) { … } from /usr/share/ucode/luci/http.uc and names it request. The http.uc module defines no name for this constructor function.

1 Like

If I understand correctly:
Openwrt uses the /usr/share/ucode/luci path for ucode script imports?

My understanding is that ucode uses a search path of /usr/lib/ucode:/usr/share/ucode by default, so when you say

import something from x.y;

the two locations are searched for a directory x, and if it exists, search for a file y.so or y.uc in that directory. The usual sort of import search mechanism you see in js, ecmascript, lua, python and so on...

There's probably a ./ in the search path somewhere, probably the start, but I haven't used/tried it.

1 Like

Makes sense to handle packages with the FHS.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.