Being a Telnet Client






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 'net/telnet'

	webserver = Net::Telnet::new('Host' => 'www.oreilly.com',
	                             'Port' => 80,
	                             'Telnetmode' => false)

	size = 0
	webserver.cmd("GET / HTTP/1.1\nHost: www.oreilly.com\n") 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, it's 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 'net/telnet'

	bbs =  
Net::Telnet::new('Host' => 'bbs.example.com')

	puts bbs.waitfor(/What is your name\?/)
	# The Retro Telnet BBS
	# Where it's 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: it's 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't define a prompt at all, because there's no such thing in HTTP. For the type of problem we still solve with telnet, prompts are a pain.

What's the alternative? Instead of having cmd wait for a prompt, you can just have it wait for the server to go silent. Here's 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 'net/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\nHost: www.oreilly.com\n") 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. It's 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't happen.

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

	require 'net/telnet'
	adventure = Net::Telnet::new('Host' => 'games.example.com',
	                             'Port' => 23266,
	                             'Waittime' => 2.0,
	                             'Prompt' => /.*/)

	commands = ['no', '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

  • The Ruby documentation for the net/ telnet standard library

  • Recipe 14.10, "Being an SSH Client"

  • The telnet text adventure is based on the version of Colossal Cave hosted at forkexec.com; the site has lots of other games you can play via telnet (http://games.forkexec.com/)



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows