3 # Uzbl tabbing wrapper using a fifo socket interface
4 # Copyright (c) 2009, Tom Adams <tom@holizz.com>
5 # Copyright (c) 2009, Dieter Plaetinck <dieter AT plaetinck.be>
6 # Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 The Python Cookie Daemon
23 ========================
25 This daemon is a re-write of the original cookies.py script found in uzbl's
26 master branch. This script provides more functionality than the original
27 cookies.py by adding numerous command line options to specify different cookie
28 jar locations, socket locations, verbose output, etc. This functionality is
29 very useful as it allows you to run multiple daemons at once serving cookies
30 to different groups of uzbl instances as required.
35 Check the cookie daemon uzbl-wiki page for more information on where to
36 find the latest version of the cookie_daemon.py
38 http://www.uzbl.org/wiki/cookie_daemon.py
43 Usage: cookie_daemon.py [options]
46 -h, --help show this help message and exit
47 -n, --no-daemon don't daemonise the process.
48 -v, --verbose print verbose output.
49 -t SECONDS, --daemon-timeout=SECONDS
50 shutdown the daemon after x seconds inactivity.
51 WARNING: Do not use this when launching the cookie
53 -s SOCKET, --cookie-socket=SOCKET
54 manually specify the socket location.
55 -j FILE, --cookie-jar=FILE
56 manually specify the cookie jar location.
57 -m, --memory store cookies in memory only - do not write to disk
62 In order to get uzbl to talk to a running cookie daemon you add the following
65 set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
67 Or if you prefer using the $HOME variable:
69 set cookie_handler = talk_to_socket $HOME/.cache/uzbl/cookie_daemon_socket
74 - There is no easy way of stopping a running daemon.
79 - Use a pid file to make stopping a running daemon easy.
80 - add {start|stop|restart} command line arguments to make the cookie_daemon
81 functionally similar to the daemons found in /etc/init.d/ (in gentoo)
82 or /etc/rc.d/ (in arch).
84 Reporting bugs / getting help
85 =============================
87 The best way to report bugs and or get help with the cookie daemon is to
88 contact the maintainers it the #uzbl irc channel found on the Freenode IRC
89 network (irc.freenode.org).
100 from traceback import print_exc
101 from signal import signal, SIGTERM
102 from optparse import OptionParser
105 import cStringIO as StringIO
111 # ============================================================================
112 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
113 # ============================================================================
116 # Location of the uzbl cache directory.
117 if 'XDG_CACHE_HOME' in os.environ.keys() and os.environ['XDG_CACHE_HOME']:
118 CACHE_DIR = os.path.join(os.environ['XDG_CACHE_HOME'], 'uzbl/')
121 CACHE_DIR = os.path.join(os.environ['HOME'], '.cache/uzbl/')
123 # Location of the uzbl data directory.
124 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
125 DATA_DIR = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
128 DATA_DIR = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
133 # Default cookie jar and daemon socket locations.
134 'cookie_socket': os.path.join(CACHE_DIR, 'cookie_daemon_socket'),
135 'cookie_jar': os.path.join(DATA_DIR, 'cookies.txt'),
137 # Time out after x seconds of inactivity (set to 0 for never time out).
138 # WARNING: Do not use this option if you are manually launching the daemon.
141 # Daemonise by default.
144 # Optionally print helpful debugging messages to the terminal.
147 } # End of config dictionary.
150 # ============================================================================
151 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
152 # ============================================================================
155 _SCRIPTNAME = os.path.basename(sys.argv[0])
157 '''Prints only if the verbose flag has been set.'''
159 if config['verbose']:
160 sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
164 '''Prints error message and exits.'''
166 sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
170 def mkbasedir(filepath):
171 '''Create the base directories of the file in the file-path if the dirs
174 dirname = os.path.dirname(filepath)
175 if not os.path.exists(dirname):
176 echo("creating dirs: %r" % dirname)
180 def check_socket_health(cookie_socket):
181 '''Check if another process (hopefully a cookie_daemon.py) is listening
182 on the cookie daemon socket. If another process is found to be
183 listening on the socket exit the daemon immediately and leave the
184 socket alone. If the connect fails assume the socket has been abandoned
185 and delete it (to be re-created in the create socket function).'''
187 if not os.path.exists(cookie_socket):
188 # What once was is now no more.
191 if os.path.isfile(cookie_socket):
192 error("regular file at %r is not a socket" % cookie_socket)
194 if os.path.isdir(cookie_socket):
195 error("directory at %r is not a socket" % cookie_socket)
198 sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
199 sock.connect(cookie_socket)
201 error("detected another process listening on %r" % cookie_socket)
204 # Failed to connect to cookie_socket so assume it has been
205 # abandoned by another cookie daemon process.
206 if os.path.exists(cookie_socket):
207 echo("deleting abandoned socket %r" % cookie_socket)
208 os.remove(cookie_socket)
212 '''Daemonize the process using the Stevens' double-fork magic.'''
220 sys.stderr.write("fork #1 failed")
233 sys.stderr.write("fork #2 failed")
239 devnull = '/dev/null'
240 stdin = file(devnull, 'r')
241 stdout = file(devnull, 'a+')
242 stderr = file(devnull, 'a+', 0)
244 os.dup2(stdin.fileno(), sys.stdin.fileno())
245 os.dup2(stdout.fileno(), sys.stdout.fileno())
246 os.dup2(stderr.fileno(), sys.stderr.fileno())
250 '''The uzbl cookie daemon class.'''
253 '''Initialise class variables.'''
255 self.server_socket = None
257 self.last_request = time.time()
258 self._running = False
262 '''Start the daemon.'''
264 # The check healthy function will exit if another daemon is detected
265 # listening on the cookie socket and remove the abandoned socket if
267 if os.path.exists(config['cookie_socket']):
268 check_socket_health(config['cookie_socket'])
271 if config['daemon_mode']:
272 echo("entering daemon mode")
275 # Register a function to cleanup on exit.
276 atexit.register(self.quit)
278 # Make SIGTERM act orderly.
279 signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
281 # Create cookie jar object from file.
282 self.open_cookie_jar()
284 # Create a way to exit nested loops by setting a running flag.
288 # Create cookie daemon socket.
292 # Enter main listen loop.
295 except KeyboardInterrupt:
296 self._running = False
309 # Always delete the socket before calling create again.
313 def open_cookie_jar(self):
314 '''Open the cookie jar.'''
316 cookie_jar = config['cookie_jar']
318 mkbasedir(cookie_jar)
320 # Create cookie jar object from file.
321 self.jar = cookielib.MozillaCookieJar(cookie_jar)
325 # Attempt to load cookies from the cookie jar.
326 self.jar.load(ignore_discard=True)
328 # Ensure restrictive permissions are set on the cookie jar
329 # to prevent other users on the system from hi-jacking your
330 # authenticated sessions simply by copying your cookie jar.
331 os.chmod(cookie_jar, 0600)
337 def create_socket(self):
338 '''Create AF_UNIX socket for communication with uzbl instances.'''
340 cookie_socket = config['cookie_socket']
341 mkbasedir(cookie_socket)
343 self.server_socket = socket.socket(socket.AF_UNIX,
344 socket.SOCK_SEQPACKET)
346 if os.path.exists(cookie_socket):
347 # Accounting for super-rare super-fast racetrack condition.
348 check_socket_health(cookie_socket)
350 self.server_socket.bind(cookie_socket)
352 # Set restrictive permissions on the cookie socket to prevent other
353 # users on the system from data-mining your cookies.
354 os.chmod(cookie_socket, 0600)
358 '''Listen for incoming cookie PUT and GET requests.'''
360 echo("listening on %r" % config['cookie_socket'])
363 # This line tells the socket how many pending incoming connections
364 # to enqueue at once. Raising this number may or may not increase
366 self.server_socket.listen(1)
368 if bool(select.select([self.server_socket], [], [], 1)[0]):
369 client_socket, _ = self.server_socket.accept()
370 self.handle_request(client_socket)
371 self.last_request = time.time()
372 client_socket.close()
374 if config['daemon_timeout']:
375 # Checks if the daemon has been idling for too long.
376 idle = time.time() - self.last_request
377 if idle > config['daemon_timeout']:
378 self._running = False
381 def handle_request(self, client_socket):
382 '''Connection made, now to serve a cookie PUT or GET request.'''
384 # Receive cookie request from client.
385 data = client_socket.recv(8192)
389 # Cookie argument list in packet is null separated.
390 argv = data.split("\0")
392 # Catch the EXIT command sent to kill running daemons.
393 if len(argv) == 1 and argv[0].strip() == "EXIT":
394 self._running = False
397 # Determine whether or not to print cookie data to terminal.
398 print_cookie = (config['verbose'] and not config['daemon_mode'])
400 print ' '.join(argv[:4])
404 uri = urllib2.urlparse.ParseResult(
410 fragment='').geturl()
412 req = urllib2.Request(uri)
415 self.jar.add_cookie_header(req)
416 if req.has_header('Cookie'):
417 cookie = req.get_header('Cookie')
418 client_socket.send(cookie)
423 client_socket.send("\0")
425 elif action == "PUT":
426 cookie = argv[4] if len(argv) > 3 else None
430 self.put_cookie(req, cookie)
436 def put_cookie(self, req, cookie=None):
437 '''Put a cookie in the cookie jar.'''
439 hdr = urllib2.httplib.HTTPMessage(\
440 StringIO.StringIO('Set-Cookie: %s' % cookie))
441 res = urllib2.addinfourl(StringIO.StringIO(), hdr,
443 self.jar.extract_cookies(res, req)
444 if config['cookie_jar']:
445 self.jar.save(ignore_discard=True)
448 def del_socket(self):
449 '''Remove the cookie_socket file on exit. In a way the cookie_socket
450 is the daemons pid file equivalent.'''
452 if self.server_socket:
454 self.server_socket.close()
459 self.server_socket = None
461 cookie_socket = config['cookie_socket']
462 if os.path.exists(cookie_socket):
463 echo("deleting socket %r" % cookie_socket)
464 os.remove(cookie_socket)
468 '''Called on exit to make sure all loose ends are tied up.'''
477 # Define command line parameters.
478 parser = OptionParser()
479 parser.add_option('-n', '--no-daemon', dest='no_daemon',
480 action='store_true', help="don't daemonise the process.")
482 parser.add_option('-v', '--verbose', dest="verbose",
483 action='store_true', help="print verbose output.")
485 parser.add_option('-t', '--daemon-timeout', dest='daemon_timeout',
486 action="store", metavar="SECONDS", help="shutdown the daemon after x "\
487 "seconds inactivity. WARNING: Do not use this when launching the "\
488 "cookie daemon manually.")
490 parser.add_option('-s', '--cookie-socket', dest="cookie_socket",
491 metavar="SOCKET", help="manually specify the socket location.")
493 parser.add_option('-j', '--cookie-jar', dest='cookie_jar',
494 metavar="FILE", help="manually specify the cookie jar location.")
496 parser.add_option('-m', '--memory', dest='memory', action='store_true',
497 help="store cookies in memory only - do not write to disk")
499 # Parse the command line arguments.
500 (options, args) = parser.parse_args()
503 error("unknown argument %r" % args[0])
506 config['verbose'] = True
507 echo("verbose mode on")
509 if options.no_daemon:
510 echo("daemon mode off")
511 config['daemon_mode'] = False
513 if options.cookie_socket:
514 echo("using cookie_socket %r" % options.cookie_socket)
515 config['cookie_socket'] = options.cookie_socket
517 if options.cookie_jar:
518 echo("using cookie_jar %r" % options.cookie_jar)
519 config['cookie_jar'] = options.cookie_jar
522 echo("using memory %r" % options.memory)
523 config['cookie_jar'] = None
525 if options.daemon_timeout:
527 config['daemon_timeout'] = int(options.daemon_timeout)
528 echo("set timeout to %d seconds" % config['daemon_timeout'])
531 error("expected int argument for -t, --daemon-timeout")
533 # Expand $VAR's in config keys that relate to paths.
534 for key in ['cookie_socket', 'cookie_jar']:
536 config[key] = os.path.expandvars(config[key])
538 CookieMonster().run()
541 if __name__ == "__main__":