Poor man's virtual hosts

I wanted to use my router to serve a (semi-) static website on the LAN, next to the Luci interface. It is possible to start 2 instances of uhttpd on different ports, but that is not very convenient. uhttpd doesn't support virtual hosts, or at least I couldn't find any documentation for that. Instead found a way using cgi.
/www/index.html is basically a redirect to /cgi-bin/luci. I changed that to a redirect to /virtualhosts.sh.
Then I wrote a cgi script /www/virtualhosts.sh:

#!/bin/sh

# optional. Strip network name to handle openwrt.lan the same as openwrt
HTTP_HOST=$(echo ${HTTP_HOST} | cut -d '.' -f 1)
# Hostname can be upper or mixed case, so change it to lower
HOST=$(echo ${HOSTNAME} | awk '{print tolower($0)}')
# Default url
URL=/cgi-bin/luci

if [ "${HTTP_HOST}" != "${HOST}" ] 
then 
    case ${HTTP_HOST} in
	domain1)
	    URL=/domain1/index.html
	    ;;
	domain2)
	    URL=/domain2/index.html
	    ;;
    esac
fi

cat <<__EOF__
Content-type: text/html; charset=utf-8

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="refresh" content="0; URL=${URL}" />
</head>
<body>
</body>
</html>
__EOF__

Then /bin/sh had to be added as cgi interpreter to /etc/config/uhttpd

config uhttpd main

<snip>
	# List of extension->interpreter mappings.
	# Files with an associated interpreter can
	# be called outside of the CGI prefix and do
	# not need to be executable.
	list interpreter	".sh=/bin/sh"

and domain1 and domain2 added as static hostnames pointing to the router's lan address. Restart uhttpd and the poor man's virtual hosts work. http://domain1.lan/ resolves to /www/domain1/index.html

3 Likes

You can make a redirection just by sending 302 Status with Location header.
But your users will always see the /domain1/ part in address bar and this may be confusing for them.
Also you may not need to configure .sh files as interpreter but instead create /cgi-bin/index.sh or index.cgi file. Don't remember exactly if uhttpd supports default cgi scripts but busybox httpd works exactly like this.

Virtual Hosts is something that very needed. But two domains may have different settings like base auth while uhttpd is single threaded and wasn't developed with multiple hosts in mind and may share some global variables. So you must run two different instances of uhttpd with their own settings and put some proxy that will redirect requests depending on Host header. This is already implemented by OpenWRT authors and the proxy called tinyproxy https://openwrt.org/docs/guide-user/services/proxy/tinyproxy.

There was some proposition to implement a lightweight variant when settings are the same but www folders are switched by domain:

In lighttpd this solution called Simple Virtual Host
I think this is a good solution for embedded devices but it didn't received any attention.

If you just need to have one domain for external domain with your site and another for local network with Luci then you can start two uhttpd instances on 80 port but on different interfaces i.e. IP:

# luci for local network
config uhttpd main
   list listen_http '192.168.1.1:80'
   option home '/www/'
config uhttpd example_domain
   list listen_http 'example.com:80'
   option home '/www_example.com/'
   option rfc1918_filter '0' # allow external IPs

I did this for my homepage https://stokito.wordpress.com/2019/04/22/setup-ddns-in-openwrt/

Another possible solution may be to use firewall eBPF rule that will check for the Host header and redirect packet to another port depending on domain. This will be a best solution from performance perspective but TLS packets can't be routed in such way so no https support possible.

Other web services like Nginx and Lighttpd supports virtual hosts out of the box

upstream... fuzzing detection...

uhttpd.main.error_page='/index.sh'
if [ "$REDIRECT_STATUS" = '404' ]; then
	if echo $REQUEST_URI | grep -q '/luci-static/'; then exit 0; fi
	if echo $REQUEST_URI | grep '/resources/' | grep -q 'loading.gif'; then exit 0; fi
	logger -t uhttpd "luci denied page $REMOTE_HOST $REQUEST_URI"
	echo "luci denied page $REMOTE_HOST $REQUEST_URI" >/dev/console
fi

on second thought... those echo's should be case... much safer...