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/>.
21 # For configuration and cookie daemon usage examples check out the the
22 # cookie daemon wiki page at http://www.uzbl.org/wiki/cookie_daemon.py
25 # - There is no easy way of stopping a running daemon.
28 # - Use a pid file to make stopping a running daemon easy.
29 # - add {start|stop|restart} command line arguments to make the cookie_daemon
30 # functionally similar to the daemons found in /etc/init.d/ (in gentoo)
31 # or /etc/rc.d/ (in arch).
42 from traceback import print_exc
43 from signal import signal, SIGTERM
44 from optparse import OptionParser
47 import cStringIO as StringIO
53 # ============================================================================
54 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
55 # ============================================================================
58 # Location of the uzbl cache directory.
59 if 'XDG_CACHE_HOME' in os.environ.keys() and os.environ['XDG_CACHE_HOME']:
60 cache_dir = os.path.join(os.environ['XDG_CACHE_HOME'], 'uzbl/')
63 cache_dir = os.path.join(os.environ['HOME'], '.cache/uzbl/')
65 # Location of the uzbl data directory.
66 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
67 data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
70 data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
75 # Default cookie jar and daemon socket locations.
76 'cookie_socket': os.path.join(cache_dir, 'cookie_daemon_socket'),
77 'cookie_jar': os.path.join(data_dir, 'cookies.txt'),
79 # Time out after x seconds of inactivity (set to 0 for never time out).
80 # Set to 0 by default until talk_to_socket is doing the spawning.
83 # Tell process to daemonise
86 # Set true to print helpful debugging messages to the terminal.
89 } # End of config dictionary.
92 # ============================================================================
93 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
94 # ============================================================================
97 _scriptname = os.path.basename(sys.argv[0])
100 print "%s: %s" % (_scriptname, msg)
103 def mkbasedir(filepath):
104 '''Create base directory of filepath if it doesn't exist.'''
106 dirname = os.path.dirname(filepath)
107 if not os.path.exists(dirname):
108 echo("creating dirs: %r" % dirname)
113 '''The uzbl cookie daemon class.'''
116 '''Initialise class variables.'''
118 self.server_socket = None
120 self.last_request = time.time()
121 self._running = False
125 '''Start the daemon.'''
127 # Check if another daemon is running. The reclaim_socket function will
128 # exit if another daemon is detected listening on the cookie socket
129 # and remove the abandoned socket if there isnt.
130 if os.path.exists(config['cookie_socket']):
131 self.reclaim_socket()
134 if config['daemon_mode']:
135 echo("entering daemon mode.")
138 # Register a function to cleanup on exit.
139 atexit.register(self.quit)
141 # Make SIGTERM act orderly.
142 signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
144 # Create cookie jar object from file.
145 self.open_cookie_jar()
147 # Creating a way to exit nested loops by setting a running flag.
151 # Create cookie daemon socket.
155 # Enter main listen loop.
158 except KeyboardInterrupt:
159 self._running = False
172 # Always delete the socket before calling create again.
176 def reclaim_socket(self):
177 '''Check if another process (hopefully a cookie_daemon.py) is listening
178 on the cookie daemon socket. If another process is found to be
179 listening on the socket exit the daemon immediately and leave the
180 socket alone. If the connect fails assume the socket has been abandoned
181 and delete it (to be re-created in the create socket function).'''
183 cookie_socket = config['cookie_socket']
186 sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
187 sock.connect(cookie_socket)
191 # Failed to connect to cookie_socket so assume it has been
192 # abandoned by another cookie daemon process.
193 echo("reclaiming abandoned cookie_socket %r." % cookie_socket)
194 if os.path.exists(cookie_socket):
195 os.remove(cookie_socket)
199 echo("detected another process listening on %r." % cookie_socket)
201 # Use os._exit() to avoid tripping the atexit cleanup function.
205 def daemonize(function):
206 '''Daemonize the process using the Stevens' double-fork magic.'''
209 if os.fork(): os._exit(0)
212 sys.stderr.write("fork #1 failed: %s\n" % e)
220 if os.fork(): os._exit(0)
223 sys.stderr.write("fork #2 failed: %s\n" % e)
229 devnull = '/dev/null'
230 stdin = file(devnull, 'r')
231 stdout = file(devnull, 'a+')
232 stderr = file(devnull, 'a+', 0)
234 os.dup2(stdin.fileno(), sys.stdin.fileno())
235 os.dup2(stdout.fileno(), sys.stdout.fileno())
236 os.dup2(stderr.fileno(), sys.stderr.fileno())
239 def open_cookie_jar(self):
240 '''Open the cookie jar.'''
242 cookie_jar = config['cookie_jar']
243 if config['cookie_jar']:
244 mkbasedir(cookie_jar)
246 # Create cookie jar object from file.
247 self.jar = cookielib.MozillaCookieJar(cookie_jar)
249 if config['cookie_jar']:
251 # Attempt to load cookies from the cookie jar.
252 self.jar.load(ignore_discard=True)
254 # Ensure restrictive permissions are set on the cookie jar
255 # to prevent other users on the system from hi-jacking your
256 # authenticated sessions simply by copying your cookie jar.
257 os.chmod(cookie_jar, 0600)
263 def create_socket(self):
264 '''Create AF_UNIX socket for interprocess uzbl instance <-> cookie
265 daemon communication.'''
267 cookie_socket = config['cookie_socket']
268 mkbasedir(cookie_socket)
270 self.server_socket = socket.socket(socket.AF_UNIX,\
271 socket.SOCK_SEQPACKET)
273 if os.path.exists(cookie_socket):
274 # Accounting for super-rare super-fast racetrack condition.
275 self.reclaim_socket()
277 self.server_socket.bind(cookie_socket)
279 # Set restrictive permissions on the cookie socket to prevent other
280 # users on the system from data-mining your cookies.
281 os.chmod(cookie_socket, 0600)
285 '''Listen for incoming cookie PUT and GET requests.'''
287 echo("listening on %r" % config['cookie_socket'])
290 # This line tells the socket how many pending incoming connections
291 # to enqueue. I haven't had any broken pipe errors so far while
292 # using the non-obvious value of 1 under heavy load conditions.
293 self.server_socket.listen(1)
295 if bool(select.select([self.server_socket],[],[],1)[0]):
296 client_socket, _ = self.server_socket.accept()
297 self.handle_request(client_socket)
298 self.last_request = time.time()
299 client_socket.close()
301 if config['daemon_timeout']:
302 idle = time.time() - self.last_request
303 if idle > config['daemon_timeout']:
304 self._running = False
307 def handle_request(self, client_socket):
308 '''Connection made, now to serve a cookie PUT or GET request.'''
310 # Receive cookie request from client.
311 data = client_socket.recv(8192)
314 # Cookie argument list in packet is null separated.
315 argv = data.split("\0")
317 # Catch the EXIT command sent to kill the daemon.
318 if len(argv) == 1 and argv[0].strip() == "EXIT":
319 self._running = False
322 # Determine whether or not to print cookie data to terminal.
323 print_cookie = (config['verbose'] and not config['daemon_mode'])
324 if print_cookie: print ' '.join(argv[:4])
328 uri = urllib2.urlparse.ParseResult(
334 fragment='').geturl()
336 req = urllib2.Request(uri)
339 self.jar.add_cookie_header(req)
340 if req.has_header('Cookie'):
341 cookie = req.get_header('Cookie')
342 client_socket.send(cookie)
343 if print_cookie: print cookie
346 client_socket.send("\0")
348 elif action == "PUT":
351 if print_cookie: print set_cookie
356 hdr = urllib2.httplib.HTTPMessage(\
357 StringIO.StringIO('Set-Cookie: %s' % set_cookie))
358 res = urllib2.addinfourl(StringIO.StringIO(), hdr,\
360 self.jar.extract_cookies(res,req)
361 if config['cookie_jar']:
362 self.jar.save(ignore_discard=True)
364 if print_cookie: print
367 def quit(self, *args):
368 '''Called on exit to make sure all loose ends are tied up.'''
370 # Only one loose end so far.
376 def del_socket(self):
377 '''Remove the cookie_socket file on exit. In a way the cookie_socket
378 is the daemons pid file equivalent.'''
380 if self.server_socket:
382 self.server_socket.close()
387 self.server_socket = None
389 cookie_socket = config['cookie_socket']
390 if os.path.exists(cookie_socket):
391 echo("deleting socket %r" % cookie_socket)
392 os.remove(cookie_socket)
395 if __name__ == "__main__":
398 parser = OptionParser()
399 parser.add_option('-n', '--no-daemon', dest='no_daemon',\
400 action='store_true', help="don't daemonise the process.")
402 parser.add_option('-v', '--verbose', dest="verbose",\
403 action='store_true', help="print verbose output.")
405 parser.add_option('-t', '--daemon-timeout', dest='daemon_timeout',\
406 action="store", metavar="SECONDS", help="shutdown the daemon after x "\
407 "seconds inactivity. WARNING: Do not use this when launching the "\
408 "cookie daemon manually.")
410 parser.add_option('-s', '--cookie-socket', dest="cookie_socket",\
411 metavar="SOCKET", help="manually specify the socket location.")
413 parser.add_option('-j', '--cookie-jar', dest='cookie_jar',\
414 metavar="FILE", help="manually specify the cookie jar location.")
416 parser.add_option('-m', '--memory', dest='memory', action='store_true',
417 help="store cookies in memory only - do not write to disk")
419 (options, args) = parser.parse_args()
422 config['verbose'] = True
423 echo("verbose mode on.")
425 if options.no_daemon:
426 echo("daemon mode off")
427 config['daemon_mode'] = False
429 if options.cookie_socket:
430 echo("using cookie_socket %r" % options.cookie_socket)
431 config['cookie_socket'] = options.cookie_socket
433 if options.cookie_jar:
434 echo("using cookie_jar %r" % options.cookie_jar)
435 config['cookie_jar'] = options.cookie_jar
438 echo("using memory %r" % options.memory)
439 config['cookie_jar'] = None
441 if options.daemon_timeout:
443 config['daemon_timeout'] = int(options.daemon_timeout)
444 echo("set timeout to %d seconds." % config['daemon_timeout'])
447 config['verbose'] = True
448 echo("fatal error: expected int argument for --daemon-timeout")
451 for key in ['cookie_socket', 'cookie_jar']:
453 config[key] = os.path.expandvars(config[key])
455 CookieMonster().run()