NOTE: apparently not everyone is having great success. I have tried this out under cygwin and redhat enterprise linux 5, but if it doesn’t work for you please leave a comment and I’ll see what I can do.
The final script can be found here
The program. If you don’t know about netcat you should. It is one of the most useful command line tools out there when working with any network enabled program. You can use it to dump network data to files, files to network, tunnel udp over TCP, and more. I’m not actually going to mimic all that. I’m just going to try to support the following:
net_tool -c -h hostame -p port # should connect to hostname:port,
# forward data on STDIN along that connection,
# and forward any incoming data to STDOUT
net_tool -l -p port # should listen on port until a connection is established,
# forward data on STDIN along that connection,
#and forward any incoming data to STDOUT
So the first contestant is ruby an object-oriented scripting language built for elegance over efficiency. It isn’t strictly necessary, but I’m going to create NetTools class to wrap all the logic in:
class NetTools def run(args) ... end end tools = NetTools.new tools.run(ARGV)
Parsing Command Line Args
The first challenge is parsing the command line arguments, determining when there is an error, and providing a reasonable help message for the user. For this ruby provides the OptionParser class.
#!/usr/bin/ruby require 'optparse' require 'ostruct' class NetTools def run(args) parse_opts(args) end def parse_opts(args) @options = OpenStruct.new opts = OptionParser.new do |opts| opts.banner = "Usage: net_tool.irb [options]" opts.on("-c", "--connect", "Connect to a remote host") do @options.connection_type = :connect end opts.on("-l", "--listen", "Listen for a remote host to connect to this host") do @options.connection_type = :listen end opts.on("-r", "--remote-host HOSTNAME", String, "Specify the host to connect to") do |hostname| @options.hostname = hostname end opts.on("-p", "--port PORT", Integer, "Specify the TCP port") do |port| @options.port = port; end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end begin opts.parse!(args) rescue OptionParser::ParseError => err puts err.message puts opts exit end if (@options.connection_type == nil) puts "no connection type specified" puts opts exit end if (@options.port == nil) puts "no port specified" puts opts exit end if(@options.connection_type == :connect && @options.hostname == nil) puts "connection type connect requires a hostname" puts opts exit end end end tools = NetTools.new tools.run(ARGV)
So that’s pretty cute. It does most of the leg work for you. The only frustration I’d have it that it’s documentation is pretty weak when you want to specify anything beyond the basic options. Also there are at least two other ways of doing it so I’m not actually sure this is the standard mechanism.
Starting up the Socket
So once you’ve parsed your command line arguments the next step is to set up the socket. This actually is pretty easy. I added a new method
class NetTools ... def connect_socket() if(@options.connection_type == :connect) @socket = TCPSocket.open(@options.hostname, @options.port) else server = TCPServer.new(@options.port) server.listen( 1) @socket = server.accept end end ... end
The second part is to do some asynchronous IO. Basically we have to simultaneously forward the socket in to STDOUT and STDIN to socket out:
class NetTools ... def forward_data() while(true) begin while( (data = @socket.recv_nonblock(100)) != "") STDOUT.write(data); end #if no data is available rather than EAGAIN #that is EOF exit rescue Errno::EAGAIN end begin while( (data = STDIN.read_nonblock(100)) != "") @socket.write(data); end rescue Errno::EAGAIN rescue EOFError #STDIN uses EOFError exit end IO.select([@socket, STDIN], [@socket, STDIN], [@socket, STDIN]) end end .... end
Ruby seems to be a little uneven here in two things:
- Some conditions are exceptions and some are C-style return values. Here ruby is showing its ties to the underlying language I suppose but why is having nothing available an exception while EOF is a special case return value than depends on the type of socket object.
- It isn’t at all clear when navigating the documentation how the write method is related to the socket object or STDIN. It’s documented under IO and not referenced from any of the socket documentation
I seem to have this problem a lot with scripting languages, but I find that the basic script is pretty easy, but the details (like correctly handling stdin EOF, asyncronous IO, extra options in the command line option parser, etc) are not well documented enough to easily produce a robust application. I’ve spent about maybe 15 minutes of actual script writing which is good, but about 3 hours of digging through disparate documentation trying to find the details of what I need which is annoying. If I wanted to do that I’d just write the thing in C.
That being said this wasn’t too bad. I’ll run it past the local ruby guru to see what he has to say. I thought it was interesting that the most complicated part of the script was parsing the command line options though.