Being a Telnet Client

Problem

You want to connect to a telnet service or use telnet to get low-level access to some other kind of server.

Solution

Use the Net::Telnet module in the Ruby standard library.

The following code uses a Telnet object to simulate an HTTP client. It sends a raw HTTP request to the web server at http://www.oreilly.com. Every chunk of data received from the web server is passed into a code block, and its size is added to a tally. Eventually the web server stops sending data, and the telnet session times out.

require et/telnet webserver = Net::Telnet::new(Host => www.oreilly.com, Port => 80, Telnetmode => false) size = 0 webserver.cmd("GET / HTTP/1.1 Host: www.oreilly.com ") do |c| size += c.size puts "Read #{c.size} bytes; total #{size}" end # Read 1431 bytes; total 1431 # Read 1434 bytes; total 2865 # Read 1441 bytes; total 4306 # Read 1436 bytes; total 5742 # … # Read 1430 bytes; total 39901 # Read 2856 bytes; total 42757 # /usr/lib/ruby/1.8/net/telnet.rb:551:in waitfor: # timed out while waiting for more data (Timeout::Error)

Discussion

Telnet is a lightweight protocol devised for connecting to a generic service running on another computer. For a long time, the most commonly exposed service was a Unix shell: you would "telnet in" to a machine on the network, log in, and run shell commands on the other machine as though it were local.

Because telnet is an insecure protocol, its very rare now to use it for remote login. Everyone uses SSH for that instead (see the next recipe). Telnet is still useful for two things:

  1. As a diagnostic tool (as seen in the Solution). Telnet is very close to being a generic TCP protocol. If you know, say, HTTP, you can connect to an HTTP server with telnet, send it a raw HTTP request, and view the raw HTTP response.

  2. As a client to text-based services other than remote shells: mainly old-school entertainments like BBSes and MUDs.

Telnet objects implement a simple loop between you and some TCP server:

  1. You send a string to the server.

  2. You read data from the server a chunk at a time and process each chunk with a code block. The continues until a chunk of data contains text that matches a regular expression known as a prompt.

  3. In response to the prompt, you send another string to the server. The loop restarts.

In this example, I script a Telnet object to log me in to a telnet-accessible BBS. I wait for the BBS to send me strings that match certain prompts ("What is your name?" and "password:"), and I send back strings of my own in response to the prompts.

require et/telnet bbs = Net::Telnet::new(Host => bs.example.com) puts bbs.waitfor(/What is your name?/) # The Retro Telnet BBS # Where its been 1986 since 1993. # Dr. Phineas Goodbody, proprietor # # What is your name? (NEW for new user) bbs.cmd(String=>leonardr, Match=>/password:/) { |c| puts c } # Hello, leonardr. Please enter your password: bbs.cmd(my_password) { |c| puts c } # Welcome to the Retro Telnet BBS, leonardr. # Choose from the menu below: # …

The problem with this code is the "prompt" concept was designed for use with remote shells. A Unix shell shows you a prompt after every command you run. The prompt always ends in a dollar sign or some other character: its easy for telnet to pick out a shell prompt in the data stream. But no one uses telnet for remote shells anymore, so this is not very useful. The BBS software defines a different prompt for every interaction: one prompt for the name and a different one for the password. The web page grabber in the Solution doesn define a prompt at all, because theres no such thing in HTTP. For the type of problem we still solve with telnet, prompts are a pain.

Whats the alternative? Instead of having cmd wait for a prompt, you can just have it wait for the server to go silent. Heres an implementation of the web page grabber from the Solution, which stops reading from the server if it ever goes more than a tenth of a second without receiving any data:

require et/telnet webserver = Net::Telnet::new(Host => www.oreilly.com, Port => 80, Waittime => 0.1, Prompt => /.*/, Telnetmode => false) size = 0 webserver.cmd("GET / HTTP/1.1 Host: www.oreilly.com ") do |c| size += c.size puts "Read #{c.size} bytes; total #{size}" end

Here, the prompt matches any string at all. The end of every data chunk is potentially the "prompt" for the next command! But Telnet only acts on this if the server sends no more data in the next tenth of a second.

When you have Telnet communicate with a server this way, you never know for sure if you really got all the data. Its possible that the server just got really slow all of a sudden. If that happens, you may lose data or it may end up read by your next call to cmd. The best you can do is try to make your Waittime large enough so that this doesn happen.

In this example, I use Telnet to script a bit of a text adventure game thats been made available over the net. This example uses the same trick (a Prompt that matches anything) as the previous one, but Ive bumped up the Waittime because this server is slower than the oreilly.com web server:

require et/telnet adventure = Net::Telnet::new(Host => games.example.com, Port => 23266, Waittime => 2.0, Prompt => /.*/) commands = [ o, enter building, get lamp] # And so on… commands.each do |command| adventure.cmd(command) { |c| print c } end # Welcome to Adventure!! Would you like instructions? # no # # You are standing at the end of a road before a small brick building. # Around you is a forest. A small stream flows out of the building and # down a gully. # enter building # # You are inside a building, a well house for a large spring. # There are some keys on the ground here. # There is a shiny brass lamp nearby. # There is food here. # There is a bottle of water here. # # get lamp # OK

See Also

Категории