source: tags/0.2.1/server.py @ 335

Revision 254, 15.2 KB checked in by marc, 5 years ago (diff)

Fixed typo, ported back rpm package

  • Property svn:keywords set to Id Rev
Line 
1#! /usr/bin/env python
2# -*- coding: utf8 -*-
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 2 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-2007 Marc E.
19# http://itaka.jardinpresente.com.ar
20#
21# $Id$
22
23""" Itaka server engine """
24
25import datetime, os, traceback, sys
26
27try:
28    import screenshot
29    import error
30except ImportError:
31    print "[*] ERROR: Failed to import Itaka modules"
32    traceback.print_exc()
33    sys.exit(1)
34
35try:
36    from twisted.python import log
37    from twisted.web import server, static, http, resource
38    from twisted.internet import reactor
39    import twisted.internet.error
40except ImportError:
41    print "[*] ERROR: Could not import Twisted Network Framework"
42    traceback.print_exc()
43    sys.exit(1)
44
45class BaseHTTPServer:
46    """
47    Base HTTP Server.
48    """
49
50    def __init__(self):
51        """
52        Constructor.
53        """
54
55        self.server_listening = False
56
57    def add_static_resource(self, name, data, type='text/html; charset=UTF-8'):
58        """
59        Create a static.Data Twisted resource.Resource.
60
61        @type name: str
62        @param name: Name of the resource.
63
64        @type data: str
65        @param data: Data in memory to add to the resource, typically HTML.
66
67        @type type: str
68        @param type: The type of data we are serving.
69
70        @rtype: resource.Resource
71        @return: The instance of the resource created.
72        """
73
74        setattr(self, name, static.Data(data, type))
75        return getattr(self, name)
76
77    def add_child_to_resource(self, name, path, resource):
78        """
79        Add a static child resource to a Twisted resource.Resource.
80
81        @type name: str
82        @param name: The name of the Twisted resource.Resource. 
83
84        @type path: str
85        @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 ''.
86
87        @type resource: instance
88        @param resource: A Twisted Resource.
89        """
90
91        getattr(self, name).putChild(path, resource)
92
93    def create_site(self, resource, version_header='TwistedWeb/' + twisted.copyright.version):
94        """
95        Creates a Twisted.server.Site with a Twisted Resource
96
97        @type resource: instance
98        @param resource: An instance of a Twisted resource created with L{add_static_resource}
99
100        @type version_header: str
101        @param version_header: The 'Server: str' that is sent on HTTP headers. Defaults to Twisted's.
102        """
103        server.version = version_header
104        self.site = server.Site(resource)
105
106    def start_server(self, port):
107        """
108        Start the server.
109
110        @type port: int
111        @param port: Port number to listen on.
112        """
113
114        try:
115            self.server = reactor.listenTCP(port, self.site)
116        except twisted.internet.error.CannotListenError, e:
117            raise error.ItakaServerErrorCannotListen, e
118
119        self.server_listening = True
120   
121    def stop_server(self):
122        """
123        Stop the server.
124        """
125
126        self.server.stopListening()
127        self.server_listening = False
128
129    def listening(self):
130        """
131        Whether the server is listening or not.
132
133        @rtype: bool
134        @return: True if it's listening or False if it is not.
135        """
136
137        return self.server_listening
138
139    def add_log_observer(self, observer):
140        """
141        Add a twisted.log observer.
142       
143        @type observer: method
144        @param observer: A method to send the logs to.
145        """
146
147        self.log_observer = observer
148        log.addObserver(observer)
149
150    def remove_log_observer(self, observer=False):
151        """
152        Remove a twisted.log observer.
153       
154        @type observer: method
155        @param observer: The name of the method specified in add_log_observer. If False, the last known log observer added will be removed.
156        """
157
158        if observer:
159            log.removeObserver(observer)
160        else:
161            log.removeObserver(self.log_observer)
162
163    def get_listening_information(self):
164        """
165        Returns the information of the current listening server.
166
167        @rtype: twisted.internet.interfaces.IAddress
168        @return: The current server's networking information.
169        """
170
171        if self.server_listening:
172            return self.server.getHost()
173
174class ScreenshotServer(BaseHTTPServer):
175    """
176    Screenshot server that builds upon BaseHTTPServer.
177    """
178
179    def __init__(self, guiinstance):
180        """
181        Constructor. Overrides BaseHTTPServer's __init__ to create our resources on-the-fly.
182
183        @type guiinstance: instance
184        @param guiinstance: An instance of our L{Gui} class.
185        """
186
187        self.gui = guiinstance
188        self.itaka_globals = self.gui.itakaglobals
189        self.configuration = self.gui.configuration
190        self.console = self.gui.console
191
192        self.server_listening = False
193
194        # Here we use our own static.Data special child resource because we need Authentication handling.
195        # Otherwise we would just use our own self.add_static_resource.
196
197        # Also we create our unique authentication handler this keeps track if the user authenticated or not
198        self.authresource = AuthenticatedResource(self.gui)
199        self.root = RootResource(self.gui, self.authresource, self.itaka_globals.headhtml + self.configuration['html']['html'] + self.itaka_globals.footerhtml)
200        self.add_child_to_resource('root', '', self.root)
201        self.add_child_to_resource('root', 'screenshot', ScreenshotResource(self.gui, self.authresource))
202
203        self.create_site(self.root, 'Itaka/%s (TwistedWeb/%s)' % (self.itaka_globals.version, twisted.copyright.version))
204
205class AuthenticatedResource:
206    """
207    Helper object to handle authentication for Resources.
208    Please read RFC 2617 to understand the HTTP Authentication process
209    """
210
211    def __init__(self, gui_instance):
212        """
213        Constructor that inherits code from resource.Resource->static.Data
214
215        @type gui_instance: instance
216        @param gui_instance: An instance of our L{Gui} class
217        """
218
219        self.gui = gui_instance
220        self.configuration = self.gui.configuration
221        self.itaka_globals = self.gui.itakaglobals
222
223        self.noauth = self.itaka_globals.headhtml + self.configuration['html']['authfailure'] + self.itaka_globals.footerhtml
224        self.request_data_set = False
225        self.authenticated = False
226
227    def set_request_data(self, data, size, type, session_end=False):
228        """
229        Set the information about the data we are handling
230
231
232        @type data: string
233        @param data: The data to be displayed
234
235        @type size: string
236        @param size: A string for Content-lenght
237
238        @type type: str
239        @param type: The type of data we are serving
240
241        @type session_end: bool
242        @param session_end: Whether this request is the last of a session. This deauthenticates the session.
243        """
244
245        self.request_data_set = True
246        self.data = data
247        self.size = size
248        self.type = type
249        self.session_end = session_end
250
251    def _prompt_auth(self):
252        """
253        Prompt the authorization dialog on the browser
254        """
255
256        self.authenticated = False
257        self.request.setHeader('WWW-Authenticate', 'Basic realm="Itaka Screenshot Server"')
258        self.request.setResponseCode(http.UNAUTHORIZED)
259        self.request.setHeader('Content-Type', 'text/html; charset=UTF-8')
260        self.request.setHeader('Content-Length', str(len(self.noauth)))
261        self.request.setHeader('Connection', 'close')
262
263    def authenticate(self, request):
264        """
265        Main handler for authenticated objects.
266
267        @type request: instance
268        @param request: twisted.web.server.Request instance
269
270        @rtype: bool
271        @return: Whether the user authenticated sucessfully.
272        """
273
274        # Get up to date configuration values everytime there is a request
275        self.configuration = self.gui.configuration
276
277        self.request = request
278        self._prompt_auth()
279        self.username = self.request.getUser()
280        self.password = self.request.getPassword()
281        self.ip = self.request.getClientIP()
282        self.time = datetime.datetime.now()
283       
284        if not self.username and not self.password:
285            self.gui.log.failure(('AuthenticatedResource', 'render'), ('Client provided empty username and password', 'Client %s provided empty username and password' % (self.ip)), 'WARNING')
286            self._prompt_auth()
287        else:
288            if self.username != self.configuration['server']['username'] or self.password != self.configuration['server']['password']:
289                self.gui.log.failure(('AuthenticatedResource', 'render'), ('Client provided incorrect username and password', 'Client %s provided incorrect username and password: %s:%s' % (self.ip, self.username, self.password)), 'WARNING')
290                self._prompt_auth()
291            elif self.username == self.configuration['server']['username'] and self.password == self.configuration['server']['password']:
292                self.authenticated = True
293        return self.authenticated
294
295    def return_object_data(self):
296        """
297        Returns the data passed by set_request_data() or the default forbidden string if authentication failed.
298
299        @rtype: str
300        @return: self.data or self.noauth
301        """
302
303        if self.request_data_set and self.authenticated:
304            self.request.setResponseCode(http.OK)
305            self.request.setHeader('Content-Type', self.type)
306            self.request.setHeader('Content-Length', self.size)
307            self.request.setHeader('Connection', 'close')
308            # Deauthenticate if it's the screenshot (last object request)
309            if self.session_end:
310                self.authenticated = False
311            self.request_data_set = True
312            return self.data
313        else:
314            # No authentication given
315            return self.noauth
316
317class RootResource(static.Data):
318    """
319    Main resource with authentication support.
320
321    Please read RFC 2617 to understand the HTTP Authentication process.
322    """
323
324    def __init__(self, guiinstance, auth_instance, data, type='text/html; charset=UTF-8'):
325
326        """
327        Constructor that inherits code from resource.Resource->static.Data.
328
329        @type guiinstance: instance
330        @param guiinstance: An instance of our L{Gui} class.
331
332        @type auth_instance: AuthenticatedResource
333        @param auth_instance: An instance of our L{AuthenticatedResource} class
334
335        @type html: string
336        @param html: The HTML to be displayed.
337
338        @type type: str
339        @param type: The type of data we are serving.
340        """
341
342        self.children = {}
343
344        self.gui = guiinstance
345        self.auth = auth_instance
346        self.console = self.gui.console
347        self.itakaglobals = self.gui.itakaglobals
348        self.configuration = self.gui.configuration
349
350        self.data = data
351        self.size = str(len(self.data))
352        self.type = type
353
354    def render(self, request):
355        """
356        Override twisted.static.Data render method. Render our static HTML.
357
358        @type request: instance
359        @param request: twisted.web.server.Request instance.
360        """
361       
362        # Get up to date configuration values everytime there is a request
363        self.configuration = self.gui.configuration
364        self.request = request
365
366        if self.configuration['server']['authentication']:
367            if self.auth.authenticate(self.request):
368                self.auth.set_request_data(self.data, self.size, self.type)
369            return self.auth.return_object_data()
370        else:
371            self.request.setHeader('Content-Type', self.type)
372            self.request.setHeader('Content-Length', self.size)
373            self.request.setHeader('Connection', 'close')
374            return self.data
375
376class ScreenshotResource(resource.Resource):
377    """
378    Handle server requests and call for a screenshot.
379    """
380
381    def __init__(self, guiinstance, auth_instance):
382        """
383        Constructor.
384
385        @type guiinstance: instance
386        @param guiinstance: An instance of our L{Gui} class.
387
388        @type auth_instance: AuthenticatedResource
389        @param auth_instance: An instance of our L{AuthenticatedResource} class
390        """
391
392        self.children = {}
393
394        self.gui = guiinstance
395        self.auth = auth_instance
396        self.console = self.gui.console
397        self.itaka_globals = self.gui.itakaglobals
398
399        self.screenshot = screenshot.Screenshot(self.gui)
400       
401        #: Server hits counter
402        self.counter = 0
403
404    def get_screenshot(self):
405        """
406        Takes a screenshot and notifies the GUI.
407        """
408
409        self.ip = self.request.getClientIP()
410        self.time = datetime.datetime.now()
411
412        try:
413            self.shot_file = self.screenshot.take_screenshot()
414        except error.ItakaScreenshotError, e:
415            raise error.ItakaScreenshotError, e
416
417        self.data = open(self.shot_file, 'rb').read()
418        self.size = len(self.data)
419        self.counter += 1
420
421        if self.configuration['server']['notify'] and self.itaka_globals.notifyavailable:
422            import pynotify
423            uri = "file://" + (os.path.join(self.itaka_globals.image_dir, "itaka-take.png"))
424
425            n = pynotify.Notification('Screenshot taken', '%s requested screenshot' % (self.ip), uri)
426
427            n.set_timeout(1500)
428            n.attach_to_status_icon(self.gui.statusIcon)
429            n.show()
430        self.gui.update_gui(self.counter, self.ip, self.time)
431
432    def render_GET(self, request):
433        """
434        Handle GET requests for screenshot
435
436        @type request: instance
437        @param request: twisted.web.server.Request instance
438        """
439
440        # Get up to date configuration values everytime there is a request
441        self.configuration = self.gui.configuration
442        self.request = request
443        self.type = "image/" + self.configuration['screenshot']['format']
444
445        if self.configuration['server']['authentication']:
446            if self.auth.authenticated or self.auth.authenticate(self.request):
447                try:
448                    self.get_screenshot()
449                except error.ItakaScreenshotError:
450                    return
451                self.auth.set_request_data(self.data, self.size, self.type, True)
452            return self.auth.return_object_data()
453        else:
454            try:
455                self.get_screenshot()
456            except error.ItakaScreenshotError:
457                return
458            self.request.setHeader('Content-Type', self.type)
459            self.request.setHeader('Content-Length', self.size)
460            self.request.setHeader('Connection', 'close')
461            return self.data
Note: See TracBrowser for help on using the repository browser.