b438a0fd17
this is due to a rather odd (imo) decision in luasocket which says that the underlying socket (and therefore its file descriptor) isn't actually created until we call something that lets it work out whether ipv4 or ipv6
548 lines
20 KiB
Lua
548 lines
20 KiB
Lua
-- this code is based on src/tftp.lua from https://github.com/ldrumm/tufty
|
|
-- which is distributed under the MIT License
|
|
-- Starting upstream revision 3cb95c869e2fe74cc61ca303d88af6c5daad6734
|
|
--
|
|
-- Changes made since then are mostly to make it work better with
|
|
-- luasocket
|
|
|
|
--[[
|
|
This package provides intefaces for handling TFTP requests as specified by
|
|
rfc1350, rfc1782, rfc2347, rfc2348 and (partial support for rfc2349)
|
|
It should work on Standard PC servers as well as small home routers running
|
|
OpenWRT or similar.
|
|
]]
|
|
|
|
local tftp = {}
|
|
local TIMEOUT = 5
|
|
local ACK_RETRIES = 10
|
|
local TFTP_PORT = 69
|
|
local BLKSIZE = 512
|
|
|
|
local OP_RRQ = 1
|
|
local OP_WRQ = 2
|
|
local OP_DATA = 3
|
|
local OP_ACK = 4
|
|
local OP_ERR = 5
|
|
local OP_OACK = 6
|
|
local ACKSIZE = 4
|
|
|
|
local ERR_NOTFOUND = 1
|
|
local ERR_ACCESS = 2
|
|
local ERR_ALLOC = 3
|
|
local ERR_ILLEGAL_OP = 4
|
|
local ERR_UNKNOWN_ID = 5
|
|
local ERR_EXISTS = 6
|
|
local ERR_WHO = 7
|
|
|
|
--rfc2347 specifies the options extension.
|
|
--rfc2348 specifies the blksize extension
|
|
--rfc2349 specifies the timeout and tsize extensions
|
|
local SUPPORTED_OPTIONS = {blksize=true, timeout=true, tsize=true}
|
|
|
|
local luasocket = require("socket")
|
|
|
|
--Use penlight's prettyprinter if available
|
|
pcall(require, 'pl')
|
|
local log = pretty and pretty.dump or print
|
|
|
|
local time = (function()
|
|
return require("socket").gettime
|
|
end)()
|
|
|
|
local poll = (function()
|
|
--[[
|
|
``poll`` is expected to accept a table of sockets keyed by
|
|
backend file descriptor formatted as follows: {
|
|
[low level socket lib file descriptor]{
|
|
fd=(low level socket lib fd)
|
|
wantread=(bool)
|
|
wantwrite=(bool)
|
|
...arbitrary ignored extra fields
|
|
},
|
|
...
|
|
}
|
|
it returns a list of fds formatted as follows
|
|
{
|
|
[low level socket lib file descriptor]{
|
|
fd= [low level socket lib file descriptor],
|
|
readable=(bool),
|
|
writable=(bool)
|
|
}
|
|
...
|
|
}
|
|
]]
|
|
|
|
return function(fds, timeout)
|
|
local wantread = {}
|
|
local wantwrite = {}
|
|
for _, fd in pairs(fds) do
|
|
fd.readable=false
|
|
fd.writeable=false
|
|
if fd.wantwrite then wantwrite[#wantwrite + 1] = fd.fd end
|
|
if fd.wantread then wantread[#wantread + 1] = fd.fd end
|
|
end
|
|
local readable, writeable, timedout = luasocket.select(wantread, wantwrite, timeout)
|
|
if timedout then return nil end
|
|
local ready = {}
|
|
for _, fd in ipairs(readable) do
|
|
ready[fd] = ready[fd] or { fd = fd }
|
|
ready[fd].readable = true
|
|
end
|
|
for _, fd in ipairs(writeable) do
|
|
ready[fd] = ready[fd] or { fd = fd }
|
|
ready[fd].writeable = true
|
|
end
|
|
return ready
|
|
end
|
|
end)()
|
|
|
|
local function UDPSocket()
|
|
--[[ We want to support the basic functionality required for TFTP operation over
|
|
UDP.
|
|
This wraps only the required functionality and in no way represents a
|
|
complete UDP socket implementation.
|
|
see http://w3.impa.br/~diego/software/luasocket/udp.html for the luasocket UDP API
|
|
]]
|
|
|
|
return {
|
|
fd = luasocket.udp(),
|
|
bind = function(self, address, port)
|
|
return self.fd:setsockname(address, port)
|
|
end,
|
|
sendto = function(self, data, address, port)
|
|
return self.fd:sendto(data, address, port)
|
|
end,
|
|
recvfrom = function(self, length)
|
|
return self.fd:receivefrom(length)
|
|
end,
|
|
close = function(self)
|
|
return self.fd:close()
|
|
end
|
|
}
|
|
end
|
|
|
|
local function is_netascii(s)
|
|
--[[Check whether a string contains only characters from the RFC764 ascii
|
|
subset. see https://tools.ietf.org/html/rfc764#page-11
|
|
]]
|
|
local ctrls = {[0]=1, [10]=1, [13]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1}
|
|
for i=1, #s do
|
|
local byte = s:sub(i, i):byte()
|
|
if (byte < 31 and ctrls[byte] == nil) or byte > 127 then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function create_opcode(val)
|
|
if val < 0 or val > 2^16-1 then error("opcodes must fit into a 16bit integer") end
|
|
local high = math.floor(val / 256)
|
|
-- RFC1350 doesn't mention byte order. Assume network order (big-endian).
|
|
return string.char(high, val - (high * 256))
|
|
end
|
|
|
|
local function parse_opcode(packet)
|
|
local opcode = string.byte(packet:sub(2, 2)) --assume big endian
|
|
return ({"RRQ", "WRQ", "DATA", "ACK", "ERROR", "OACK"})[opcode]
|
|
end
|
|
|
|
function tftp:handle_RRQ(socket, host, port, source, options)
|
|
local blksize = options and tonumber(options.blksize) or BLKSIZE
|
|
local timeout_secs = options and tonumber(options.timeout) or TIMEOUT --rfc2349 timout option
|
|
local length = options and tonumber(options.length)
|
|
local tid = 1
|
|
local time = time
|
|
local started = time()
|
|
local err = self.ERROR
|
|
local done = false
|
|
local error, success = error, error -- to terminate the coroutine immediately, we raise an error
|
|
local yield = coroutine.yield
|
|
|
|
return coroutine.create(function()
|
|
if options then
|
|
--[[The handler coroutine should not start running until we know the client is ready.
|
|
This depends on whether the client has requested rfc2347 options and responded to an OACK.
|
|
Without the options extension request, the client can be responded to immediately.]]
|
|
local acked, timeout = false, time() + timeout_secs
|
|
assert(socket:sendto(self.OACK(options), host, port))
|
|
log(("sent OACK to %s:%d"):format(host, port))
|
|
repeat
|
|
yield(true, false)
|
|
local msg, port, host = socket:recvfrom(ACKSIZE)
|
|
if self.parse_ACK(msg) == 0 then acked = true end
|
|
timedout = time() > timeout
|
|
until acked or timedout
|
|
if timedout then error("Request timed out waiting for OACK response") end
|
|
end
|
|
log(("coroutine started on %s:%s/"):format(host, port))
|
|
while not done do
|
|
if tid >= 2^16 then
|
|
socket:sendto(err("File too big."), host, port)
|
|
error("File too big.")
|
|
end
|
|
local okay, continue, data = pcall(source, blksize)
|
|
if not okay then
|
|
packet = socket:sendto(err("An unknown error occurred"), host, port)
|
|
error("generator failure")
|
|
end
|
|
if data == nil and not continue then
|
|
done = true
|
|
end
|
|
if data == nil and continue then
|
|
--[[The generator ``source`` can be async and return `true, nil`
|
|
if no data is ready, but things are going well.
|
|
]]
|
|
yield(false, true)
|
|
end
|
|
socket:sendto(self.DATA(data, tid), host, port)
|
|
--[[ Now check for an ACK.
|
|
RFC1350 requires that for every packet sent, an ACK is received
|
|
before the next packet can be sent.
|
|
]]
|
|
local acked
|
|
local retried = 0
|
|
local timeout = time() + timeout_secs
|
|
local timedout = false
|
|
repeat
|
|
yield(true, false) -- we need to wait until the socket is readable again
|
|
local ack, ackhost, ackport = socket:recvfrom(ACKSIZE)
|
|
if ackhost ~= host or ackport ~= port or self.parse_ACK(ack) ~= tid then
|
|
--[[https://tools.ietf.org/html/rfc1350#page-5
|
|
"If a source TID does not match, the packet should be
|
|
discarded as erroneously sent from somewhere else.
|
|
An error packet should be sent to the source of the
|
|
incorrect packet, while not disturbing the transfer."
|
|
]]
|
|
socket:sendto(err(ERR_UNKNOWN_ID), ackhost, ackport)
|
|
yield(true, false)
|
|
else
|
|
acked = true
|
|
end
|
|
retried = retried + 1
|
|
timedout = time() > timeout
|
|
until acked or retried > ACK_RETRIES or timedout
|
|
if timedout or retried > ACK_RETRIES then
|
|
--There doesn't seem to be a standard error for timeout.
|
|
socket:sendto(err("Ack timeout"), host, port)
|
|
error("Timeout waiting for ACK")
|
|
end
|
|
--Okay, we've been acked in reasonable time.
|
|
tid = tid + 1
|
|
if done then success() end
|
|
yield(true, true)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function tftp:handle_WRQ(socket, host, port, sink)
|
|
error"Not Implemented"
|
|
end
|
|
|
|
function tftp:listen(rrq_generator_callback, wrq_generator_callback, hosts, port, logfile)
|
|
--[[--
|
|
Listen for TFTP requests on UDP ```bind``:`port`` (0.0.0.0:69 by default)
|
|
and get data from / send data to user-generated source/sink functions.
|
|
Data is generated/received by functions returned by the the user-supplied
|
|
``rrq_generator_callback``/``wrq_generator_callback`` factory functions.
|
|
For each resource requested, the generator function will be called
|
|
with three arguments:
|
|
- the requested resource as a C-style string (no embedded NUL chars)
|
|
- the ip address of the peer, as a dotted-quad string ("1.2.3.4")
|
|
- the port number of the peer
|
|
It should return a source or sink function that will be called repeatedly
|
|
until the data transfer is complete:
|
|
(SOURCE) will be called once for each block of data : it takes a
|
|
single argument of the requested data length in bytes
|
|
and returns the next block of data. It must return as follows
|
|
`true, data` on success
|
|
`true, nil` on wouldblock but should retry next round,
|
|
`false` on finished
|
|
(SINK) takes two arguments
|
|
``data`` to write
|
|
``done`` (truthy), whether all data has been received and backends can cleanup.
|
|
|
|
The (SOURCE) model therefore supports both blocking and non-blocking behaviour.
|
|
If the given function blocks, however, it will block the whole process as Lua
|
|
is single threaded. That may or may not be acceptable depending on your needs.
|
|
If the requested resource is invalid or other termination conditions are met,
|
|
(SOURCE) and (SINK) functions should raise an error.
|
|
@return This method never returns unless interrupted.
|
|
]]
|
|
|
|
local function create_handler(callbacks, request, requestsocket, host, port)
|
|
--[[ Given a parsed request, instantiate the generator function from the given callbacks,
|
|
and create a new coroutine to be called when the state of the handler's
|
|
new socket changes to available.
|
|
On success, returns a table of the form:
|
|
```{
|
|
handler=coroutine to call,
|
|
socket= new socket on a random port on which all new communication will happen,
|
|
fd=socket.fd as above fd
|
|
host=remote host,
|
|
port = remote port,
|
|
request = the original parsed request, including accepted options, if any.
|
|
}```
|
|
On error, responds to the client with an ERROR packet, and returns nil.
|
|
]]
|
|
local okay, generator, tsize = pcall(callbacks[request.opcode], request.filename, host, port)
|
|
if not okay then
|
|
requestsocket:sendto(self.ERROR(ERR_NOTFOUND), host, port)
|
|
return nil
|
|
else
|
|
if request.options then
|
|
request.options.tsize = request.options.tsize and tostring(tsize)
|
|
for k, v in pairs(request.options) do
|
|
if not SUPPORTED_OPTIONS[k] then request.options[k] = nil end
|
|
end
|
|
else
|
|
--RFC1350 requires WRQ requests to be responded to with a zero TID before transfer commences,
|
|
--but when responding to an options request, it is dropped.
|
|
if request.opcode == 'WRQ' then requestsocket:sendto(self.ACK(0), host, port) end
|
|
end
|
|
local handlersocket = UDPSocket()
|
|
handlersocket:bind("*", 0)
|
|
local handler = self['handle_' .. request.opcode](self, handlersocket, host, port, generator, request.options)
|
|
return {
|
|
handler=handler,
|
|
socket=handlersocket,
|
|
fd=handlersocket.fd,
|
|
host=host,
|
|
port=port,
|
|
request=request,
|
|
wantread=false,
|
|
wantwrite=true,
|
|
}
|
|
end
|
|
end
|
|
|
|
local function accept(socket)
|
|
--[[ Read an incoming request from ``socket``, parse, and ACK as appropriate.
|
|
If the request is invalid, responds to the client with error and returns `nil`
|
|
otherwise returns the parsed request.
|
|
]]
|
|
local msg, host, port = socket:recvfrom()
|
|
if msg ~= false then
|
|
local okay, xRQ = pcall(self.parse_XRQ, msg)
|
|
if not okay then
|
|
return nil
|
|
else
|
|
return host, port, xRQ
|
|
end
|
|
end
|
|
end
|
|
|
|
local socket = UDPSocket()
|
|
local user_generator_callbacks = {RRQ=rrq_generator_callback, WRQ=wrq_generator_callback}
|
|
local port = port or TFTP_PORT
|
|
local logfile = logfile or io.stderr
|
|
--listen on all given addresses, default to localhost if not given
|
|
for i, address in pairs((type(hosts) == 'table' and hosts) or (hosts ~= nil and{hosts}) or {'127.0.0.1'}) do
|
|
local ok, err = socket:bind(address, port)
|
|
if not ok then error(err .. " binding to " .. address .. ":" .. port) end
|
|
end
|
|
|
|
--[[The main event loop does two things:
|
|
1. Accepts new connections.
|
|
2. Handles events occurring on all sockets by dispatching to a handler coroutine.
|
|
3. Removes finished requests from the queue and destroys the sockets.
|
|
]]
|
|
local handlers = {[socket.fd]={fd=socket.fd, socket=socket, listener=true, wantread=true}}
|
|
while true do
|
|
ready_fds = poll(handlers)
|
|
do
|
|
local n = 0
|
|
for _ in pairs(ready_fds) do
|
|
n = n + 1
|
|
end
|
|
-- log(('There are %d sockets ready'):format(n))
|
|
end
|
|
for fd, status in pairs(ready_fds) do
|
|
-- pretty.dump(ready_fds)
|
|
-- log(('There are %d sockets ready'):format(#ready_fds))
|
|
ready = handlers[fd]
|
|
if ready.listener and status.readable then
|
|
--we've got a listener and should accept a new connection
|
|
local host, port, request = accept(ready.socket)
|
|
if host ~= nil then
|
|
log(("accepted new %s request - %s:%s/%s"):format(request.opcode, host, port, request.filename))
|
|
local handler = create_handler(
|
|
user_generator_callbacks,
|
|
request,
|
|
ready.socket,
|
|
host,
|
|
port
|
|
)
|
|
if handler then handlers[handler.socket.fd] = handler end
|
|
end
|
|
elseif (status.readable or status.writeable) and ready.handler then
|
|
--We've received an event on a socket associated with an existing handler coroutine.
|
|
local co_state = coroutine.status(ready.handler)
|
|
local okay, wantread, wantwrite
|
|
if co_state ~= 'dead' then
|
|
if (ready.wantread and status.readable) or (ready.wantwrite and status.writeable) then
|
|
okay, wantread, wantwrite = coroutine.resume(ready.handler)
|
|
ready.wantread = wantread
|
|
ready.wantwrite = wantwrite
|
|
end
|
|
end
|
|
if (not okay) or co_state == 'dead' then
|
|
--- the handler is finished; cleanup
|
|
ready.socket:close()
|
|
handlers[ready.fd] = nil
|
|
ready.fd = nil
|
|
ready = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
--[[ RRQ/ZRQ read/write request packets
|
|
https://tools.ietf.org/html/rfc1350
|
|
2 bytes string 1 byte string 1 byte
|
|
------------------------------------------------
|
|
| Opcode | Filename | 0 | Mode | 0 |
|
|
------------------------------------------------
|
|
Figure 5-1: RRQ/WRQ packet
|
|
]]
|
|
function tftp.RRQ(filename)
|
|
-- RFC1350:"The mail mode is obsolete and should not be implemented or used."
|
|
-- We don't support netascii, which leaves 'octet' mode only
|
|
return table.concat({create_opcode(OP_RRQ), filename, '\0', "octet", '\0'}, '')
|
|
end
|
|
|
|
function tftp.parse_XRQ(request)
|
|
local opcode = assert(parse_opcode(request), "Invalid opcode")
|
|
assert(({RRQ=true, XRQ=true})[opcode], "Not an xRQ")
|
|
assert(request:sub(#request) == '\0', "Invalid request: expected ASCII NUL terminated request")
|
|
|
|
local cstrings = {}
|
|
function zero_iter(s)
|
|
local pos = 1
|
|
return function()
|
|
--This is ugly. Lua 5.2 handles embedded NUL bytes in string.gmatch,
|
|
--but vanilla Lua5.1 doesn't match correctly and luajit can't seem to parse them
|
|
for i=pos, #s do
|
|
if s:byte(i) == 0 then
|
|
local sub = s:sub(pos, i-1)
|
|
pos = i+1
|
|
return sub
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for s in zero_iter(request:sub(3)) do
|
|
cstrings[#cstrings+1] = s
|
|
end
|
|
assert(#cstrings >= 2)
|
|
local filename = assert(is_netascii(cstrings[1]) and cstrings[1], "Requested filename must be netascii")
|
|
local mode = assert(({netascii='netascii', octet='octet'})[cstrings[2]])
|
|
local options
|
|
if #cstrings > 2 then
|
|
options = {}
|
|
assert(#cstrings % 2 == 0)
|
|
for i=3, #cstrings, 2 do
|
|
--[[ RFC1782, and 3247 require case insensitive comparisons.
|
|
We normalize them to lowercase with the consequence that
|
|
duplicate keys are replaced which are forbidden by the standard anyway.
|
|
]]
|
|
options[cstrings[i]:lower()] = cstrings[i+1]:lower()
|
|
end
|
|
end
|
|
|
|
return {opcode=opcode, filename=filename, mode=mode, options=options}
|
|
end
|
|
|
|
--[[ ACK functions
|
|
2 bytes 2 bytes
|
|
---------------------
|
|
| Opcode | Block # |
|
|
---------------------
|
|
Figure 5-3: ACK packet
|
|
]]
|
|
function tftp.parse_ACK(ack)
|
|
--get the sequence number from an ACK or raise a error if not valid
|
|
assert(#ack == ACKSIZE, "invalid ack")
|
|
assert(parse_opcode(ack) == 'ACK', "invalid ack")
|
|
|
|
-- extract the low and high order bytes and convert to an integer
|
|
local high, low = ack:byte(3, 4)
|
|
return (high * 256) + low
|
|
end
|
|
|
|
--[[
|
|
+-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
|
|
| opc | opt1 | 0 | value1 | 0 | optN | 0 | valueN | 0 |
|
|
+-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
|
|
]]
|
|
function tftp.OACK(options)
|
|
local stropts = {}
|
|
for k, v in pairs(options) do
|
|
assert(is_netascii(k))
|
|
stropts[#stropts+1] = k .. '\0' .. v .. '\0'
|
|
end
|
|
return create_opcode(OP_OACK) .. table.concat(stropts, '')
|
|
end
|
|
|
|
function tftp.ACK(tid)
|
|
return table.concat({create_opcode(OP_ACK), create_opcode(tid)}, '')
|
|
end
|
|
|
|
|
|
--[[ DATA functions
|
|
2 bytes 2 bytes n bytes
|
|
----------------------------------
|
|
| Opcode | Block # | Data |
|
|
----------------------------------
|
|
Figure 5-2: DATA packet
|
|
]]
|
|
function tftp.DATA(data, tid)
|
|
local opcode = create_opcode(OP_DATA)
|
|
local block = create_opcode(tid)
|
|
return table.concat({opcode, block, data}, '')
|
|
end
|
|
|
|
function tftp.parse_DATA(data)
|
|
assert(#data <= 512, "tftp data packets must be 512 bytes or less")
|
|
assert(parse_opcode(data) == OP_DATA, "Invalid opcode")
|
|
return {tid=parse_opcode(data:sub(3, 4)), data=data:sub(5)}
|
|
end
|
|
|
|
--[[ ERROR Functions
|
|
2 bytes 2 bytes string 1 byte
|
|
-----------------------------------------
|
|
| Opcode | ErrorCode | ErrMsg | 0 |
|
|
-----------------------------------------
|
|
Figure 5-4: ERROR packet
|
|
]]
|
|
function tftp.ERROR(err)
|
|
local defined_errors = {
|
|
--https://tools.ietf.org/html/rfc1350#page-10
|
|
[0] = type(err) == 'string' and err or "Not defined",
|
|
"File not found.",
|
|
"Access violation.",
|
|
"Disk full or allocation exceeded.",
|
|
"Illegal TFTP operation.",
|
|
"Unknown transfer ID.",
|
|
"File already exists.",
|
|
"No such user.",
|
|
}
|
|
|
|
local errno = type(err) == 'string' and 0 or err
|
|
return table.concat({
|
|
create_opcode(OP_ERR),
|
|
create_opcode(errno),
|
|
defined_errors[errno],
|
|
'\0'
|
|
}, '')
|
|
end
|
|
|
|
function tftp.parse_ERROR(err)
|
|
assert(parse_opcode(err) == OP_ERR)
|
|
local error_code = parse_opcode(err:sub(3, 4))
|
|
return {errcode=error_code, errmsg=err:sub(5, #err-1)}
|
|
end
|
|
|
|
return tftp
|