2022-01-19 06:42:31 -07:00
#!/usr/bin/lua
--[[
Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
Copyright (C) 2021 Tim Wilkinson
Original Perl Copyright (C) 2015 Conrad Lara
See Contributors file for additional contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Additional Terms:
Additional use restrictions exist on the AREDN(TM) trademark and logo.
See AREDNLicense.txt for more info.
Attributions to the AREDN Project must be retained in the source code.
If importing this code into a new or existing project attribution
to the AREDN project must be added to the source code.
You must not misrepresent the origin of the material contained within.
Modified versions must be modified to attribute to the original source
and be marked in reasonable ways as differentiate it from the original
version
--]]
require("nixio")
require("aredn.hardware")
require("aredn.http")
require("aredn.utils")
local html = require("aredn.html")
require("uci")
local aredn_info = require("aredn.info")
-- helpers start
function mesh_ip_to_hostnames(ip)
if not ip or ip == "" or ip == "none" then
return ""
end
2022-11-30 15:04:41 -07:00
local pattern = "^" .. ip .. "%s+([%w%-%.]+)"
local host = "none"
2022-01-19 06:42:31 -07:00
for line in io.lines("/etc/hosts")
do
local host = line:match(pattern)
if host then
return host.gsub("%s+", " / ")
end
end
2022-10-14 14:31:09 -06:00
if nixio.fs.stat("/var/run/hosts_olsr.stable") then
for line in io.lines("/var/run/hosts_olsr.stable")
do
local host = line:match(pattern)
if host then
2022-11-30 15:04:41 -07:00
host = host:gsub("^dtdlink%.","")
host = host:gsub("^mid[0-9]*%.","")
host = host:gsub("%.local.mesh$","")
return host
2022-10-14 14:31:09 -06:00
end
2022-01-19 06:42:31 -07:00
end
end
2022-11-30 15:04:41 -07:00
return host
2022-01-19 06:42:31 -07:00
end
2022-11-30 15:04:41 -07:00
function get_default_gw(iface)
-- wan will route via table 254 default gw
-- wifi will route via OLSR table 31 default gw
local rtable = ""
if iface == "wan" then
rtable = "ip route list table 254"
elseif iface == "wifi" then
rtable = "ip route list table 31"
else
return "none"
2022-01-19 06:42:31 -07:00
end
2022-11-30 15:04:41 -07:00
local p = io.popen(rtable)
2022-01-19 06:42:31 -07:00
if p then
for line in p:lines()
do
local gw = line:match("^default%svia%s([%d%.]+)")
if gw then
p:close()
return gw
end
end
p:close()
end
return "none"
end
2022-11-10 21:43:00 -07:00
function get_memavail()
local f = io.open("/proc/meminfo", "r")
if f then
for line in f:lines()
do
local memavail = line:match("^MemAvailable:%s+(%d+)%s+kB")
if memavail then
f:close()
return tonumber(memavail)
end
end
f:close()
end
return 0
end
2022-01-19 06:42:31 -07:00
function get_wifi_signal(wifiif)
local signal = -1000
local noise = -1000
for mac, station in pairs(iwinfo.nl80211.assoclist(wifiif))
do
if station.signal ~= 0 and station.signal > signal then
signal = station.signal
end
if station.noise ~= 0 and station.noise > noise then
noise = station.noise
end
end
if signal == -1000 or noise == -1000 then
2022-11-24 12:07:22 -07:00
return "none", "none"
2022-01-19 06:42:31 -07:00
else
2022-11-24 12:07:22 -07:00
if signal > 0 then
signal = (0 - signal)
end
if noise > 0 then
noise = (0 - noise)
end
2022-01-19 06:42:31 -07:00
return signal, noise
end
end
-- helpers end
local node = aredn_info.get_nvram("node")
if node == "" then
node = "NOCALL"
end
local tactical = aredn_info.get_nvram("tactical")
local config = aredn_info.get_nvram("config")
if config == "" or nixio.fs.stat("/etc/config.mesh", "type") ~= "dir" then
config = "not set"
end
2022-11-24 12:07:22 -07:00
2023-04-28 12:01:58 -06:00
local haswifi = aredn.hardware.has_wifi()
2022-01-19 06:42:31 -07:00
local wifi_iface = aredn.hardware.get_iface_name("wifi")
local wifi_nr = wifi_iface:match("wlan(%d+)")
local wifi_disabled = true
local radio = "radio0"
2023-04-28 12:01:58 -06:00
if haswifi and wifi_nr then
2022-01-19 06:42:31 -07:00
wifi_disabled = false
radio = "radio" .. wifi_nr
end
local cursor = uci.cursor()
local wifi_channel
local wifi_chanbw
2023-12-06 13:06:34 -07:00
local wifi_freq_range = ""
2022-01-19 06:42:31 -07:00
local wifi_ssid
if not wifi_disabled then
2022-12-22 13:22:49 -07:00
wifi_channel = cursor:get("wireless", radio, "channel")
wifi_channel = tonumber(wifi_channel) or 0
2023-12-06 13:06:34 -07:00
wifi_chanbw = tonumber(cursor:get("wireless", radio, "chanbw") or "20")
2023-06-25 20:10:07 -06:00
local rfchans = aredn.hardware.get_rfchannels(wifi_iface)
if rfchans and rfchans[1] then
local basefreq = rfchans[1].frequency
if basefreq > 3000 and basefreq < 5000 then
wifi_channel = wifi_channel * 5 + 3000
elseif basefreq > 900 and basefreq < 2300 then
wifi_channel = wifi_channel * 5 + 887
end
2023-12-06 13:06:34 -07:00
for _, chan in ipairs(rfchans)
do
if chan.number == wifi_channel then
wifi_freq_range = (chan.frequency - wifi_chanbw / 2) .. " - " .. (chan.frequency + wifi_chanbw / 2) .. " MHz"
break
end
end
2022-01-19 06:42:31 -07:00
end
2023-12-06 13:06:34 -07:00
2022-11-24 12:07:22 -07:00
wifi_ssid = "none"
2022-01-19 06:42:31 -07:00
cursor:foreach("wireless", "wifi-iface",
function (section)
if section.network == "wifi" then
wifi_ssid = section.ssid
return false
end
end
)
end
local node_desc = cursor:get("system", "@system[0]", "description")
local lat_lon = "<strong>Location Not Available</strong>"
local lat = cursor:get("aredn", "@location[0]", "lat")
local lon = cursor:get("aredn", "@location[0]", "lon")
if lat and lon then
lat_lon = string.format("<center><strong>Location: </strong> %s %s</center>", lat, lon)
end
2022-10-11 17:46:39 -06:00
local host_total = 0
local host_nodes = 0
2022-10-14 14:31:09 -06:00
if nixio.fs.stat("/var/run/hosts_olsr.stable") then
for line in io.lines("/var/run/hosts_olsr.stable")
do
if line:match("^10%.") and not line:match("%smid%d+%.") then
host_total = host_total + 1
local host = line:match("^10%..+%sdtdlink%.")
if host then
host_nodes = host_nodes + 1
end
2022-01-19 06:42:31 -07:00
end
end
end
-- post data
if os.getenv("REQUEST_METHOD") == "POST" then
2023-12-17 17:20:41 -07:00
require('luci.http')
2022-12-22 13:22:49 -07:00
local request = luci.http.Request(nixio.getenv(),
2022-01-19 06:42:31 -07:00
function()
local v = io.read(1024)
if not v then
io.close()
end
return v
end
)
local css = request:formvalue("css")
if css and css:match("%.css$") and nixio.fs.stat("/www/" .. css) then
nixio.fs.unlink("/tmp/web/style.css")
nixio.fs.symlink("/www/" .. css, "/tmp/web/style.css")
end
end
-- generate page
http_header()
html.header(node .. " status", true)
html.print("<body><form method='post' action='/cgi-bin/status' enctype='multipart/form-data'>")
html.print("<center>")
html.alert_banner()
2022-06-07 20:30:27 -06:00
html.msg_banner()
2022-01-19 06:42:31 -07:00
-- page header
html.print("<h1><big>" .. node)
if tactical ~= "" then
html.print(" / " .. tactical)
end
html.print("</big></h1>")
html.print("<center>" .. lat_lon .. "</center>")
if node_desc then
html.print("<table id='node_description_display'><tr><td>" .. node_desc .. "</td></tr></table>")
end
html.print("<hr>")
2023-04-25 21:07:19 -06:00
-- Work out what config mode we're in. We use this to better instruct the
-- user about the next steps
local config_mode = nil
2022-01-19 06:42:31 -07:00
if config == "not set" then
2023-04-25 21:07:19 -06:00
config_mode = "ram"
2022-12-22 13:22:49 -07:00
for line in io.lines("/proc/mounts")
do
2023-04-28 12:01:58 -06:00
if line:match("overlay") or line:match("ext4") then
2023-04-25 21:07:19 -06:00
config_mode = "setup"
2022-12-22 13:22:49 -07:00
break
end
end
2023-04-25 21:07:19 -06:00
end
-- nav buttons
html.navbar_user("status", config_mode)
html.print("<input type=hidden name=reload value=reload>")
if config_mode then
html.print("<b><br><br>This node is not yet configured.<br>")
if config_mode == "setup" then
2022-12-22 13:22:49 -07:00
html.print("Go to the <a href='setup'>setup page</a> and set your node name and password.<br>")
html.print("Click Save Changes, <u>even if you didn't make any changes</u>, then the node will reboot.</b>")
else
2023-04-25 21:07:19 -06:00
html.print("<br><b>*** WARNING ***</b><p>AREDN is currently running a temporary image.<br>All configuration changes will be lost if you reboot.</p>")
html.print("Before doing anything else, please go<br>to the <a href='admin'>administration page</a> and upload firmware.<br>")
2022-12-22 13:22:49 -07:00
end
2022-01-19 06:42:31 -07:00
html.print("<br><br>")
2022-11-19 10:38:30 -07:00
html.print("<div style='max-width:540px; text-align:left'>")
2022-01-19 06:42:31 -07:00
html.print("<p>This device can be configured to either permit or prohibit known encrypted traffic on its RF link. It is up to the user to decide which is appropriate based on how it will be used and the license under which it will be operated. These rules vary by country, frequency, and intended use. You are encouraged to read and understand these rules before going further.</p>")
html.print("<p>This device is pre-configured with no restrictions as to the type of data being passed.</p>")
html.print("<p>Follow these steps if <span style=\"text-decoration: underline\">you wish to prohibit</span> known encrypted traffic on the RF link. These instructions will disappear, so copy them for your reference:</p>")
html.print("<p><ol>")
html.print("<li>Setup your node name and password as instructed at the top of this page</li>")
html.print("<li>After you Save Changes allow your node to reboot</li>")
html.print("<li>Return to the Node Status page and navigate to Setup > Administration</li>")
html.print("<li>Obtain the blockknownencryption package from the AREDN™ website OR refresh the Package list (node must be connected to the internet)</li>")
html.print("<li>Install the blockknownencryption package by uploading it or choosing it from the package drop-down list</li>")
html.print("<li>Wait until the package installs and then reboot your node</li>")
html.print("</ol></p>")
html.print("</div>")
end
-- status display
local col1 = {}
local col2 = {}
-- left column - network interface info
local ip = cursor:get("network", "wifi", "ipaddr")
2023-04-28 12:01:58 -06:00
local mask = cursor:get("network", "wifi", "netmask")
local cidr
if not ip or not mask then
col1[#col1 + 1] = "<th align=right><nobr>primary address:</nobr></th><td>none<br>"
2018-12-12 14:16:07 -07:00
else
2023-04-28 12:01:58 -06:00
cidr = netmask_to_cidr(mask)
if wifi_disabled then
col1[#col1 + 1] = "<th align=right><nobr>primary address:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>"
else
wifi_gw = get_default_gw("wifi")
2023-12-06 13:06:34 -07:00
col1[#col1 + 1] = "<th align=right><nobr>mesh address:</nobr><br><nobr>mesh gateway:</nobr><br><nobr>gateway node:</nobr><br>SSID:<br>channel:<br><nobr>channel width:</nobr><br>frequency range:</br></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wifi_gw .. "<br>" .. mesh_ip_to_hostnames(wifi_gw) .. "<br>" .. wifi_ssid .. "<br>" .. wifi_channel .. "<br>" .. wifi_chanbw .. " MHz<br>" .. wifi_freq_range .. "</td>"
2023-04-28 12:01:58 -06:00
end
2022-01-19 06:42:31 -07:00
end
ip = cursor:get("network", "lan", "ipaddr")
mask = cursor:get("network", "lan", "netmask")
local browser_ip
local remote_ip = os.getenv("REMOTE_ADDRESS")
if remote_ip then
remote_ip = remote_ip.match("::ffff:([%d%.]+)")
end
local hide_local = false
if remote_ip then
browser_ip = remote_ip
if not validate_same_subnet(remote_ip, ip, mask) then
hide_local = true
end
end
if ip:match("^10%.") or not hide_local then
cidr = netmask_to_cidr(mask)
2022-11-30 15:04:41 -07:00
local lan_wifi_ssid = "none"
2022-11-24 12:07:22 -07:00
cursor:foreach("wireless", "wifi-iface",
function (section)
2022-11-30 15:04:41 -07:00
if section.network == "lan" then
lan_wifi_ssid = section.ssid
2022-11-24 12:07:22 -07:00
return false
end
end
)
2022-11-30 15:04:41 -07:00
if lan_wifi_ssid ~= "none" then
col1[#col1 + 1] = "<th align=right><nobr>LAN address:</nobr><br><nobr>LAN AP SSID:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. lan_wifi_ssid .. "</td>"
else
col1[#col1 + 1] = "<th align=right><nobr>LAN address:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small></td>"
2022-08-02 17:57:44 -06:00
end
2022-11-30 15:04:41 -07:00
end
local wan_iface = aredn.hardware.get_iface_name("wan")
if wan_iface and not hide_local then
local ip, bcast, mask = aredn.hardware.get_interface_ip4(wan_iface)
if not ip then
2022-12-06 13:23:47 -07:00
if not wifi_gw or wifi_gw == "none" then
col1[#col1 + 1] = "<th align=right valign=top><nobr>WAN address:</nobr><br><nobr>default gateway:</nobr></th><td valign=top>none<br>none</td>"
else
col1[#col1 + 1] = "<th align=right valign=top><nobr>WAN address:</nobr><br><nobr>default gateway:</nobr><br><nobr>gateway node:</nobr></th><td valign=top>none<br>" .. wifi_gw .. "<br>" .. mesh_ip_to_hostnames(wifi_gw) .. "</td>"
end
2022-11-30 15:04:41 -07:00
else
local wprefix = ""
local wan_wifi_snr = "none"
local wan_wifi_ssid
if wan_iface:match("^wlan%d+$") then
wprefix = "wifi "
local s, n = get_wifi_signal(wan_iface)
if s ~= "none" and n ~= "none" then
wan_wifi_snr = math.abs(s - n)
end
cursor:foreach("wireless", "wifi-iface",
function (section)
if section.network == "wan" then
wan_wifi_ssid = section.ssid
return false
end
end
)
if not wan_wifi_ssid then -- if still nil then set default
wan_wifi_ssid = "none"
end
end
2022-11-24 12:07:22 -07:00
2022-01-19 06:42:31 -07:00
cidr = netmask_to_cidr(mask)
2022-11-30 15:04:41 -07:00
wan_gw = get_default_gw("wan")
if wprefix == "" then -- no wifi wan
if wan_gw:match("^10%.") or not hide_local then
if wan_gw:match("^10%.") then
col1[#col1 + 1] = "<th align=right><nobr><nobr>WAN address:</nobr><br>gateway:<br><nobr>gateway node:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "<br><nobr>" .. mesh_ip_to_hostnames(wan_gw) .. "</nobr></td>"
else
col1[#col1 + 1] = "<th align=right><nobr>WAN address:</nobr><br>default gateway:</th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "</td>"
end
end
else -- with wifi wan
2022-11-24 12:07:22 -07:00
if wan_wifi_ssid ~= "none" and wan_wifi_snr ~= "none" then
2022-11-30 15:04:41 -07:00
if wan_gw:match("^10%.") or not hide_local then
if wan_gw:match("^10%.") then
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br><nobr>SSID | SNR:</nobr><br>gateway:<br><nobr>gateway node:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br><nobr>" .. wan_wifi_ssid .. " | " .. wan_wifi_snr .. " dB<br>" .. wan_gw .. "<br><nobr>" .. mesh_ip_to_hostnames(wan_gw) .. "</nobr></td>"
else
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br><nobr>SSID | SNR:</nobr><br>default gateway:</th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br><nobr>" .. wan_wifi_ssid .. " | " .. wan_wifi_snr .. " dB<br>" .. wan_gw .. "</td>"
end
end
2022-11-24 12:07:22 -07:00
else
2022-11-30 15:04:41 -07:00
if wan_gw:match("^10%.") or not hide_local then
if wan_gw:match("^10%.") then
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br>gateway:<br><nobr>gateway node:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "<br><nobr>" .. mesh_ip_to_hostnames(wan_gw) .. "</nobr></td>"
else
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br>default gateway:</th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "</td>"
end
end
2022-11-24 12:07:22 -07:00
end
end
2022-01-19 06:42:31 -07:00
end
end
if browser_ip then
2022-11-19 10:38:30 -07:00
col1[#col1 + 1] = "<th align=right><nobr>your address:</nobr></th><td>" .. browser_ip .. "<br><nobr>" .. mesh_ip_to_hostnames(browser_ip) .. "</nobr></td>"
2022-01-19 06:42:31 -07:00
end
-- right column - system info
if config == "mesh" and not wifi_disabled then
2022-11-30 15:04:41 -07:00
col2[#col2 + 1] = "<th align=right valign=middle><nobr>signal|noise|SNR:</nobr></th><td valign=middle><nobr>"
2022-01-19 06:42:31 -07:00
local s, n = get_wifi_signal(wifi_iface)
2022-11-24 12:07:22 -07:00
if s == "none" then
2022-11-19 10:38:30 -07:00
col2[#col2] = col2[#col2] .. "no RF links"
2022-12-26 17:31:33 -07:00
col2[#col2] = col2[#col2] .. " <button type=button onClick='window.location=\"signal?realtime=1\"' title='Display continuous or archived signal strength on a chart'>Charts</button></nobr></td>"
2022-01-19 06:42:31 -07:00
else
2022-11-30 15:04:41 -07:00
col2[#col2] = col2[#col2] .. "<b>" .. s .. " | " .. n .. " | " .. math.abs(s - n) .. " dB</b>"
2022-11-19 10:38:30 -07:00
col2[#col2] = col2[#col2] .. " <button type=button onClick='window.location=\"signal?realtime=1\"' title='Display continuous or archived signal strength on a chart'>Charts</button></nobr></td>"
2022-01-19 06:42:31 -07:00
end
end
2023-12-06 13:30:23 -07:00
local azimuth = cursor:get("aredn", "@location[0]", "azimuth")
if tonumber(azimuth) then
azimuth = azimuth .. "°"
else
azimuth = nil
end
2023-12-17 22:33:27 -07:00
local elevation = cursor:get("aredn", "@location[0]", "elevation")
if tonumber(elevation) then
elevation = elevation .. "°"
else
elevation = nil
end
local height = cursor:get("aredn", "@location[0]", "height")
if tonumber(height) then
height = height .. "m"
else
height = nil
end
2023-12-06 13:30:23 -07:00
local antenna
if not wifi_disabled then
antenna = aredn.hardware.get_current_antenna(wifi_iface)
if antenna then
antenna = antenna.description
end
end
2023-12-17 22:33:27 -07:00
col2[#col2 + 1] = "<th align=right><nobr>firmware version:</nobr><br><nobr>model:</nobr><br>" ..
(antenna and "<br><nobr>antenna:</nobr>" or "") ..
(azimuth and "<br><nobr>azimuth:</nobr>" or "") ..
(elevation and "<br><nobr>elevation:</nobr>" or "") ..
(height and "<br><nobr>height:</nobr>" or "") ..
"</th><td>" ..
2023-12-17 23:02:05 -07:00
read_all("/etc/mesh-release") .. "<br>" .. (aredn.hardware.get_radio() or { name = "unknown" }).name .. "<br>" ..
(antenna and "<br>" .. antenna or "") ..
(azimuth and "<br>" .. azimuth or "") ..
(elevation and "<br>" .. elevation or "") ..
(height and "<br>" .. height or "") ..
2023-12-17 22:33:27 -07:00
"</td>"
2022-01-19 06:42:31 -07:00
local sysinfo = nixio.sysinfo()
local uptime = string.format("%d:%02d", math.floor(sysinfo.uptime / 3600) % 24, math.floor(sysinfo.uptime / 60) % 60)
if sysinfo.uptime >= 172800 then
uptime = math.floor(sysinfo.uptime / 86400) .. " days, " .. uptime
elseif sysinfo.uptime >= 86400 then
uptime = "1 day, " .. uptime
end
2022-11-19 10:38:30 -07:00
col2[#col2 + 1] = "<th align=right><nobr>system time:</nobr><br>uptime:</th><td>" .. os.date("%a %b %e %Y") .. " " .. os.date("%T %Z") .. "<br>" .. uptime .. "</td>"
2022-01-19 06:42:31 -07:00
local vfs = nixio.fs.statvfs("/overlay")
local fspace = vfs.bfree * vfs.bsize / 1024
if fspace < 100 then
fspace = "<blink><b>" .. fspace .. " KB</b></blink>"
else
fspace = fspace .. " KB"
end
2022-11-10 21:43:00 -07:00
local rspace = sysinfo.freeram / 1024
local mavail = get_memavail()
if rspace < mavail then
rspace = mavail
2022-01-19 06:42:31 -07:00
end
2022-11-10 21:43:00 -07:00
2022-01-19 06:42:31 -07:00
if rspace < 500 then
rspace = "<blink><b>" .. rspace .. " KB</b></blink>"
else
rspace = rspace .. " KB"
end
2022-06-04 21:45:00 -06:00
2022-11-19 10:38:30 -07:00
col2[#col2 + 1] = "<th align=right valign=top><nobr>load average:</nobr><br><nobr>available space:</nobr></th><td>" .. string.format("%.2f, %.2f, %.2f", sysinfo.loads[1], sysinfo.loads[2], sysinfo.loads[3]) .. "<br><nobr><em>flash</em> = " .. fspace .. "</nobr><br><nobr><em>memory</em> = " .. rspace .. "</nobr></td>"
col2[#col2 + 1] = "<th align=right valign=top><nobr>host entries:</nobr></th><td><b>" .. host_nodes .. "</b> nodes / <b>" .. host_total .. "</b> total devices</td>"
2022-01-19 06:42:31 -07:00
-- now print the tables
html.print("<br><br><table>")
html.print("<tr><td valign=top><table cellpadding=4>")
for i, line in ipairs(col1)
do
html.print("<tr>" .. line .. "</tr>")
end
html.print("</table></td><td valign=top><table cellpadding=4>")
for i, line in ipairs(col2)
do
html.print("<tr>" .. line .. "</tr>")
end
html.print("</table></td></tr></table>")
-- end
html.print("</center></form>")
html.footer()
html.print("</body></html>")