2 # -*- coding: iso-8859-1 -*-
5 # linksys.py -- program settings on a Linkys router
7 # This tool is designed to help you recover from the occasional episodes
8 # of catatonia that afflict Linksys boxes. It allows you to batch-program
9 # them rather than manually entering values to the Web interface. Commands
10 # are taken from the command line first, then standard input.
12 # The somewhat spotty coverage of status queries is because I only did the
13 # ones that were either (a) easy, or (b) necessary. If you want to know the
14 # status of the box, look at the web interface.
16 # This code has been tested against the following hardware:
19 # ---------- ---------------------
20 # BEFW11S4v2 1.44.2.1, Dec 20 2002
22 # The code is, of course, sensitive to changes in the names of CGI pages
25 # Note: to make the no-arguments form work, you'll need to have the following
26 # entry in your ~/.netrc file. If you have changed the router IP address or
27 # name/password, modify accordingly.
33 # By Eric S. Raymond, August April 2003. All rites reversed.
35 import sys, re, copy, curl, exceptions
37 class LinksysError(exceptions.Exception):
38 def __init__(self, *args):
42 months = 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec'
44 WAN_CONNECT_AUTO = '1'
45 WAN_CONNECT_STATIC = '2'
46 WAN_CONNECT_PPOE = '3'
48 WAN_CONNECT_PPTP = '5'
49 WAN_CONNECT_HEARTBEAT = '6'
51 # Substrings to check for on each page load.
52 # This may enable us to detect when a firmware change has hosed us.
54 "": "basic setup functions",
55 "Passwd.htm": "For security reasons,",
56 "DHCP.html": "You can configure the router to act as a DHCP",
57 "Log.html": "There are some log settings and lists in this page.",
58 "Forward.htm":"Port forwarding can be used to set up public services",
63 self.host = "http://192.168.1.1"
64 self.verbosity = False
67 def set_verbosity(self, flag):
70 # This is not a performance hack -- we need the page cache to do
71 # sanity checks at configure time.
72 def cache_load(self, page):
73 if page not in self.pagecache:
74 fetch = curl.Curl(self.host)
75 fetch.set_verbosity(self.verbosity)
77 self.pagecache[page] = fetch.body()
78 if fetch.answered("401"):
79 raise LinksysError("authorization failure.", True)
80 elif not fetch.answered(LinksysSession.check_strings[page]):
81 del self.pagecache[page]
82 raise LinksysError("check string for page %s missing!" % os.path.join(self.host, page), False)
84 def cache_flush(self):
88 def screen_scrape(self, page, template):
90 match = re.compile(template).search(self.pagecache[page])
92 result = match.group(1)
96 def get_MAC_address(self, page, prefix):
97 return self.screen_scrape("", prefix+r":[^M]*\(MAC Address: *([^)]*)")
98 def set_flag(page, flag, value):
100 self.actions.append(page, flag, "1")
102 self.actions.append(page, flag, "0")
103 def set_IP_address(self, page, cgi, role, ip):
105 for octet in ip.split("."):
106 self.actions.append(("", "F1", role + `ind+1`, octet))
109 # Scrape configuration data off the main page
110 def get_firmware_version(self):
111 # This is fragile. There is no distinguishing tag before the firmware
112 # version, so we have to key off the pattern of the version number.
113 # Our model is ">1.44.2.1, Dec 20 2002<"
114 return self.screen_scrape("", ">([0-9.v]*, (" + \
115 LinksysSession.months + ")[^<]*)<", )
116 def get_LAN_MAC(self):
117 return self.get_MAC_address("", r"LAN IP Address")
118 def get_Wireless_MAC(self):
119 return self.get_MAC_address("", r"Wireless")
120 def get_WAN_MAC(self):
121 return self.get_MAC_address("", r"WAN Connection Type")
123 # Set configuration data on the main page
124 def set_host_name(self, name):
125 self.actions.append(("", "hostName", name))
126 def set_domain_name(self, name):
127 self.actions.append(("", "DomainName", name))
128 def set_LAN_IP(self, ip):
129 self.set_IP_address("", "ipAddr", ip)
130 def set_LAN_netmask(self, ip):
131 if not ip.startswith("255.255.255."):
133 lastquad = ip.split(".")[-1]
134 if lastquad not in ("0", "128", "192", "240", "252"):
136 self.actions.append("", "netMask", lastquad)
137 def set_wireless(self, flag):
138 self.set_flag("", "wirelessStatus")
139 def set_SSID(self, ssid):
140 self.actions.append(("", "wirelessESSID", ssid))
141 def set_SSID_broadcast(self, flag):
142 self.set_flag("", "broadcastSSID")
143 def set_channel(self, channel):
144 self.actions.append(("", "wirelessChannel", channel))
145 def set_WEP(self, flag):
146 self.set_flag("", "WepType")
147 # FIXME: Add support for setting WEP keys
148 def set_connection_type(self, type):
149 self.actions.append(("", "WANConnectionType", type))
150 def set_WAN_IP(self, ip):
151 self.set_IP_address("", "aliasIP", ip)
152 def set_WAN_netmask(self, ip):
153 self.set_IP_address("", "aliasMaskIP", ip)
154 def set_WAN_gateway_address(self, ip):
155 self.set_IP_address("", "routerIP", ip)
156 def set_DNS_server(self, index, ip):
157 self.set_IP_address("", "dns" + "ABC"[index], ip)
159 # Set configuration data on the password page
160 def set_password(self, str):
161 self.actions.append("Passwd.htm","sysPasswd", str)
162 self.actions.append("Passwd.htm","sysPasswdConfirm", str)
163 def set_UPnP(self, flag):
164 self.set_flag("Passwd.htm", "UPnP_Work")
166 self.actions.append("Passwd.htm", "FactoryDefaults")
169 def set_DHCP(self, flag):
171 self.actions.append("DHCP.htm","dhcpStatus","Enable")
173 self.actions.append("DHCP.htm","dhcpStatus","Disable")
174 def set_DHCP_starting_IP(self, val):
175 self.actions.append("DHCP.htm","dhcpS4", str(val))
176 def set_DHCP_users(self, val):
177 self.actions.append("DHCP.htm","dhcpLen", str(val))
178 def set_DHCP_lease_time(self, val):
179 self.actions.append("DHCP.htm","leaseTime", str(val))
180 def set_DHCP_DNS_server(self, index, ip):
181 self.set_IP_address("DHCP.htm", "dns" + "ABC"[index], ip)
182 # FIXME: add support for setting WINS key
185 def set_logging(self, flag):
187 self.actions.append("Log.htm", "rLog", "Enable")
189 self.actions.append("Log.htm", "rLog", "Disable")
190 def set_log_address(self, val):
191 self.actions.append("DHCP.htm","trapAddr3", str(val))
193 # The AOL parental control flag is not supported by design.
195 # FIXME: add Filters and other advanced features
198 "Write configuration changes to the Linksys."
202 for (page, field, value) in self.actions:
203 self.cache_load(page)
204 if self.pagecache[page].find(field) == -1:
205 print >>sys.stderr, "linksys: field %s not found where expected in page %s!" % (field, os.path.join(self.host, page))
208 fields.append((field, value))
209 # Clearing the action list before fieldsping is deliberate.
210 # Otherwise we could get permanently wedged by a 401.
212 transaction = curl.Curl(self.host)
213 transaction.set_verbosity(self.verbosity)
214 transaction.get("Gozila.cgi", tuple(fields))
217 if __name__ == "__main__":
220 class LinksysInterpreter(cmd.Cmd):
221 """Interpret commands to perform LinkSys programming actions."""
223 self.session = LinksysSession()
226 print "Type ? or `help' for help."
227 self.prompt = self.session.host + ": "
232 def flag_command(self, func):
233 if line.strip() in ("on", "enable", "yes"):
235 elif line.strip() in ("off", "disable", "no"):
238 print >>sys.stderr, "linksys: unknown switch value"
241 def do_connect(self, line):
242 newhost = line.strip()
244 self.session.host = newhost
245 self.session.cache_flush()
246 self.prompt = self.session.host + ": "
248 print self.session.host
250 def help_connect(self):
251 print "Usage: connect [<hostname-or-IP>]"
252 print "Connect to a Linksys by name or IP address."
253 print "If no argument is given, print the current host."
255 def do_status(self, line):
256 self.session.cache_load("")
257 if "" in self.session.pagecache:
258 print "Firmware:", self.session.get_firmware_version()
259 print "LAN MAC:", self.session.get_LAN_MAC()
260 print "Wireless MAC:", self.session.get_Wireless_MAC()
261 print "WAN MAC:", self.session.get_WAN_MAC()
264 def help_status(self):
265 print "Usage: status"
266 print "The status command shows the status of the Linksys."
267 print "It is mainly useful as a sanity check to make sure"
268 print "the box is responding correctly."
270 def do_verbose(self, line):
271 self.flag_command(self.session.set_verbosity)
272 def help_verbose(self):
273 print "Usage: verbose {on|off|enable|disable|yes|no}"
274 print "Enables display of HTTP requests."
276 def do_host(self, line):
277 self.session.set_host_name(line)
280 print "Usage: host <hostname>"
281 print "Sets the Host field to be queried by the ISP."
283 def do_domain(self, line):
284 print "Usage: host <domainname>"
285 self.session.set_domain_name(line)
287 def help_domain(self):
288 print "Sets the Domain field to be queried by the ISP."
290 def do_lan_address(self, line):
291 self.session.set_LAN_IP(line)
293 def help_lan_address(self):
294 print "Usage: lan_address <ip-address>"
295 print "Sets the LAN IP address."
297 def do_lan_netmask(self, line):
298 self.session.set_LAN_netmask(line)
300 def help_lan_netmask(self):
301 print "Usage: lan_netmask <ip-mask>"
302 print "Sets the LAN subnetwork mask."
304 def do_wireless(self, line):
305 self.flag_command(self.session.set_wireless)
307 def help_wireless(self):
308 print "Usage: wireless {on|off|enable|disable|yes|no}"
309 print "Switch to enable or disable wireless features."
311 def do_ssid(self, line):
312 self.session.set_SSID(line)
315 print "Usage: ssid <string>"
316 print "Sets the SSID used to control wireless access."
318 def do_ssid_broadcast(self, line):
319 self.flag_command(self.session.set_SSID_broadcast)
321 def help_ssid_broadcast(self):
322 print "Usage: ssid_broadcast {on|off|enable|disable|yes|no}"
323 print "Switch to enable or disable SSID broadcast."
325 def do_channel(self, line):
326 self.session.set_channel(line)
328 def help_channel(self):
329 print "Usage: channel <number>"
330 print "Sets the wireless channel."
332 def do_wep(self, line):
333 self.flag_command(self.session.set_WEP)
336 print "Usage: wep {on|off|enable|disable|yes|no}"
337 print "Switch to enable or disable WEP security."
339 def do_wan_type(self, line):
341 type=eval("LinksysSession.WAN_CONNECT_"+line.strip().upper())
342 self.session.set_connection_type(type)
344 print >>sys.stderr, "linksys: unknown connection type."
346 def help_wan_type(self):
347 print "Usage: wan_type {auto|static|ppoe|ras|pptp|heartbeat}"
348 print "Set the WAN connection type."
350 def do_wan_address(self, line):
351 self.session.set_WAN_IP(line)
353 def help_wan_address(self):
354 print "Usage: wan_address <ip-address>"
355 print "Sets the WAN IP address."
357 def do_wan_netmask(self, line):
358 self.session.set_WAN_netmask(line)
360 def help_wan_netmask(self):
361 print "Usage: wan_netmask <ip-mask>"
362 print "Sets the WAN subnetwork mask."
364 def do_wan_gateway(self, line):
365 self.session.set_WAN_gateway(line)
367 def help_wan_gateway(self):
368 print "Usage: wan_gateway <ip-address>"
369 print "Sets the LAN subnetwork mask."
371 def do_dns(self, line):
372 (index, address) = line.split()
373 if index in ("1", "2", "3"):
374 self.session.set_DNS_server(eval(index), address)
376 print >>sys.stderr, "linksys: server index out of bounds."
379 print "Usage: dns {1|2|3} <ip-mask>"
380 print "Sets a primary, secondary, or tertiary DNS server address."
382 def do_password(self, line):
383 self.session.set_password(line)
385 def help_password(self):
386 print "Usage: password <string>"
387 print "Sets the router password."
389 def do_upnp(self, line):
390 self.flag_command(self.session.set_UPnP)
393 print "Usage: upnp {on|off|enable|disable|yes|no}"
394 print "Switch to enable or disable Universal Plug and Play."
396 def do_reset(self, line):
398 def help_reset(self):
400 print "Reset Linksys settings to factory defaults."
402 def do_dhcp(self, line):
403 self.flag_command(self.session.set_DHCP)
405 print "Usage: dhcp {on|off|enable|disable|yes|no}"
406 print "Switch to enable or disable DHCP features."
408 def do_dhcp_start(self, line):
409 self.session.set_DHCP_starting_IP(line)
410 def help_dhcp_start(self):
411 print "Usage: dhcp_start <number>"
412 print "Set the start address of the DHCP pool."
414 def do_dhcp_users(self, line):
415 self.session.set_DHCP_users(line)
416 def help_dhcp_users(self):
417 print "Usage: dhcp_users <number>"
418 print "Set number of address slots to allocate in the DHCP pool."
420 def do_dhcp_lease(self, line):
421 self.session.set_DHCP_lease(line)
422 def help_dhcp_lease(self):
423 print "Usage: dhcp_lease <number>"
424 print "Set number of address slots to allocate in the DHCP pool."
426 def do_dhcp_dns(self, line):
427 (index, address) = line.split()
428 if index in ("1", "2", "3"):
429 self.session.set_DHCP_DNS_server(eval(index), address)
431 print >>sys.stderr, "linksys: server index out of bounds."
433 def help_dhcp_dns(self):
434 print "Usage: dhcp_dns {1|2|3} <ip-mask>"
435 print "Sets primary, secondary, or tertiary DNS server address."
437 def do_logging(self, line):
438 self.flag_command(self.session.set_logging)
439 def help_logging(self):
440 print "Usage: logging {on|off|enable|disable|yes|no}"
441 print "Switch to enable or disable session logging."
443 def do_log_address(self, line):
444 self.session.set_Log_address(line)
445 def help_log_address(self):
446 print "Usage: log_address <number>"
447 print "Set the last quad of the address to which to log."
449 def do_configure(self, line):
450 self.session.configure()
452 def help_configure(self):
453 print "Usage: configure"
454 print "Writes the configuration to the Linksys."
456 def do_cache(self, line):
457 print self.session.pagecache
458 def help_cache(self):
460 print "Display the page cache."
462 def do_quit(self, line):
464 def help_quit(self, line):
465 print "The quit command ends your linksys session without"
466 print "writing configuration changes to the Linksys."
467 def do_EOF(self, line):
469 self.session.configure()
472 print "The EOF command writes the configuration to the linksys"
473 print "and ends your session."
475 def default(self, line):
476 """Pass the command through to be executed by the shell."""
481 print "On-line help is available through this command."
482 print "? is a convenience alias for help."
484 def help_introduction(self):
487 This program supports changing the settings on Linksys blue-box routers. This
488 capability may come in handy when they freeze up and have to be reset. Though
489 it can be used interactively (and will command-prompt when standard input is a
490 terminal) it is really designed to be used in batch mode. Commands are taken
491 from the command line first, then standard input.
493 By default, it is assumed that the Linksys is at http://192.168.1.1, the
494 default LAN address. You can connect to a different address or IP with the
495 'connect' command. Note that your .netrc must contain correct user/password
496 credentials for the router. The entry corresponding to the defaults is:
502 Most commands queue up changes but don't actually send them to the Linksys.
503 You can force pending changes to be written with 'configure'. Otherwise, they
504 will be shipped to the Linksys at the end of session (e.g. when the program
505 running in batch mode encounters end-of-file or you type a control-D). If you
506 end the session with `quit', pending changes will be discarded.
508 For more help, read the topics 'wan', 'lan', and 'wireless'."""
512 The `lan_address' and `lan_netmask' commands let you set the IP location of
513 the Linksys on your LAN, or inside. Normally you'll want to leave these
518 The WAN commands become significant if you are using the BEFSR41 or any of
519 the other Linksys boxes designed as DSL or cable-modem gateways. You will
520 need to use `wan_type' to declare how you expect to get your address.
522 If your ISP has issued you a static address, you'll need to use the
523 `wan_address', `wan_netmask', and `wan_gateway' commands to set the address
524 of the router as seen from the WAN, the outside. In this case you will also
525 need to use the `dns' command to declare which remote servers your DNS
526 requests should be forwarded to.
528 Some ISPs may require you to set host and domain for use with dynamic-address
531 def help_wireless(self):
533 The channel, ssid, ssid_broadcast, wep, and wireless commands control
536 def help_switches(self):
537 print "Switches may be turned on with 'on', 'enable', or 'yes'."
538 print "Switches may be turned off with 'off', 'disable', or 'no'."
539 print "Switch commands include: wireless, ssid_broadcast."
541 def help_addresses(self):
542 print "An address argument must be a valid IP address;"
543 print "four decimal numbers separated by dots, each "
544 print "between 0 and 255."
549 interpreter = LinksysInterpreter()
550 for arg in sys.argv[1:]:
551 interpreter.onecmd(arg)
555 interpreter.cmdloop()
557 except LinksysError, (message, fatal):
558 print "linksys:", message
560 # The following sets edit modes for GNU EMACS