Several bug fixes to cookie_daemon.py
[uzbl-mobile] / examples / data / uzbl / scripts / cookie_daemon.py
1 #!/usr/bin/env python
2
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>
7 #
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.
12 #
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.
17 #
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/>.
20
21
22 # Todo list:
23 #  - Setup some option parsing so the daemon can take optional command line
24 #    arguments. 
25
26
27 import cookielib
28 import os
29 import sys
30 import urllib2
31 import select
32 import socket
33 import time
34 import atexit
35 from signal import signal, SIGTERM
36
37 try:
38     import cStringIO as StringIO
39
40 except ImportError:
41     import StringIO
42
43
44 # ============================================================================
45 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
46 # ============================================================================
47
48 # Location of the uzbl cache directory.
49 if 'XDG_CACHE_HOME' in os.environ.keys() and os.environ['XDG_CACHE_HOME']:
50     cache_dir = os.path.join(os.environ['XDG_CACHE_HOME'], 'uzbl/')
51
52 else:
53     cache_dir = os.path.join(os.environ['HOME'], '.cache/uzbl/')
54
55 # Location of the uzbl data directory.
56 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
57     data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
58
59 else:
60     data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
61
62 # Create cache dir and data dir if they are missing.
63 for path in [data_dir, cache_dir]:
64     if not os.path.exists(path):
65         os.makedirs(path) 
66
67 # Default config
68 cookie_socket = os.path.join(cache_dir, 'cookie_daemon_socket')
69 cookie_jar = os.path.join(data_dir, 'cookies.txt')
70
71 # Time out after x seconds of inactivity (set to 0 for never time out).
72 # Set to 0 by default until talk_to_socket is doing the spawning.
73 daemon_timeout = 0
74
75 # Enable/disable daemonizing the process (useful when debugging). 
76 # Set to False by default until talk_to_socket is doing the spawning.
77 daemon_mode = False
78
79 # ============================================================================
80 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
81 # ============================================================================
82
83
84 class CookieMonster:
85     '''The uzbl cookie daemon class.'''
86
87     def __init__(self, cookie_socket, cookie_jar, daemon_timeout,\
88       daemon_mode):
89
90         self.cookie_socket = os.path.expandvars(cookie_socket)
91         self.server_socket = None
92         self.cookie_jar = os.path.expandvars(cookie_jar)
93         self.daemon_mode = daemon_mode
94         self.jar = None
95         self.daemon_timeout = daemon_timeout
96         self.last_request = time.time()
97
98     
99     def run(self):
100         '''Start the daemon.'''
101         
102         # Daemonize process.
103         if self.daemon_mode:
104             self.daemonize()
105         
106         # Cleanup & quit function
107         atexit.register(self.quit)
108
109         # Tell SIGTERM to act orderly.
110         signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
111
112         try:
113             # Create cookie_socket 
114             self.create_socket()
115         
116             # Create jar object
117             self.open_cookie_jar()
118             
119             # Listen for GET and PULL cookie requests.
120             self.listen()
121        
122         except KeyboardInterrupt:
123             print
124
125         except:
126             # Clean up
127             self.del_socket()
128
129             # Raise exception
130             raise
131        
132
133     def daemonize(function):
134         '''Daemonize the process using the Stevens' double-fork magic.'''
135
136         try:
137             if os.fork(): os._exit(0)
138
139         except OSError, e:
140             sys.stderr.write("fork #1 failed: %s\n" % e)
141             sys.exit(1)
142         
143         os.chdir('/')
144         os.setsid()
145         os.umask(0)
146         
147         try:
148             if os.fork(): os._exit(0)
149
150         except OSError, e:
151             sys.stderr.write("fork #2 failed: %s\n" % e)
152             sys.exit(1)
153         
154         sys.stdout.flush()
155         sys.stderr.flush()
156
157         devnull = '/dev/null'
158         stdin = file(devnull, 'r')
159         stdout = file(devnull, 'a+')
160         stderr = file(devnull, 'a+', 0)
161
162         os.dup2(stdin.fileno(), sys.stdin.fileno())
163         os.dup2(stdout.fileno(), sys.stdout.fileno())
164         os.dup2(stderr.fileno(), sys.stderr.fileno())
165         
166
167     def open_cookie_jar(self):
168         '''Open the cookie jar.'''
169         
170         # Open cookie jar.
171         self.jar = cookielib.MozillaCookieJar(cookie_jar)
172         try:
173             self.jar.load(ignore_discard=True)
174
175         except:
176             pass
177
178
179     def create_socket(self):
180         '''Open socket AF_UNIX socket for uzbl instance <-> daemon
181         communication.'''
182     
183         if os.path.exists(self.cookie_socket):
184             # Don't you just love racetrack conditions! 
185             sys.exit(1)
186             
187         self.server_socket = socket.socket(socket.AF_UNIX,\
188           socket.SOCK_SEQPACKET)
189
190         self.server_socket.bind(self.cookie_socket)
191         
192         # Only allow the current user to read and write to the socket.
193         os.chmod(self.cookie_socket, 0600)
194
195
196     def listen(self):
197         '''Listen for incoming cookie PUT and GET requests.'''
198
199         while True:
200             # If you get broken pipe errors increase this listen number.
201             self.server_socket.listen(1)
202
203             if bool(select.select([self.server_socket],[],[],1)[0]):
204                 client_socket, _ = self.server_socket.accept()
205                 self.handle_request(client_socket)
206                 self.last_request = time.time()
207             
208             if self.daemon_timeout:
209                 idle = time.time() - self.last_request
210                 if idle > self.daemon_timeout: break
211         
212
213     def handle_request(self, client_socket):
214         '''Connection made, now to serve a cookie PUT or GET request.'''
215          
216         # Receive cookie request from client.
217         data = client_socket.recv(8192)
218         argv = data.split("\0")
219                 
220         # For debugging:
221         print ' '.join(argv[:4])
222
223         action = argv[0]
224         
225         uri = urllib2.urlparse.ParseResult(
226           scheme=argv[1],
227           netloc=argv[2],
228           path=argv[3],
229           params='',
230           query='',
231           fragment='').geturl()
232         
233         req = urllib2.Request(uri)
234
235         if action == "GET":
236             self.jar.add_cookie_header(req)
237             if req.has_header('Cookie'):
238                 cookie = req.get_header('Cookie')
239                 client_socket.send(cookie)
240                 print cookie
241
242             else:
243                 client_socket.send("\0")
244
245         elif action == "PUT":
246             if len(argv) > 3:
247                 set_cookie = argv[4]
248                 print set_cookie
249
250             else:
251                 set_cookie = None
252
253             hdr = urllib2.httplib.HTTPMessage(\
254               StringIO.StringIO('Set-Cookie: %s' % set_cookie))
255             res = urllib2.addinfourl(StringIO.StringIO(), hdr,\
256               req.get_full_url())
257             self.jar.extract_cookies(res,req)
258             self.jar.save(ignore_discard=True) 
259
260         print
261             
262         client_socket.close()
263
264
265     def quit(self, *args):
266         '''Called on exit to make sure all loose ends are tied up.'''
267         
268         # Only one loose end so far.
269         self.del_socket()
270
271         sys.exit(0)
272     
273
274     def del_socket(self):
275         '''Remove the cookie_socket file on exit. In a way the cookie_socket 
276         is the daemons pid file equivalent.'''
277     
278         if self.server_socket:
279             self.server_socket.close()
280
281         if os.path.exists(self.cookie_socket):
282             os.remove(self.cookie_socket)
283
284
285 if __name__ == "__main__":
286     
287     if os.path.exists(cookie_socket):
288         print "Error: cookie socket already exists: %r" % cookie_socket
289         sys.exit(1)
290     
291     CookieMonster(cookie_socket, cookie_jar, daemon_timeout,\
292       daemon_mode).run()
293