source: trunk/server.py @ 309

Revision 309, 18.6 KB checked in by marc, 8 years ago (diff)

Improved spec file, removed unused images from installation, fixed a bug with secure images not being shown in ntotifications, bumped changelog, and improved debian/rules build on converting images

  • Property svn:keywords set to Id Rev
Line 
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Itaka is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 3 of the License, or
7# any later version.
8#
9# Itaka is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with Itaka; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17#
18# Copyright 2003-2009 Marc E.
19# http://itaka.jardinpresente.com.ar
20#
21# $Id$
22
23""" Itaka server engine """
24
25import datetime
26import os
27import traceback
28import sys
29
30try:
31    import screenshot
32    import error
33except ImportError:
34    print_e(_('Failed to import Itaka modules'))
35    traceback.print_exc()
36    sys.exit(1)
37
38try:
39    from twisted.python import log
40    from twisted.web import server, static, http, resource
41    import twisted.internet.error
42except ImportError:
43    print_e(_('Could not import Twisted Network Framework'))
44    traceback.print_exc()
45    sys.exit(1)
46
47class BaseHTTPServer:
48    """
49    Base HTTP Server
50    """
51
52    def __init__(self):
53        """
54        Constructor
55        """
56
57        self.server_listening = False
58
59    def add_static_resource(self, name, data, type='text/html; charset=UTF-8'):
60        """
61        Create a static.Data Twisted resource.Resource
62
63        @type name: str
64        @param name: Name of the resource
65
66        @type data: str
67        @param data: Data in memory to add to the resource, typically HTML
68
69        @type type: str
70        @param type: The type of data we are serving
71
72        @rtype: resource.Resource
73        @return: The instance of the resource created
74        """
75
76        setattr(self, name, static.Data(data, type))
77        return getattr(self, name)
78
79    def add_child_to_resource(self, name, path, resource):
80        """
81        Add a static child resource to a Twisted resource.Resource
82
83        @type name: str
84        @param name: The name of the Twisted resource.Resource. 
85
86        @type path: str
87        @param path: The path name (i.e http://www.site.com/PATH) of the Resource. You almost certainly don't want '/' in your path. If you intended to have the root of a folder, e.g. /foo/, you want path to be ''
88
89        @type resource: instance
90        @param resource: A Twisted Resource
91        """
92
93        getattr(self, name).putChild(path, resource)
94
95    def create_site(self, resource, version_header='TwistedWeb/' + twisted.copyright.version):
96        """
97        Creates a Twisted.server.Site with a Twisted Resource
98
99        @type resource: instance
100        @param resource: An instance of a Twisted resource created with L{add_static_resource}
101
102        @type version_header: str
103        @param version_header: The 'Server: str' that is sent on HTTP headers. Defaults to Twisted's.
104        """
105        server.version = version_header
106        self.site = server.Site(resource)
107
108    def start_server(self, reactor, port):
109        """
110        Start the server
111
112        @type reactor: twisted.internet.gtk2reactor.Gtk2Reactor
113        @param reactor: An instance of a reactor already run()
114
115        @type port: int
116        @param port: Port number to listen on
117        """
118
119        try:
120            # We need to create a new instance because of server port changes, don't use hasattr()
121            # Returns a 'twisted.internet.tcp.Port'
122            self.server = reactor.listenTCP(port, self.site)
123        except twisted.internet.error.CannotListenError, e:
124            raise error.ItakaServerCannotListenError, e
125
126        self.server_listening = True
127   
128    def stop_server(self):
129        """
130        Stop the server
131        """
132
133        self.server.stopListening()
134        self.server_listening = False
135
136    def listening(self):
137        """
138        Whether the server is listening or not
139
140        @rtype: bool
141        @return: True if it's listening or False if it is not
142        """
143
144        return self.server_listening
145
146    def add_log_observer(self, observer):
147        """
148        Add a twisted.log observer
149       
150        See: http://twistedmatrix.com/projects/core/documentation/howto/logging.html
151        @type observer: method
152        @param observer: A method to send the logs to
153        """
154
155        self.log_observer = observer
156        log.addObserver(observer)
157
158    def remove_log_observer(self, observer=False):
159        """
160        Remove a twisted.log observer
161       
162        @type observer: method
163        @param observer: The name of the method specified in add_log_observer. If False, the last known log observer added will be removed
164        """
165
166        if observer:
167            log.removeObserver(observer)
168        else:
169            log.removeObserver(self.log_observer)
170
171    def get_listening_information(self):
172        """
173        Returns the information of the current listening server
174
175        @rtype: twisted.internet.interfaces.IAddress
176        @return: The current server's networking information
177        """
178
179        if self.server_listening:
180            return self.server.getHost()
181
182
183class ScreenshotServer(BaseHTTPServer):
184    """
185    Screenshot server that builds upon BaseHTTPServer
186    """
187
188    def __init__(self, gui_instance):
189        """
190        Constructor. Overrides BaseHTTPServer's __init__ to create our resources on-the-fly
191
192        @type gui_instance: instance
193        @param gui_instance: An instance of our L{Gui} class
194        """
195
196        self.gui = gui_instance
197        self.itaka_globals = self.gui.itaka_globals
198        self.configuration = self.gui.configuration
199        self.console = self.gui.console
200
201        self.server_listening = False
202
203        # Here we use our own static.Data special child resource because we need Authentication handling
204        # Otherwise we would just use our own self.add_static_resource
205
206        # Also we create our unique authentication handler this keeps track if the user authenticated or not
207        self.authresource = AuthenticatedResource(self.gui)
208        self.root = DataResource(self.gui, self.authresource, self.itaka_globals.head_html + self.configuration['html']['html'] + self.itaka_globals.footer_html)
209        self.add_child_to_resource('root', '', self.root)
210        self.add_child_to_resource('root', 'screenshot', ScreenshotResource(self.gui, self.authresource))
211        self.add_child_to_resource('root', 'favicon.ico', FileResource(self.gui, self.authresource, os.path.join(self.itaka_globals.image_dir, 'favicon.ico'), 'image/x-icon'))
212
213        self.create_site(self.root, 'Itaka/%s (TwistedWeb/%s)' % (self.itaka_globals.__version__, twisted.copyright.version))
214
215
216class AuthenticatedResource:
217    """
218    Helper object to handle authentication for Resources.
219    Please read RFC 2617 to understand the HTTP Authentication process
220    """
221
222    def __init__(self, gui_instance):
223        """
224        Constructor that inherits code from resource.Resource->static.Data
225
226        @type gui_instance: instance
227        @param gui_instance: An instance of our L{Gui} class
228        """
229
230        self.gui = gui_instance
231        self.configuration = self.gui.configuration
232        self.itaka_globals = self.gui.itaka_globals
233        self.noauth = self.itaka_globals.head_html + self.configuration['html']['authfailure'] + self.itaka_globals.footer_html
234        self.request_data_set = False
235        self.authenticated = False
236
237    def set_request_data(self, data, size, type, session_end=False):
238        """
239        Set the information about the data we are handling
240
241
242        @type data: string
243        @param data: The data to be displayed
244
245        @type size: string
246        @param size: A string for Content-lenght
247
248        @type type: str
249        @param type: The type of data we are serving
250
251        @type session_end: bool
252        @param session_end: Whether this request is the last of a session. This deauthenticates the session.
253        """
254
255        self.request_data_set = True
256        self.data = data
257        self.size = size
258        self.type = type
259        self.session_end = session_end
260
261    def _prompt_auth(self):
262        """
263        Prompt the authorization dialog on the browser
264        """
265
266        self.authenticated = False
267        self.request.setHeader('WWW-Authenticate', 'Basic realm="Itaka Screenshot Server"')
268        self.request.setResponseCode(http.UNAUTHORIZED)
269        self.request.setHeader('Content-Type', 'text/html; charset=UTF-8')
270        self.request.setHeader('Content-Length', str(len(self.noauth)))
271        self.request.setHeader('Connection', 'close')
272
273    def authenticate(self, request):
274        """
275        Main handler for authenticated objects.
276
277        @type request: instance
278        @param request: twisted.web.server.Request instance
279
280        @rtype: bool
281        @return: Whether the user authenticated sucessfully.
282        """
283
284        # Get up to date configuration values everytime there is a request
285        self.configuration = self.gui.configuration
286
287        self.request = request
288        self._prompt_auth()
289        self.username = self.request.getUser()
290        self.password = self.request.getPassword()
291        self.ip = self.request.getClientIP()
292        self.time = datetime.datetime.now()
293       
294        if not self.username and not self.password:
295            self.gui.log.failure(('AuthenticatedResource', 'authenticate'), (_('Client provided empty username and password'), _('Client %s provided empty username and password') % (self.ip)), 'WARNING')
296            self._prompt_auth()
297        else:
298            if self.username != self.configuration['server']['username'] or self.password != self.configuration['server']['password']:
299                self.gui.log.failure(('AuthenticatedResource', 'authenticate'), (_('Client provided incorrect username and password'), _('Client %(ip)s provided incorrect username and password: %(username)s:%(password)s') % {'ip': self.ip, 'username': self.username, 'password': self.password}), 'WARNING')
300                self._prompt_auth()
301            elif self.username == self.configuration['server']['username'] and self.password == self.configuration['server']['password']:
302                self.authenticated = True
303        return self.authenticated
304
305    def return_object_data(self):
306        """
307        Returns the data passed by set_request_data() or the default forbidden string if authentication failed.
308
309        @rtype: str
310        @return: self.data or self.noauth
311        """
312
313        if self.request_data_set and self.authenticated:
314            self.request.setResponseCode(http.OK)
315            self.request.setHeader('Content-Type', self.type)
316            self.request.setHeader('Content-Length', self.size)
317            self.request.setHeader('Connection', 'close')
318            # Deauthenticate if it's the screenshot (last object request)
319            if self.session_end:
320                self.authenticated = False
321            self.request_data_set = True
322            return self.data
323        else:
324            # No authentication given
325            return self.noauth
326   
327class DataResource(static.Data):
328    """
329    Generic Resource for data
330    """
331
332    def __init__(self, gui_instance, auth_instance, data, type='text/html; charset=UTF-8'):
333
334        """
335        Constructor that inherits code from resource.Resource->static.Data
336
337        @type gui_instance: Gui
338        @param gui_instance: An instance of our L{Gui} class
339
340        @type auth_instance: AuthenticatedResource
341        @param auth_instance: An instance of our L{AuthenticatedResource} class
342
343        @type data: string
344        @param data: The HTML to be displayed
345
346        @type type: str
347        @param type: The type of data we are serving
348        """
349
350        self.children = {}
351
352        self.gui = gui_instance
353        self.auth = auth_instance
354        self.configuration = self.gui.configuration
355
356        self.data = data
357        self.size = str(len(self.data))
358        self.type = type
359
360    def render(self, request):
361        """
362        Override twisted.static.Data render method. Render our static HTML
363
364        @type request: instance
365        @param request: twisted.web.server.Request instance
366        """
367       
368        # Get up to date configuration values everytime there is a request
369        self.configuration = self.gui.configuration
370        self.request = request
371
372        if self.configuration['server']['authentication']:
373            if self.auth.authenticate(self.request):
374                self.auth.set_request_data(self.data, self.size, self.type)
375            return self.auth.return_object_data()
376        else:
377            self.request.setHeader('Content-Type', self.type)
378            self.request.setHeader('Content-Length', self.size)
379            self.request.setHeader('Connection', 'close')
380            return self.data
381
382class FileResource(resource.Resource):
383    """
384    Generic Resource for file objects
385    """
386
387    def __init__(self, gui_instance, auth_instance, path, type):
388        """
389        Constructor
390
391        @type gui_instance: Gui
392        @param gui_instance: An instance of our L{Gui} class
393
394        @type auth_instance: AuthenticatedResource
395        @param auth_instance: An instance of our L{AuthenticatedResource} class
396
397        @type path: string
398        @param path: The path to the file to be served
399
400        @type type: str
401        @param type: The MIME-type to bepassed to Content-Type
402        """
403
404        self.children = {}
405
406        self.gui = gui_instance
407        self.auth = auth_instance
408        self.itaka_globals = self.gui.itaka_globals
409           
410        self.type = type
411        self.data = open(path, 'rb').read()
412        self.size = str(len(self.data))
413
414    def render_GET(self, request):
415        """
416        Handle GET requests
417
418        @type request: instance
419        @param request: twisted.web.server.Request instance
420        """
421
422        self.configuration = self.gui.configuration
423        self.request = request
424
425        if self.configuration['server']['authentication']:
426            if self.auth.authenticated or self.auth.authenticate(self.request):
427                self.auth.set_request_data(self.data, self.size, self.type, True)
428            return self.auth.return_object_data()
429        else:
430            self.request.setHeader('Content-Type', self.type)
431            self.request.setHeader('Content-Length', self.size)
432            self.request.setHeader('Connection', 'close')
433            return self.data
434
435class ScreenshotResource(resource.Resource):
436    """
437    Handle request and call for a screenshot
438    """
439
440    def __init__(self, gui_instance, auth_instance):
441        """
442        Constructor
443
444        @type gui_instance: Gui
445        @param gui_instance: An instance of our L{Gui} class
446
447        @type auth_instance: AuthenticatedResource
448        @param auth_instance: An instance of our L{AuthenticatedResource} class
449        """
450
451        self.children = {}
452
453        self.gui = gui_instance
454        self.auth = auth_instance
455        self.console = self.gui.console
456        self.itaka_globals = self.gui.itaka_globals
457
458        self.screenshot = screenshot.Screenshot(self.gui)
459       
460        #: Server hits counter
461        self.counter = 0
462
463    def get_screenshot(self):
464        """
465        Takes a screenshot and notifies the GUI.
466        """
467
468        self.ip = self.request.getClientIP()
469        self.time = datetime.datetime.now()
470
471        try:
472            self.shot_file = self.screenshot.take_screenshot()
473        except error.ItakaScreenshotError, e:
474            raise error.ItakaScreenshotError, e
475
476        self.data = open(self.shot_file, 'rb').read()
477        self.size = str(len(self.data))
478        self.counter += 1
479
480        if self.configuration['server']['notify'] and self.itaka_globals.notify_available:
481            import pynotify
482
483            # 48x48 image by default looks bad in Ubuntu
484            if self.configuration['server']['authentication']:
485                uri = "file://" + (os.path.join(self.itaka_globals.image_dir, "itaka-secure-take.png"))
486            else:
487                uri = "file://" + (os.path.join(self.itaka_globals.image_dir, "itaka-take.png"))
488           
489            n = pynotify.Notification(_('Screenshot taken'), _('%s captured the screen' % (self.ip)), uri)
490
491            n.set_timeout(1500)
492            n.attach_to_status_icon(self.gui.status_icon)
493            n.show()
494        # if self.configuration['server']['notifysound']
495        if self.itaka_globals.platform == 'Windows':
496            import winsound
497            # ASYNC lets the program continue and does not wait for end of play.
498            winsound.PlaySound(os.path.join(self.itaka_globals.sound_dir, "snap.wav"), winsound.SND_FILENAME|winsound.SND_ASYNC)
499        elif self.itaka_globals.platform == 'Linux':
500                """
501               from wave import open as waveOpen
502               from ossaudiodev import open as ossOpen
503               s = waveOpen(os.path.join(self.itaka_globals.sound_dir, "snap.wav"),'rb')
504               (nc,sw,fr,nf,comptype, compname) = s.getparams( )
505               dsp = ossOpen('/dev/dsp','w')
506               try:
507                 from ossaudiodev import AFMT_S16_NE
508               except ImportError:
509                 if byteorder == "little":
510                   AFMT_S16_NE = ossaudiodev.AFMT_S16_LE
511                 else:
512                   AFMT_S16_NE = ossaudiodev.AFMT_S16_BE
513               dsp.setparameters(AFMT_S16_NE, nc, fr)
514               data = s.readframes(nf)
515               s.close()
516               dsp.write(data)
517               dsp.close()
518               """
519       
520        self.gui.update_gui(self.counter, self.ip, self.time)
521
522    def render_GET(self, request):
523        """
524        Handle GET requests for screenshot
525
526        @type request: instance
527        @param request: twisted.web.server.Request instance
528        """
529
530        # Get up to date configuration values everytime there is a request
531        self.configuration = self.gui.configuration
532        self.request = request
533        self.type = "image/" + self.configuration['screenshot']['format']
534
535        if self.configuration['server']['authentication']:
536            if self.auth.authenticated or self.auth.authenticate(self.request):
537                try:
538                    self.get_screenshot()
539                except error.ItakaScreenshotError:
540                    return
541                self.auth.set_request_data(self.data, self.size, self.type, True)
542            return self.auth.return_object_data()
543        else:
544            try:
545                self.get_screenshot()
546            except error.ItakaScreenshotError:
547                return
548            self.request.setHeader('Content-Type', self.type)
549            self.request.setHeader('Content-Length', self.size)
550            self.request.setHeader('Connection', 'close')
551            return self.data
552
Note: See TracBrowser for help on using the repository browser.