source: tags/0.2/server.py @ 335

Revision 215, 11.3 KB checked in by marc, 5 years ago (diff)

Minor UI changes, debug meta option in config, fixed a major bug in screenshooting active windows

  • 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):
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
101        self.site = server.Site(resource)
102
103    def start_server(self, port):
104        """
105        Start the server.
106
107        @type port: int
108        @param port: Port number to listen on.
109        """
110
111        try:
112            self.server = reactor.listenTCP(port, self.site)
113        except twisted.internet.error.CannotListenError, e:
114            raise error.ItakaServerErrorCannotListen, e
115
116        self.server_listening = True
117   
118    def stop_server(self):
119        """
120        Stop the server.
121        """
122
123        self.server.stopListening()
124        self.server_listening = False
125
126    def listening(self):
127        """
128        Whether the server is listening or not.
129
130        @rtype: bool
131        @return: True if it's listening or False if it is not.
132        """
133
134        return self.server_listening
135
136    def add_log_observer(self, observer):
137        """
138        Add a twisted.log observer.
139       
140        @type observer: method
141        @param observer: A method to send the logs to.
142        """
143
144        self.log_observer = observer
145        log.addObserver(observer)
146
147    def remove_log_observer(self, observer=False):
148        """
149        Remove a twisted.log observer.
150       
151        @type observer: method
152        @param observer: The name of the method specified in add_log_observer. If False, the last known log observer added will be removed.
153        """
154
155        if observer:
156            log.removeObserver(observer)
157        else:
158            log.removeObserver(self.log_observer)
159
160    def get_listening_information(self):
161        """
162        Returns the information of the current listening server.
163
164        @rtype: twisted.internet.interfaces.IAddress
165        @return: The current server's networking information.
166        """
167
168        if self.server_listening:
169            return self.server.getHost()
170
171class ScreenshotServer(BaseHTTPServer):
172    """
173    Screenshot server that builds upon BaseHTTPServer.
174    """
175
176    def __init__(self, guiinstance):
177        """
178        Constructor. Overrides BaseHTTPServer's __init__ to create our resources on-the-fly.
179
180        @type guiinstance: instance
181        @param guiinstance: An instance of our L{Gui} class.
182        """
183
184        self.gui = guiinstance
185        self.itakaglobals = self.gui.itakaglobals
186        self.configuration = self.gui.configuration
187        self.console = self.gui.console
188
189        self.server_listening = False
190
191        # Here we use our own static.Data special child resource because we need Authentication handling.
192        # Otherwise we would just use our own self.add_static_resource.
193        self.root = RootResource(self.gui, self.itakaglobals.headhtml + self.configuration['html']['html'] + self.itakaglobals.footerhtml)
194        self.add_child_to_resource('root', '', self.root)
195        self.add_child_to_resource('root', 'screenshot', ScreenshotResource(self.gui))
196        self.create_site(self.root)
197
198class RootResource(static.Data):
199    """
200    Main resource with authentication support.
201
202    Please read RFC 2617 to understand the HTTP Authentication process.
203    """
204
205    def __init__(self, guiinstance, data, type='text/html; charset=UTF-8'):
206
207        """
208        Constructor that inherits code from resource.Resource->static.Data.
209
210        @type guiinstance: instance
211        @param guiinstance: An instance of our L{Gui} class.
212
213        @type html: string
214        @param html: The HTML to be displayed.
215
216        @type type: str
217        @param type: The type of data we are serving.
218        """
219
220        self.gui = guiinstance
221        self.console = self.gui.console
222        self.itakaglobals = self.gui.itakaglobals
223        self.configuration = self.gui.configuration
224
225        # Inherited from the actual code of Twisted's static.Data
226        self.children = {}
227        self.data = data
228        self.type = type
229
230        self.noauth = self.itakaglobals.headhtml + self.configuration['html']['authfailure'] + self.itakaglobals.footerhtml
231
232    def _promptAuth(self):
233        """
234        Prompt the authorization dialog on the browser.
235        """
236
237        self.request.setHeader('WWW-Authenticate', 'Basic realm="Itaka Screenshot Server"')
238        self.request.setResponseCode(http.UNAUTHORIZED)
239        self.request.setHeader('Content-Type', self.type)
240        self.request.setHeader('Connection', 'close')
241        self.request.setHeader('Content-Length', str(len(self.noauth)))
242
243    def render(self, request):
244        """
245        Override twisted.static.Data render method. Render our static HTML.
246
247        @type request: instance
248        @param request: twisted.web.server.Request instance.
249        """
250       
251        # Get up to date configuration values everytime there is a request
252        self.configuration = self.gui.configuration
253
254        self.request = request
255        self.ip = self.request.getClientIP()
256        self.time = datetime.datetime.now()
257
258        if self.configuration['server']['authentication']:
259            self._promptAuth()
260            self.username = self.request.getUser()
261            self.password = self.request.getPassword()
262           
263            if not self.username and not self.password:
264                self.gui.log.failure(('RootResource', 'render'), ('Client provided empty username and password', 'Client %s provided empty username and password' % (self.ip)), 'WARNING')
265                self._promptAuth()
266            else:
267                if self.username != self.configuration['server']['username'] or self.password != self.configuration['server']['password']:
268                    self.gui.log.failure(('RootResource', 'render'), ('Client provided incorrect username and password', 'Client %s provided incorrect username and password: %s:%s' % (self.ip, self.username, self.password)), 'WARNING')
269                    self._promptAuth()
270                elif self.username == self.configuration['server']['username'] and self.password == self.configuration['server']['password']:
271                    self.request.setResponseCode(http.OK)
272                    self.request.setHeader('Content-Type', self.type)
273                    self.request.setHeader('Connection', 'close')
274                    self.request.setHeader('Content-Length', str(len(self.data)))
275                    return self.data
276        else:
277            self.request.setHeader('Content-Type', self.type)
278            self.request.setHeader('Connection', 'close')
279            self.request.setHeader('Content-Length', str(len(self.data)))
280            return self.data
281
282        # No auth given
283        return self.noauth
284
285class ScreenshotResource(resource.Resource):
286    """
287    Handle server requests and call for a screenshot.
288    """
289
290    def __init__(self, guiinstance):
291        """
292        Constructor.
293
294        @type guiinstance: instance
295        @param guiinstance: An instance of our L{Gui} class.
296        """
297
298        self.gui = guiinstance
299        self.console = self.gui.console
300        self.itakaglobals = self.gui.itakaglobals
301        self.screenshot = screenshot.Screenshot(self.gui)
302       
303        #: Server hits counter
304        self.counter = 0
305
306    def render_GET(self, request):
307        """
308        Handle GET requests for screenshot.
309
310        @type request: instance
311        @param request: twisted.web.server.Request instance.
312
313        @rtype: str
314        @return: Screenshot image.
315        """
316
317        # Get up to date configuration values everytime there is a request
318        self.configuration = self.gui.configuration
319
320        self.request = request
321        self.ip = self.request.getClientIP()
322        self.time = datetime.datetime.now()
323
324        if (self.request.uri == "/screenshot"):
325            try:
326                self.shotFile = self.screenshot.take_screenshot()
327            except error.ItakaScreenshotError:
328                return
329           
330            self.request.setHeader('Content-Type', "image/" + self.configuration['screenshot']['format'])
331            self.request.setHeader('Content-Length', str(len(self.shotFile)))
332            self.request.setHeader('Connection', 'close')
333
334            self.counter += 1
335
336            if self.configuration['server']['notify'] and self.itakaglobals.notifyavailable:
337                import pynotify
338                uri = "file://" + (os.path.join(self.itakaglobals.image_dir, "itaka-take.png"))
339
340                n = pynotify.Notification("Screenshot taken",
341                "%s requested screenshot"
342                % (self.ip), uri)
343
344                n.set_timeout(1500)
345                n.attach_to_status_icon(self.gui.statusIcon)
346                n.show()
347
348            self.gui.update_gui(self.counter, self.ip, self.time)
349
350            return open(self.shotFile, 'rb').read()
Note: See TracBrowser for help on using the repository browser.