1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
|
--- Library implementing a minimal TFTP server
--
-- Currently only write-operations are supported so that script can trigger
-- TFTP transfers and receive the files and return them as result.
--
-- The library contains the following classes
-- * <code>Packet</code>
-- ** The <code>Packet</code> classes contain one class for each TFTP operation.
-- * <code>File</code>
-- ** The <code>File</code> class holds a received file including the name and contents
-- * <code>ConnHandler</code>
-- ** The <code>ConnHandler</code> class handles and processes incoming connections.
--
-- The following code snippet starts the TFTP server and waits for the file incoming.txt
-- to be uploaded for 10 seconds:
-- <code>
-- tftp.start()
-- local status, f = tftp.waitFile("incoming.txt", 10)
-- if ( status ) then return f:getContent() end
-- </code>
--
-- @author Patrik Karlsson <patrik@cqure.net>
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
--
-- version 0.2
--
-- 2011-01-22 - re-wrote library to use coroutines instead of new_thread code.
local coroutine = require "coroutine"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
_ENV = stdnse.module("tftp", stdnse.seeall)
threads, infiles, running = {}, {}, {}
state = "STOPPED"
srvthread = {}
-- All opcodes supported by TFTP
OpCode = {
RRQ = 1,
WRQ = 2,
DATA = 3,
ACK = 4,
ERROR = 5,
}
--- A minimal packet implementation
--
-- The current code only implements the ACK and ERROR packets
-- As the server is write-only the other packet types are not needed
Packet = {
-- Implements the ACK packet
ACK = {
new = function( self, block )
local o = {}
setmetatable(o, self)
self.__index = self
o.block = block
return o
end,
__tostring = function( self )
return string.pack(">I2I2", OpCode.ACK, self.block)
end,
},
-- Implements the error packet
ERROR = {
new = function( self, code, msg )
local o = {}
setmetatable(o, self)
self.__index = self
o.msg = msg
o.code = code
return o
end,
__tostring = function( self )
return string.pack(">I2I2z", OpCode.ERROR, self.code, self.msg)
end,
}
}
--- The File class holds files received by the TFTP server
File = {
--- Creates a new file object
--
-- @param filename string containing the filename
-- @param content string containing the file content
-- @return o new class instance
new = function(self, filename, content, sender)
local o = {}
setmetatable(o, self)
self.__index = self
o.name = filename
o.content = content
o.sender = sender
return o
end,
getContent = function(self) return self.content end,
setContent = function(self, content) self.content = content end,
getName = function(self) return self.name end,
setName = function(self, name) self.name = name end,
setSender = function(self, sender) self.sender = sender end,
getSender = function(self) return self.sender end,
}
-- The thread dispatcher is called by the start function once
local function dispatcher()
local last = os.time()
local f_condvar = nmap.condvar(infiles)
local s_condvar = nmap.condvar(state)
while(true) do
-- check if other scripts are active
local counter = 0
for t in pairs(running) do
counter = counter + 1
end
if ( counter == 0 ) then
state = "STOPPING"
s_condvar "broadcast"
end
if #threads == 0 then break end
for i, thread in ipairs(threads) do
local status, res = coroutine.resume(thread)
if ( not(res) ) then -- thread finished its task?
table.remove(threads, i)
break
end
end
-- Make sure to process waitFile atleast every 2 seconds
-- in case no files have arrived
if ( os.time() - last >= 2 ) then
last = os.time()
f_condvar "broadcast"
end
end
state = "STOPPED"
s_condvar "broadcast"
stdnse.debug1("Exiting _dispatcher")
end
-- Processes a new incoming file transfer
-- Currently only uploads are supported
--
-- @param host containing the hostname or ip of the initiating host
-- @param port containing the port of the initiating host
-- @param data string containing the initial data passed to the server
local function processConnection( host, port, data )
local op, pos = string.unpack(">I2", data)
local socket = nmap.new_socket("udp")
socket:set_timeout(1000)
local status, err = socket:connect(host, port)
if ( not(status) ) then return status, err end
socket:set_timeout(10)
-- If we get anything else than a write request, abort the connection
if ( OpCode.WRQ ~= op ) then
stdnse.debug1("Unsupported opcode")
socket:send( tostring(Packet.ERROR:new(0, "TFTP server has write-only support")))
end
local filename, enctype, pos = string.unpack("zz", data, pos)
status, err = socket:send( tostring( Packet.ACK:new(0) ) )
local blocks = {}
local lastread = os.time()
while( true ) do
local status, pdata = socket:receive()
if ( not(status) ) then
-- if we're here and haven't successfully read a packet for 5 seconds, abort
if ( os.time() - lastread > 5 ) then
coroutine.yield(false)
else
coroutine.yield(true)
end
else
-- record last time we had a successful read
lastread = os.time()
op, pos = string.unpack(">I2", pdata)
if ( OpCode.DATA ~= op ) then
stdnse.debug1("Expected a data packet, terminating TFTP transfer")
end
local block, data
block, data, pos = string.unpack(">I2 c" .. #pdata - 4, pdata, pos )
blocks[block] = data
-- First block was not 1
if ( #blocks == 0 ) then
socket:send( tostring(Packet.ERROR:new(0, "Did not receive block 1")))
break
end
-- for every fifth block check that we've received the preceding four
if ( ( #blocks % 5 ) == 0 ) then
for b = #blocks - 4, #blocks do
if ( not(blocks[b]) ) then
socket:send( tostring(Packet.ERROR:new(0, "Did not receive block " .. b)))
end
end
end
-- Ack the data block
status, err = socket:send( tostring(Packet.ACK:new(block)) )
if ( ( #blocks % 20 ) == 0 ) then
-- yield every 5th iteration so other threads may work
coroutine.yield(true)
end
-- If the data length was less than 512, this was our last block
if ( #data < 512 ) then
socket:close()
break
end
end
end
local filecontent = {}
-- Make sure we received all the blocks needed to proceed
for i=1, #blocks do
if ( not(blocks[i]) ) then
return false, ("Block #%d was missing in transfer")
end
filecontent[#filecontent+1] = blocks[i]
end
stdnse.debug1("Finished receiving file \"%s\"", filename)
-- Add anew file to the global infiles table
table.insert( infiles, File:new(filename, table.concat(filecontent), host) )
local condvar = nmap.condvar(infiles)
condvar "broadcast"
end
-- Waits for a connection from a client
local function waitForConnection()
local srvsock = nmap.new_socket("udp")
local status = srvsock:bind(nil, 69)
assert(status, "Failed to bind to TFTP server port")
srvsock:set_timeout(0)
while( state == "RUNNING" ) do
local status, data = srvsock:receive()
if ( not(status) ) then
coroutine.yield(true)
else
local status, _, _, rhost, rport = srvsock:get_info()
local x = coroutine.create( function() processConnection(rhost, rport, data) end )
table.insert( threads, x )
coroutine.yield(true)
end
end
end
--- Starts the TFTP server and creates a new thread handing over to the dispatcher
function start()
local disp = nil
local mutex = nmap.mutex("srvsocket")
-- register a running script
running[coroutine.running()] = true
mutex "lock"
if ( state == "STOPPED" ) then
srvthread = coroutine.running()
table.insert( threads, coroutine.create( waitForConnection ) )
stdnse.new_thread( dispatcher )
state = "RUNNING"
end
mutex "done"
end
local function waitLast()
-- The thread that started the server needs to wait here until the rest
-- of the scripts finish running. We know we are done once the state
-- shifts to STOPPED and we get a signal from the condvar in the
-- dispatcher
local s_condvar = nmap.condvar(state)
while( srvthread == coroutine.running() and state ~= "STOPPED" ) do
s_condvar "wait"
end
end
--- Waits for a file with a specific filename for at least the number of
-- seconds specified by the timeout parameter.
--
-- If this function is called from the thread that's running the server it will
-- wait until all the other threads have finished executing before returning.
--
-- @param filename string containing the name of the file to receive
-- @param timeout number containing the minimum number of seconds to wait
-- for the file to be received
-- @return status true on success false on failure
-- @return File instance on success, nil on failure
function waitFile( filename, timeout )
local condvar = nmap.condvar(infiles)
local t = os.time()
while(os.time() - t < timeout) do
for _, f in ipairs(infiles) do
if (f:getName() == filename) then
running[coroutine.running()] = nil
waitLast()
return true, f
end
end
condvar "wait"
end
-- de-register a running script
running[coroutine.running()] = nil
waitLast()
return false
end
return _ENV;
|