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 application is a re-write of the original cookies.py script found in
26 uzbl's master branch. This script provides more functionality than the original
27 cookies.py by adding command line options to specify different cookie jar
28 locations, socket locations, verbose output, etc.
33 Check the cookie daemon uzbl-wiki page for more information on where to
34 find the latest version of the cookie_daemon.py
36 http://www.uzbl.org/wiki/cookie_daemon.py
41 Usage: cookie_daemon.py [options]
44 -h, --help show this help message and exit
45 -n, --no-daemon don't daemonise the process.
46 -v, --verbose print verbose output.
47 -t SECONDS, --daemon-timeout=SECONDS
48 shutdown the daemon after x seconds inactivity.
49 WARNING: Do not use this when launching the cookie
51 -s SOCKET, --cookie-socket=SOCKET
52 manually specify the socket location.
53 -j FILE, --cookie-jar=FILE
54 manually specify the cookie jar location.
55 -m, --memory store cookies in memory only - do not write to disk
60 In order to get uzbl to talk to a running cookie daemon you add the following
63 set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
65 Or if you prefer using the $HOME variable:
67 set cookie_handler = talk_to_socket $HOME/.cache/uzbl/cookie_daemon_socket
72 - There is no easy way of stopping a running daemon.
77 - Use a pid file to make stopping a running daemon easy.
78 - add {start|stop|restart} command line arguments to make the cookie_daemon
79 functionally similar to the daemons found in /etc/init.d/ (in gentoo)
80 or /etc/rc.d/ (in arch).
82 Reporting bugs / getting help
83 =============================
85 The best and fastest way to get hold of the maintainers of the cookie_daemon.py
86 is to send them a message in the #uzbl irc channel found on the Freenode IRC
87 network (irc.freenode.org).
98 from traceback import print_exc
99 from signal import signal, SIGTERM
100 from optparse import OptionParser
103 import cStringIO as StringIO
109 # ============================================================================
110 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
111 # ============================================================================
114 # Location of the uzbl cache directory.
115 if 'XDG_CACHE_HOME' in os.environ.keys() and os.environ['XDG_CACHE_HOME']:
116 CACHE_DIR = os.path.join(os.environ['XDG_CACHE_HOME'], 'uzbl/')
119 CACHE_DIR = os.path.join(os.environ['HOME'], '.cache/uzbl/')
121 # Location of the uzbl data directory.
122 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
123 DATA_DIR = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
126 DATA_DIR = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
131 # Default cookie jar and daemon socket locations.
132 'cookie_socket': os.path.join(CACHE_DIR, 'cookie_daemon_socket'),
133 'cookie_jar': os.path.join(DATA_DIR, 'cookies.txt'),
135 # Time out after x seconds of inactivity (set to 0 for never time out).
136 # Set to 0 by default until talk_to_socket is doing the spawning.
139 # Tell process to daemonise
142 # Set true to print helpful debugging messages to the terminal.
145 } # End of config dictionary.
148 # ============================================================================
149 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
150 # ============================================================================
153 _SCRIPTNAME = os.path.basename(sys.argv[0])
155 '''Prints messages sent to it only if the verbose flag has been set.'''
157 if config['verbose']:
158 print "%s: %s" % (_SCRIPTNAME, msg)
161 def mkbasedir(filepath):
162 '''Create base directory of filepath if it doesn't exist.'''
164 dirname = os.path.dirname(filepath)
165 if not os.path.exists(dirname):
166 echo("creating dirs: %r" % dirname)
170 def reclaim_socket():
171 '''Check if another process (hopefully a cookie_daemon.py) is listening
172 on the cookie daemon socket. If another process is found to be
173 listening on the socket exit the daemon immediately and leave the
174 socket alone. If the connect fails assume the socket has been abandoned
175 and delete it (to be re-created in the create socket function).'''
177 cookie_socket = config['cookie_socket']
180 sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
181 sock.connect(cookie_socket)
185 # Failed to connect to cookie_socket so assume it has been
186 # abandoned by another cookie daemon process.
187 echo("reclaiming abandoned cookie_socket %r." % cookie_socket)
188 if os.path.exists(cookie_socket):
189 os.remove(cookie_socket)
193 echo("detected another process listening on %r." % cookie_socket)
195 # Use os._exit() to avoid tripping the atexit cleanup function.
200 '''Daemonize the process using the Stevens' double-fork magic.'''
208 sys.stderr.write("fork #1 failed")
221 sys.stderr.write("fork #2 failed")
227 devnull = '/dev/null'
228 stdin = file(devnull, 'r')
229 stdout = file(devnull, 'a+')
230 stderr = file(devnull, 'a+', 0)
232 os.dup2(stdin.fileno(), sys.stdin.fileno())
233 os.dup2(stdout.fileno(), sys.stdout.fileno())
234 os.dup2(stderr.fileno(), sys.stderr.fileno())
238 '''The uzbl cookie daemon class.'''
241 '''Initialise class variables.'''
243 self.server_socket = None
245 self.last_request = time.time()
246 self._running = False
250 '''Start the daemon.'''
252 # Check if another daemon is running. The reclaim_socket function will
253 # exit if another daemon is detected listening on the cookie socket
254 # and remove the abandoned socket if there isnt.
255 if os.path.exists(config['cookie_socket']):
259 if config['daemon_mode']:
260 echo("entering daemon mode.")
263 # Register a function to cleanup on exit.
264 atexit.register(self.quit)
266 # Make SIGTERM act orderly.
267 signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
269 # Create cookie jar object from file.
270 self.open_cookie_jar()
272 # Creating a way to exit nested loops by setting a running flag.
276 # Create cookie daemon socket.
280 # Enter main listen loop.
283 except KeyboardInterrupt:
284 self._running = False
297 # Always delete the socket before calling create again.
301 def open_cookie_jar(self):
302 '''Open the cookie jar.'''
304 cookie_jar = config['cookie_jar']
306 mkbasedir(cookie_jar)
308 # Create cookie jar object from file.
309 self.jar = cookielib.MozillaCookieJar(cookie_jar)
313 # Attempt to load cookies from the cookie jar.
314 self.jar.load(ignore_discard=True)
316 # Ensure restrictive permissions are set on the cookie jar
317 # to prevent other users on the system from hi-jacking your
318 # authenticated sessions simply by copying your cookie jar.
319 os.chmod(cookie_jar, 0600)
325 def create_socket(self):
326 '''Create AF_UNIX socket for interprocess uzbl instance <-> cookie
327 daemon communication.'''
329 cookie_socket = config['cookie_socket']
330 mkbasedir(cookie_socket)
332 self.server_socket = socket.socket(socket.AF_UNIX,
333 socket.SOCK_SEQPACKET)
335 if os.path.exists(cookie_socket):
336 # Accounting for super-rare super-fast racetrack condition.
339 self.server_socket.bind(cookie_socket)
341 # Set restrictive permissions on the cookie socket to prevent other
342 # users on the system from data-mining your cookies.
343 os.chmod(cookie_socket, 0600)
347 '''Listen for incoming cookie PUT and GET requests.'''
349 echo("listening on %r" % config['cookie_socket'])
352 # This line tells the socket how many pending incoming connections
353 # to enqueue. I haven't had any broken pipe errors so far while
354 # using the non-obvious value of 1 under heavy load conditions.
355 self.server_socket.listen(1)
357 if bool(select.select([self.server_socket], [], [], 1)[0]):
358 client_socket, _ = self.server_socket.accept()
359 self.handle_request(client_socket)
360 self.last_request = time.time()
361 client_socket.close()
363 if config['daemon_timeout']:
364 idle = time.time() - self.last_request
365 if idle > config['daemon_timeout']:
366 self._running = False
369 def handle_request(self, client_socket):
370 '''Connection made, now to serve a cookie PUT or GET request.'''
372 # Receive cookie request from client.
373 data = client_socket.recv(8192)
377 # Cookie argument list in packet is null separated.
378 argv = data.split("\0")
380 # Catch the EXIT command sent to kill the daemon.
381 if len(argv) == 1 and argv[0].strip() == "EXIT":
382 self._running = False
385 # Determine whether or not to print cookie data to terminal.
386 print_cookie = (config['verbose'] and not config['daemon_mode'])
388 print ' '.join(argv[:4])
392 uri = urllib2.urlparse.ParseResult(
398 fragment='').geturl()
400 req = urllib2.Request(uri)
403 self.jar.add_cookie_header(req)
404 if req.has_header('Cookie'):
405 cookie = req.get_header('Cookie')
406 client_socket.send(cookie)
411 client_socket.send("\0")
413 elif action == "PUT":
414 cookie = argv[4] if len(argv) > 3 else None
418 self.put_cookie(req, cookie)
424 def put_cookie(self, req, cookie=None):
425 '''Put a cookie in the cookie jar.'''
427 hdr = urllib2.httplib.HTTPMessage(\
428 StringIO.StringIO('Set-Cookie: %s' % cookie))
429 res = urllib2.addinfourl(StringIO.StringIO(), hdr,
431 self.jar.extract_cookies(res, req)
432 if config['cookie_jar']:
433 self.jar.save(ignore_discard=True)
437 '''Called on exit to make sure all loose ends are tied up.'''
443 def del_socket(self):
444 '''Remove the cookie_socket file on exit. In a way the cookie_socket
445 is the daemons pid file equivalent.'''
447 if self.server_socket:
449 self.server_socket.close()
454 self.server_socket = None
456 cookie_socket = config['cookie_socket']
457 if os.path.exists(cookie_socket):
458 echo("deleting socket %r" % cookie_socket)
459 os.remove(cookie_socket)
465 # Define command line parameters.
466 parser = OptionParser()
467 parser.add_option('-n', '--no-daemon', dest='no_daemon',
468 action='store_true', help="don't daemonise the process.")
470 parser.add_option('-v', '--verbose', dest="verbose",
471 action='store_true', help="print verbose output.")
473 parser.add_option('-t', '--daemon-timeout', dest='daemon_timeout',
474 action="store", metavar="SECONDS", help="shutdown the daemon after x "\
475 "seconds inactivity. WARNING: Do not use this when launching the "\
476 "cookie daemon manually.")
478 parser.add_option('-s', '--cookie-socket', dest="cookie_socket",
479 metavar="SOCKET", help="manually specify the socket location.")
481 parser.add_option('-j', '--cookie-jar', dest='cookie_jar',
482 metavar="FILE", help="manually specify the cookie jar location.")
484 parser.add_option('-m', '--memory', dest='memory', action='store_true',
485 help="store cookies in memory only - do not write to disk")
487 # Parse the command line arguments.
488 (options, args) = parser.parse_args()
491 config['verbose'] = True
493 echo("unknown argument %r" % arg)
499 config['verbose'] = True
500 echo("verbose mode on.")
502 if options.no_daemon:
503 echo("daemon mode off.")
504 config['daemon_mode'] = False
506 if options.cookie_socket:
507 echo("using cookie_socket %r" % options.cookie_socket)
508 config['cookie_socket'] = options.cookie_socket
510 if options.cookie_jar:
511 echo("using cookie_jar %r" % options.cookie_jar)
512 config['cookie_jar'] = options.cookie_jar
515 echo("using memory %r" % options.memory)
516 config['cookie_jar'] = None
518 if options.daemon_timeout:
520 config['daemon_timeout'] = int(options.daemon_timeout)
521 echo("set timeout to %d seconds." % config['daemon_timeout'])
524 config['verbose'] = True
525 echo("fatal error: expected int argument for --daemon-timeout")
528 # Expand $VAR's in config keys that relate to paths.
529 for key in ['cookie_socket', 'cookie_jar']:
531 config[key] = os.path.expandvars(config[key])
533 CookieMonster().run()
536 if __name__ == "__main__":