Merge remote-tracking branch 'origin/pycurl' into planetlab-4_0-branch
[plcapi.git] / pycurl / examples / linksys.py
1 #! /usr/bin/env python
2 # -*- coding: iso-8859-1 -*-
3 # vi:ts=4:et
4 #
5 # linksys.py -- program settings on a Linkys router
6 #
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.
11 #
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.
15 #
16 # This code has been tested against the following hardware:
17 #
18 #   Hardware    Firmware
19 #   ----------  ---------------------
20 #   BEFW11S4v2  1.44.2.1, Dec 20 2002
21 #
22 # The code is, of course, sensitive to changes in the names of CGI pages
23 # and field names.
24 #
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.
28 #
29 # machine 192.168.1.1
30 #   login ""
31 #   password admin
32 #
33 # By Eric S. Raymond, August April 2003.  All rites reversed.
34
35 import sys, re, copy, curl, exceptions
36
37 class LinksysError(exceptions.Exception):
38     def __init__(self, *args):
39         self.args = args
40
41 class LinksysSession:
42     months = 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec'
43
44     WAN_CONNECT_AUTO = '1'
45     WAN_CONNECT_STATIC = '2'
46     WAN_CONNECT_PPOE = '3'
47     WAN_CONNECT_RAS = '4'
48     WAN_CONNECT_PPTP = '5'
49     WAN_CONNECT_HEARTBEAT = '6'
50
51     # Substrings to check for on each page load.
52     # This may enable us to detect when a firmware change has hosed us.
53     check_strings = {
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",
59         }
60
61     def __init__(self):
62         self.actions = []
63         self.host = "http://192.168.1.1"
64         self.verbosity = False
65         self.pagecache = {}
66
67     def set_verbosity(self, flag):
68         self.verbosity = flag
69
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)
76             fetch.get(page)
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)
83             fetch.close()
84     def cache_flush(self):
85         self.pagecache = {}
86
87     # Primitives
88     def screen_scrape(self, page, template):
89         self.cache_load(page)
90         match = re.compile(template).search(self.pagecache[page])
91         if match:
92             result = match.group(1)
93         else:
94             result = None
95         return result
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):
99         if value:
100             self.actions.append(page, flag, "1")
101         else:
102             self.actions.append(page, flag, "0")
103     def set_IP_address(self, page, cgi, role, ip):
104         ind = 0
105         for octet in ip.split("."):
106             self.actions.append(("", "F1", role + `ind+1`, octet))
107             ind += 1
108
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")
122
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."):
132             raise ValueError
133         lastquad = ip.split(".")[-1]
134         if lastquad not in ("0", "128", "192", "240", "252"):
135             raise ValueError
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)
158
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")
165     def reset(self):
166         self.actions.append("Passwd.htm", "FactoryDefaults")
167
168     # DHCP features
169     def set_DHCP(self, flag):
170         if flag:
171             self.actions.append("DHCP.htm","dhcpStatus","Enable")
172         else:
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
183
184     # Logging features
185     def set_logging(self, flag):
186         if flag:
187             self.actions.append("Log.htm", "rLog", "Enable")
188         else:
189             self.actions.append("Log.htm", "rLog", "Disable")
190     def set_log_address(self, val):
191         self.actions.append("DHCP.htm","trapAddr3", str(val))
192
193     # The AOL parental control flag is not supported by design.
194
195     # FIXME: add Filters and other advanced features
196
197     def configure(self):
198         "Write configuration changes to the Linksys."
199         if self.actions:
200             fields = []
201             self.cache_flush()
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))
206                     continue
207                 else:
208                     fields.append((field, value))
209             # Clearing the action list before fieldsping is deliberate.
210             # Otherwise we could get permanently wedged by a 401.
211             self.actions = []
212             transaction = curl.Curl(self.host)
213             transaction.set_verbosity(self.verbosity)
214             transaction.get("Gozila.cgi", tuple(fields))
215             transaction.close()
216
217 if __name__ == "__main__":
218     import os, cmd
219
220     class LinksysInterpreter(cmd.Cmd):
221         """Interpret commands to perform LinkSys programming actions."""
222         def __init__(self):
223             self.session = LinksysSession()
224             if os.isatty(0):
225                 import readline
226                 print "Type ? or `help' for help."
227                 self.prompt = self.session.host + ": "
228             else:
229                 self.prompt = ""
230                 print "Bar1"
231
232         def flag_command(self, func):
233             if line.strip() in ("on", "enable", "yes"):
234                 func(True)
235             elif line.strip() in ("off", "disable", "no"):
236                 func(False)
237             else:
238                 print >>sys.stderr, "linksys: unknown switch value"
239             return 0
240
241         def do_connect(self, line):
242             newhost = line.strip()
243             if newhost:
244                 self.session.host = newhost
245                 self.session.cache_flush()
246                 self.prompt = self.session.host + ": "
247             else:
248                 print self.session.host
249             return 0
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."
254
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()
262                 print "."
263             return 0
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."
269
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."
275
276         def do_host(self, line):
277             self.session.set_host_name(line)
278             return 0
279         def help_host(self):
280             print "Usage: host <hostname>"
281             print "Sets the Host field to be queried by the ISP."
282
283         def do_domain(self, line):
284             print "Usage: host <domainname>"
285             self.session.set_domain_name(line)
286             return 0
287         def help_domain(self):
288             print "Sets the Domain field to be queried by the ISP."
289
290         def do_lan_address(self, line):
291             self.session.set_LAN_IP(line)
292             return 0
293         def help_lan_address(self):
294             print "Usage: lan_address <ip-address>"
295             print "Sets the LAN IP address."
296
297         def do_lan_netmask(self, line):
298             self.session.set_LAN_netmask(line)
299             return 0
300         def help_lan_netmask(self):
301             print "Usage: lan_netmask <ip-mask>"
302             print "Sets the LAN subnetwork mask."
303
304         def do_wireless(self, line):
305             self.flag_command(self.session.set_wireless)
306             return 0
307         def help_wireless(self):
308             print "Usage: wireless {on|off|enable|disable|yes|no}"
309             print "Switch to enable or disable wireless features."
310
311         def do_ssid(self, line):
312             self.session.set_SSID(line)
313             return 0
314         def help_ssid(self):
315             print "Usage: ssid <string>"
316             print "Sets the SSID used to control wireless access."
317
318         def do_ssid_broadcast(self, line):
319             self.flag_command(self.session.set_SSID_broadcast)
320             return 0
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."
324
325         def do_channel(self, line):
326             self.session.set_channel(line)
327             return 0
328         def help_channel(self):
329             print "Usage: channel <number>"
330             print "Sets the wireless channel."
331
332         def do_wep(self, line):
333             self.flag_command(self.session.set_WEP)
334             return 0
335         def help_wep(self):
336             print "Usage: wep {on|off|enable|disable|yes|no}"
337             print "Switch to enable or disable WEP security."
338
339         def do_wan_type(self, line):
340             try:
341                 type=eval("LinksysSession.WAN_CONNECT_"+line.strip().upper())
342                 self.session.set_connection_type(type)
343             except ValueError:
344                 print >>sys.stderr, "linksys: unknown connection type."
345             return 0
346         def help_wan_type(self):
347             print "Usage: wan_type {auto|static|ppoe|ras|pptp|heartbeat}"
348             print "Set the WAN connection type."
349
350         def do_wan_address(self, line):
351             self.session.set_WAN_IP(line)
352             return 0
353         def help_wan_address(self):
354             print "Usage: wan_address <ip-address>"
355             print "Sets the WAN IP address."
356
357         def do_wan_netmask(self, line):
358             self.session.set_WAN_netmask(line)
359             return 0
360         def help_wan_netmask(self):
361             print "Usage: wan_netmask <ip-mask>"
362             print "Sets the WAN subnetwork mask."
363
364         def do_wan_gateway(self, line):
365             self.session.set_WAN_gateway(line)
366             return 0
367         def help_wan_gateway(self):
368             print "Usage: wan_gateway <ip-address>"
369             print "Sets the LAN subnetwork mask."
370
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)
375             else:
376                 print >>sys.stderr, "linksys: server index out of bounds."
377             return 0
378         def help_dns(self):
379             print "Usage: dns {1|2|3} <ip-mask>"
380             print "Sets a primary, secondary, or tertiary DNS server address."
381
382         def do_password(self, line):
383             self.session.set_password(line)
384             return 0
385         def help_password(self):
386             print "Usage: password <string>"
387             print "Sets the router password."
388
389         def do_upnp(self, line):
390             self.flag_command(self.session.set_UPnP)
391             return 0
392         def help_upnp(self):
393             print "Usage: upnp {on|off|enable|disable|yes|no}"
394             print "Switch to enable or disable Universal Plug and Play."
395
396         def do_reset(self, line):
397             self.session.reset()
398         def help_reset(self):
399             print "Usage: reset"
400             print "Reset Linksys settings to factory defaults."
401
402         def do_dhcp(self, line):
403             self.flag_command(self.session.set_DHCP)
404         def help_dhcp(self):
405             print "Usage: dhcp {on|off|enable|disable|yes|no}"
406             print "Switch to enable or disable DHCP features."
407
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."
413
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."
419
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."
425
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)
430             else:
431                 print >>sys.stderr, "linksys: server index out of bounds."
432             return 0
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."
436
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."
442
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."
448
449         def do_configure(self, line):
450             self.session.configure()
451             return 0
452         def help_configure(self):
453             print "Usage: configure"
454             print "Writes the configuration to the Linksys."
455
456         def do_cache(self, line):
457             print self.session.pagecache
458         def help_cache(self):
459             print "Usage: cache"
460             print "Display the page cache."
461
462         def do_quit(self, line):
463             return 1
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):
468             print ""
469             self.session.configure()
470             return 1
471         def help_EOF(self):
472             print "The EOF command writes the configuration to the linksys"
473             print "and ends your session."
474
475         def default(self, line):
476             """Pass the command through to be executed by the shell."""
477             os.system(line)
478             return 0
479
480         def help_help(self):
481             print "On-line help is available through this command."
482             print "? is a convenience alias for help."
483
484         def help_introduction(self):
485             print """\
486
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.
492
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:
497
498 machine 192.168.1.1
499     login ""
500     password admin
501
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.
507
508 For more help, read the topics 'wan', 'lan', and 'wireless'."""
509
510         def help_lan(self):
511             print """\
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
514 untouched."""
515
516         def help_wan(self):
517             print """\
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.
521
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.
527
528 Some ISPs may require you to set host and domain for use with dynamic-address
529 allocation."""
530
531         def help_wireless(self):
532             print """\
533 The channel, ssid, ssid_broadcast, wep, and wireless commands control
534 wireless routing."""
535
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."
540
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."
545
546         def emptyline(self):
547             pass
548
549     interpreter = LinksysInterpreter()
550     for arg in sys.argv[1:]:
551         interpreter.onecmd(arg)
552     fatal = False
553     while not fatal:
554         try:
555             interpreter.cmdloop()
556             fatal = True
557         except LinksysError, (message, fatal):
558             print "linksys:", message
559
560 # The following sets edit modes for GNU EMACS
561 # Local Variables:
562 # mode:python
563 # End: