Ncat/Socket abstractions/vs --lua-exec
First ten weeks of this project I basically spent exploring --lua-exec feature. My main goal was to make this feature as powerful as possible, David's was to keep Ncat stable and coherent instead. These two kind of contradict each other, so while I could experiment with my ideas in feature branches, I knew that not everything will get merged. The final product had to be debugged and tested, documented and also ported to Windows. Moreover, it couldn't be messy.
My first ideas didn't satisfy the last requirement. --with was messy, the concept of running scripts that take over Ncat's main loop in a separate process and employ a lot of pipes (assuming you run a server or a lot of stacked scripts at least)... well, I have to admit it *was* messy. But I kept trying to defend it as a powerful feature (and partially because I was afraid of taking up a completely new model for the last six weeks of GSoC).
Recently David told me that ncat-lua-select branch won't probably be merged in and without the it, the "ncatin" concept wouldn't let me write a convenient filter script. Moreover, I'd still have to port script stacking to Windows, which kept giving me goosebumps everytime I thought about it. And even if I added select() and script stacking, it still wouldn't be able to do some things without more nasty kludges - for example, script wouldn't be able to talk to each other, so doing for example a chat server would be a pain. And you still would lack a way to make new connections while handling the client, so it'd be quite a lot of features to add before it gets as powerful as I'd like it to be. Which is why I decided to try a different idea that David kept pushing on two weeks ago (rightly so, as it turned out).
The idea was to implement sockets in a way similar to object-oriented programming. There's the "root"/"base" class that has methods that expose an interface to the low-level networking interface. So, when you run "recv", you actually perform a non-blocking read from the socket and receive the buffer. When you run "send", stuff gets sent and "close" terminates the connection. These can be ran automatically when some events happen, so if you sent something to Ncat's connection, you'll trigger a call of the recv() function and whatever it returns will be shown in the terminal. The second option is to run the methods manually, so for example when you receive some data and it starts with "hello", your recv() function can call something like "self:send('Yeah, hi!')". This obviously wouldn't be much fun if it wasn't for the fact that you can overload these methods with versions that process the buffers the way you want them to. So, for (not necessarily useful) example, you can write a filter that adds an X to the beginning of the buffer you're going to send and strips it from the messages you receive so it all seems to be done transparently. As a reminder, here's how it would look like with --lua-exec and ncatins+select, the "messy" version:
while true do selected = {io.stdin, io.ncatin} io.select(selected) --Is there any network input? if selected[0] == io.stdin or selected[1] == io.stdin then text = io.ncatin:read("*line") print(text:sub(2)) end --Is there any user input? if selected[0] == io.ncatin or selected[1] == io.ncatin then text = io.stdin:read("*line") io.ncatout:write('X'..text) end end
And here's how the script looks like using the new socket abstraction feature:
return { recv = function (self) line, err = self.super:recv() if line == nil then return line ,err end return line:sub(2) end, send = function(self, buf) return self.super:send('X'..buf) end, }
There are a few obvious differences here. The first is that there's no loop in the second script and it basically returns right after it's run. It's job isn't to take over the Ncat execution, it only explains how to install itself into the program. The core data structure is a table which contains functions that take "self" as its first parameter. Every connection has its own table, but if you want to share some of the state, you're free to use global variables. The values of the table don't have to be functions - you can store any other kind of values and they will be copied. Be aware, though, that the copy of the filter table will be shallow - if there's a table assigned to one of the filter table's keys, it will share references (perhaps we should issue a warning there?). While the first script clearly shows all the logic, the second definitely needs more comment. There's obviously something happening under the hood - for example, what's the "super" variable that we didn't specify?
I believe it will be best explained by example. The current command-line switch to turn on the feature (the code is in d33tah/ncat-lua-callbacks branch) is --load-lua-socket-file, or -L for short (though I'm pretty sure this *WILL* change later on). So, once you compiled ncat and are in the Ncat's source code directory, when you run:
./ncat --listen --load-lua-socket-file scripts/filters/numberer.lua --load-lua-socket-file scripts/filters/rot13.lua
During the initialization phase, a chain of filters will be created - on top of the chain there's the "root" (or "base", I will use them interchangeably in this e-mail) socket prototype with its recv, send, connect and close (and whatever I'll come up with later on) methods set to the default behavior. Then, down the chain, there's rot13.lua with "self.super" set to the base "class". And at the very bottom, we have numberer.lua with rot13.lua as its self.super. Note that scripts/filters/numberer.lua does has "recv" method only and neither "send" nor any other remaining functions are defined. This works because the non-defined methods are automatically set to a function that simply calls the parent. This way, even "return {}" is a valid script.
Once the chain is set up, Ncat either jumps into the connect or listen mode. In listen mode, the first interesting thing happens when we receive an incoming connection. In this case, we spawn a new shallow copy of the socket prototype, store it in connections[] global (at least for now, though it might get hidden later) variable with file descriptor being the index. This is important, because it means that if you want to broadcast a message to each connection, you can't use "for i=1,#connections" - instead, you're looking at "for k,_ in pairs(connections)".