Also chmod(cookie_jar, 0600) in 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         # Register a function to cleanup on exit. 
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(self.cookie_jar)
172         try:
173             # Load cookies from the cookie_jar file.
174             self.jar.load(ignore_discard=True)
175
176             # Check cookie_jar only readable and writable by this user.
177             os.chmod(self.cookie_jar, 0600)
178
179         except:
180             pass
181
182
183     def create_socket(self):
184         '''Open socket AF_UNIX socket for uzbl instance <-> daemon
185         communication.'''
186     
187         if os.path.exists(self.cookie_socket):
188             # Don't you just love racetrack conditions! 
189             sys.exit(1)
190             
191         self.server_socket = socket.socket(socket.AF_UNIX,\
192           socket.SOCK_SEQPACKET)
193
194         self.server_socket.bind(self.cookie_socket)
195         
196         # Only allow the current user to read and write to the socket.
197         os.chmod(self.cookie_socket, 0600)
198
199
200     def listen(self):
201         '''Listen for incoming cookie PUT and GET requests.'''
202
203         while True:
204             # If you get broken pipe errors increase this listen number.
205             self.server_socket.listen(1)
206
207             if bool(select.select([self.server_socket],[],[],1)[0]):
208                 client_socket, _ = self.server_socket.accept()
209                 self.handle_request(client_socket)
210                 self.last_request = time.time()
211             
212             if self.daemon_timeout:
213                 idle = time.time() - self.last_request
214                 if idle > self.daemon_timeout: break
215         
216
217     def handle_request(self, client_socket):
218         '''Connection made, now to serve a cookie PUT or GET request.'''
219          
220         # Receive cookie request from client.
221         data = client_socket.recv(8192)
222         argv = data.split("\0")
223                 
224         # For debugging:
225         print ' '.join(argv[:4])
226
227         action = argv[0]
228         
229         uri = urllib2.urlparse.ParseResult(
230           scheme=argv[1],
231           netloc=argv[2],
232           path=argv[3],
233           params='',
234           query='',
235           fragment='').geturl()
236         
237         req = urllib2.Request(uri)
238
239         if action == "GET":
240             self.jar.add_cookie_header(req)
241             if req.has_header('Cookie'):
242                 cookie = req.get_header('Cookie')
243                 client_socket.send(cookie)
244                 print cookie
245
246             else:
247                 client_socket.send("\0")
248
249         elif action == "PUT":
250             if len(argv) > 3:
251                 set_cookie = argv[4]
252                 print set_cookie
253
254             else:
255                 set_cookie = None
256
257             hdr = urllib2.httplib.HTTPMessage(\
258               StringIO.StringIO('Set-Cookie: %s' % set_cookie))
259             res = urllib2.addinfourl(StringIO.StringIO(), hdr,\
260               req.get_full_url())
261             self.jar.extract_cookies(res,req)
262             self.jar.save(ignore_discard=True) 
263
264         print
265             
266         client_socket.close()
267
268
269     def quit(self, *args):
270         '''Called on exit to make sure all loose ends are tied up.'''
271         
272         # Only one loose end so far.
273         self.del_socket()
274
275         sys.exit(0)
276     
277
278     def del_socket(self):
279         '''Remove the cookie_socket file on exit. In a way the cookie_socket 
280         is the daemons pid file equivalent.'''
281     
282         if self.server_socket:
283             self.server_socket.close()
284
285         if os.path.exists(self.cookie_socket):
286             os.remove(self.cookie_socket)
287
288
289 if __name__ == "__main__":
290     
291     if os.path.exists(cookie_socket):
292         print "Error: cookie socket already exists: %r" % cookie_socket
293         sys.exit(1)
294     
295     CookieMonster(cookie_socket, cookie_jar, daemon_timeout,\
296       daemon_mode).run()
297