1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | import locale |
---|
5 | locale.setlocale(locale.LC_ALL, 'C.UTF-8') |
---|
6 | |
---|
7 | """ |
---|
8 | ******************************************************************************* |
---|
9 | * |
---|
10 | * $Id: SecureDocXMLRPCServer.py 4 2008-06-04 18:44:13Z yingera $ |
---|
11 | * $URL: https://xxxxxx/repos/utils/trunk/tools/SVNRPCServer.py $ |
---|
12 | * |
---|
13 | * $Date: 2008-06-04 13:44:13 -0500 (Wed, 04 Jun 2008) $ |
---|
14 | * $Author: yingera $ |
---|
15 | * |
---|
16 | * Authors: Laszlo Nagy, Andrew Yinger |
---|
17 | * |
---|
18 | * Description: Threaded, Documenting SecureDocXMLRPCServer.py - over HTTPS. |
---|
19 | * |
---|
20 | * requires pyOpenSSL: http://sourceforge.net/project/showfiles.php?group_id=31249 |
---|
21 | * ...and open SSL certs installed. |
---|
22 | * |
---|
23 | * Based on this article: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81549 |
---|
24 | * |
---|
25 | ******************************************************************************* |
---|
26 | """ |
---|
27 | |
---|
28 | import SocketServer |
---|
29 | import BaseHTTPServer |
---|
30 | import SimpleHTTPServer |
---|
31 | import SimpleXMLRPCServer |
---|
32 | import imp |
---|
33 | import glob |
---|
34 | |
---|
35 | |
---|
36 | import fcntl |
---|
37 | import time |
---|
38 | import random |
---|
39 | import base64 |
---|
40 | import socket, os |
---|
41 | from OpenSSL import SSL |
---|
42 | from threading import Event, currentThread, Thread, Condition |
---|
43 | from thread import start_new_thread as start |
---|
44 | from DocXMLRPCServer import DocXMLRPCServer, DocXMLRPCRequestHandler |
---|
45 | |
---|
46 | import xmlrpclib |
---|
47 | import threading |
---|
48 | |
---|
49 | threading._DummyThread._Thread__stop = lambda x: 42 |
---|
50 | |
---|
51 | #locale.resetlocale() |
---|
52 | |
---|
53 | # static stuff |
---|
54 | |
---|
55 | ''' |
---|
56 | DEFAULTKEYFILE='/etc/lliurex-secrets/pki/n4d/n4d.key' # Replace with your PEM formatted key file |
---|
57 | DEFAULTCERTFILE='/etc/lliurex-secrets/certs/n4d/n4d' # Replace with your PEM formatted certificate file |
---|
58 | ''' |
---|
59 | |
---|
60 | DEFAULTKEYFILE='/etc/n4d/cert/n4dkey.pem' # Replace with your PEM formatted key file |
---|
61 | DEFAULTCERTFILE='/etc/n4d/cert/n4dcert.pem' # Replace with your PEM formatted certificate file |
---|
62 | |
---|
63 | SITESENABLEDPATH="/usr/share/n4d/sites-enabled/" |
---|
64 | |
---|
65 | TRIGGER_BLOCK=False |
---|
66 | |
---|
67 | |
---|
68 | class SecureDocXMLRpcRequestHandler(DocXMLRPCRequestHandler): |
---|
69 | """Secure Doc XML-RPC request handler class. |
---|
70 | It it very similar to DocXMLRPCRequestHandler but it uses HTTPS for transporting XML data. |
---|
71 | """ |
---|
72 | |
---|
73 | |
---|
74 | def setup(self): |
---|
75 | self.connection = self.request |
---|
76 | self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) |
---|
77 | self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) |
---|
78 | |
---|
79 | def address_string(self): |
---|
80 | "getting 'FQDN' from host seems to stall on some ip addresses, so... just (quickly!) return raw host address" |
---|
81 | host, port = self.client_address |
---|
82 | #print dir(self) |
---|
83 | try: |
---|
84 | self.client_url=self.headers["host"] |
---|
85 | except Exception as e: |
---|
86 | self.client_url=None |
---|
87 | #return socket.getfqdn(host) |
---|
88 | return host |
---|
89 | |
---|
90 | |
---|
91 | def do_POST(self): |
---|
92 | """Handles the HTTPS POST request. |
---|
93 | It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly. |
---|
94 | """ |
---|
95 | try: |
---|
96 | # get arguments |
---|
97 | data = self.rfile.read(int(self.headers["content-length"])) |
---|
98 | # In previous versions of SimpleXMLRPCServer, _dispatch |
---|
99 | # could be overridden in this class, instead of in |
---|
100 | # SimpleXMLRPCDispatcher. To maintain backwards compatibility, |
---|
101 | # check to see if a subclass implements _dispatch and dispatch |
---|
102 | # using that method if present. |
---|
103 | addr,num=self.client_address |
---|
104 | response = self.server._marshaled_dispatch(data, getattr(self, '_dispatch', None),client_address=addr) |
---|
105 | |
---|
106 | |
---|
107 | except: # This should only happen if the module is buggy |
---|
108 | # internal error, report as HTTP server error |
---|
109 | self.send_response(500) |
---|
110 | self.end_headers() |
---|
111 | else: |
---|
112 | # got a valid XML RPC response |
---|
113 | self.send_response(200) |
---|
114 | self.send_header("Content-type", "text/xml") |
---|
115 | self.send_header("Content-length", str(len(response))) |
---|
116 | self.end_headers() |
---|
117 | self.wfile.write(response) |
---|
118 | |
---|
119 | # shut down the connection |
---|
120 | self.wfile.flush() |
---|
121 | ''' |
---|
122 | global TRIGGER_BLOCK |
---|
123 | while(TRIGGER_BLOCK): |
---|
124 | print "waiting for trigger_block to be unlocked..." |
---|
125 | time.sleep(int(5*random.random())) |
---|
126 | TRIGGER_BLOCK=True |
---|
127 | print "done with response. Sleeping..." |
---|
128 | time.sleep(5) |
---|
129 | print "done sleeping." |
---|
130 | TRIGGER_BLOCK=False |
---|
131 | ''' |
---|
132 | self.connection.shutdown() # Modified here! |
---|
133 | |
---|
134 | def do_GET(self): |
---|
135 | """Handles the HTTP GET request. |
---|
136 | |
---|
137 | Interpret all HTTP GET requests as requests for server |
---|
138 | documentation. |
---|
139 | """ |
---|
140 | |
---|
141 | self.address_string() |
---|
142 | |
---|
143 | web_modules_list=os.listdir(SITESENABLEDPATH) |
---|
144 | path=self.path.lstrip("/") |
---|
145 | |
---|
146 | |
---|
147 | if path+".py" in web_modules_list: |
---|
148 | |
---|
149 | try: |
---|
150 | obj=imp.load_source(path,SITESENABLEDPATH+path+".py") |
---|
151 | inst=getattr(obj,path)() |
---|
152 | info={} |
---|
153 | info["client_ip"]=self.client_address[0] |
---|
154 | info["client_url"]=self.client_url |
---|
155 | response=inst.get_response(info) |
---|
156 | self.send_response(200) |
---|
157 | self.send_header("Content-type", "text/html") |
---|
158 | self.send_header("Content-length", str(len(response))) |
---|
159 | self.end_headers() |
---|
160 | self.wfile.write(response) |
---|
161 | return |
---|
162 | |
---|
163 | except Exception as e: |
---|
164 | print e |
---|
165 | |
---|
166 | |
---|
167 | ''' |
---|
168 | if "/n4d/" in self.path: |
---|
169 | |
---|
170 | if os.path.exists(self.path) and os.path.isfile(self.path): |
---|
171 | |
---|
172 | file_length=os.stat(self.path).st_size |
---|
173 | f=open(self.path,"rB") |
---|
174 | response=f.read(file_length) |
---|
175 | f.close() |
---|
176 | |
---|
177 | self.send_response(200) |
---|
178 | self.send_header("Content-type","text/html") |
---|
179 | self.send_header("Content-length",file_length) |
---|
180 | self.end_headers() |
---|
181 | |
---|
182 | self.wfile.write(response) |
---|
183 | |
---|
184 | return |
---|
185 | ''' |
---|
186 | |
---|
187 | # Check that the path is legal |
---|
188 | |
---|
189 | if not self.is_rpc_path_valid(): |
---|
190 | self.report_404() |
---|
191 | return |
---|
192 | |
---|
193 | |
---|
194 | #print self.path |
---|
195 | #response = self.server.generate_html_documentation() |
---|
196 | |
---|
197 | header="<html><body>" |
---|
198 | foot="</body></html>" |
---|
199 | |
---|
200 | response=header |
---|
201 | |
---|
202 | for plugin in l.cm.plugins: |
---|
203 | response+="<b>["+plugin.type+":"+plugin.class_name+"]</b><br>" |
---|
204 | for method in plugin.function: |
---|
205 | try: |
---|
206 | #server.register_function(getattr(l.objects[plugin.class_name],method)) |
---|
207 | args=getattr(l.objects[plugin.class_name],method).func_code.co_varnames[:getattr(l.objects[plugin.class_name],method).func_code.co_argcount] |
---|
208 | response+="<pre> " + method+str(args).replace("'self',","")+" , " + str(getattr(l.objects[plugin.class_name],method).__doc__) + "</pre>" |
---|
209 | except: |
---|
210 | pass |
---|
211 | |
---|
212 | response+=foot |
---|
213 | |
---|
214 | |
---|
215 | |
---|
216 | self.send_response(200) |
---|
217 | self.send_header("Content-type", "text/html") |
---|
218 | self.send_header("Content-length", str(len(response))) |
---|
219 | self.end_headers() |
---|
220 | self.wfile.write(response) |
---|
221 | |
---|
222 | # shut down the connection |
---|
223 | self.wfile.flush() |
---|
224 | self.connection.shutdown() # Modified here! |
---|
225 | |
---|
226 | |
---|
227 | |
---|
228 | def report_404 (self): |
---|
229 | # Report a 404 error |
---|
230 | self.send_response(404) |
---|
231 | response = 'No such page' |
---|
232 | self.send_header("Content-type", "text/plain") |
---|
233 | self.send_header("Content-length", str(len(response))) |
---|
234 | self.end_headers() |
---|
235 | self.wfile.write(response) |
---|
236 | # shut down the connection |
---|
237 | self.wfile.flush() |
---|
238 | self.connection.shutdown() # Modified here! |
---|
239 | |
---|
240 | def do_OPTIONS(self): |
---|
241 | self.send_response(200) |
---|
242 | self.end_headers() |
---|
243 | self.wfile.flush() |
---|
244 | self.connection.shutdown() # Modified here! |
---|
245 | |
---|
246 | def end_headers(self): |
---|
247 | self.send_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") |
---|
248 | self.send_header("Access-Control-Allow-Origin", "*") |
---|
249 | DocXMLRPCRequestHandler.end_headers(self) |
---|
250 | |
---|
251 | class CustomThreadingMixIn: |
---|
252 | """Mix-in class to handle each request in a new thread.""" |
---|
253 | # Decides how threads will act upon termination of the main process |
---|
254 | daemon_threads = True |
---|
255 | |
---|
256 | def process_request_thread(self, request, client_address): |
---|
257 | """Same as in BaseServer but as a thread. |
---|
258 | In addition, exception handling is done here. |
---|
259 | """ |
---|
260 | try: |
---|
261 | self.finish_request(request, client_address) |
---|
262 | self.close_request(request) |
---|
263 | except (socket.error, SSL.SysCallError), why: |
---|
264 | print 'socket.error finishing request from "%s"; Error: %s' % (client_address, str(why)) |
---|
265 | self.close_request(request) |
---|
266 | except: |
---|
267 | self.handle_error(request, client_address) |
---|
268 | self.close_request(request) |
---|
269 | |
---|
270 | def process_request(self, request, client_address): |
---|
271 | """Start a new thread to process the request.""" |
---|
272 | t = Thread(target = self.process_request_thread, args = (request, client_address)) |
---|
273 | if self.daemon_threads: |
---|
274 | t.setDaemon(1) |
---|
275 | t.start() |
---|
276 | |
---|
277 | |
---|
278 | |
---|
279 | class SecureDocXMLRPCServer(CustomThreadingMixIn, DocXMLRPCServer): |
---|
280 | def __init__(self, registerInstance, server_address, keyFile=DEFAULTKEYFILE, certFile=DEFAULTCERTFILE, logRequests=True): |
---|
281 | """Secure Documenting XML-RPC server. |
---|
282 | It it very similar to DocXMLRPCServer but it uses HTTPS for transporting XML data. |
---|
283 | """ |
---|
284 | DocXMLRPCServer.__init__(self, server_address, SecureDocXMLRpcRequestHandler, logRequests,True) |
---|
285 | self.logRequests = logRequests |
---|
286 | |
---|
287 | # stuff for doc server |
---|
288 | try: self.set_server_title(registerInstance.title) |
---|
289 | except AttributeError: self.set_server_title('N4D Documentation page') |
---|
290 | try: self.set_server_name(registerInstance.name) |
---|
291 | except AttributeError: self.set_server_name('N4D') |
---|
292 | #for plugin in registerInstace |
---|
293 | if registerInstance.__doc__: self.set_server_documentation(registerInstance.__doc__) |
---|
294 | else: self.set_server_documentation('default documentation') |
---|
295 | |
---|
296 | |
---|
297 | #self.register_introspection_functions() |
---|
298 | |
---|
299 | # init stuff, handle different versions: |
---|
300 | try: |
---|
301 | SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,allow_none=True) |
---|
302 | except TypeError: |
---|
303 | # An exception is raised in Python 2.5 as the prototype of the __init__ |
---|
304 | # method has changed and now has 3 arguments (self, allow_none, encoding) |
---|
305 | SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, allow_none=True, encoding=None) |
---|
306 | SocketServer.BaseServer.__init__(self, server_address, SecureDocXMLRpcRequestHandler) |
---|
307 | self.register_instance(registerInstance) # for some reason, have to register instance down here! |
---|
308 | |
---|
309 | # SSL socket stuff |
---|
310 | ctx = SSL.Context(SSL.SSLv23_METHOD) |
---|
311 | ctx.use_privatekey_file(keyFile) |
---|
312 | ctx.use_certificate_file(certFile) |
---|
313 | self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type)) |
---|
314 | #self.socket = socket.socket(self.address_family, self.socket_type) |
---|
315 | |
---|
316 | old = fcntl.fcntl(self.socket.fileno(), fcntl.F_GETFD) |
---|
317 | fcntl.fcntl(self.socket.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC) |
---|
318 | |
---|
319 | self.server_bind() |
---|
320 | self.server_activate() |
---|
321 | |
---|
322 | # requests count and condition, to allow for keyboard quit via CTL-C |
---|
323 | self.requests = 0 |
---|
324 | self.rCondition = Condition() |
---|
325 | |
---|
326 | |
---|
327 | def startup(self): |
---|
328 | 'run until quit signaled from keyboard...' |
---|
329 | print '[SERVER] Starting; Press CTRL-C to quit ...' |
---|
330 | while True: |
---|
331 | try: |
---|
332 | self.rCondition.acquire() |
---|
333 | start(self.handle_request, ()) # we do this async, because handle_request blocks! |
---|
334 | while not self.requests: |
---|
335 | self.rCondition.wait(timeout=3.0) |
---|
336 | if self.requests: self.requests -= 1 |
---|
337 | self.rCondition.release() |
---|
338 | except KeyboardInterrupt: |
---|
339 | print "[SERVER] Exiting..." |
---|
340 | return |
---|
341 | |
---|
342 | def get_request(self): |
---|
343 | request, client_address = self.socket.accept() |
---|
344 | self.rCondition.acquire() |
---|
345 | self.requests += 1 |
---|
346 | self.rCondition.notifyAll() |
---|
347 | self.rCondition.release() |
---|
348 | return (request, client_address) |
---|
349 | |
---|
350 | def listMethods(self): |
---|
351 | 'return list of method names (strings)' |
---|
352 | methodNames = self.funcs.keys() |
---|
353 | methodNames.sort() |
---|
354 | return methodNames |
---|
355 | |
---|
356 | def methodHelp(self, methodName): |
---|
357 | 'method help' |
---|
358 | if methodName in self.funcs: |
---|
359 | return self.funcs[methodName].__doc__ |
---|
360 | else: |
---|
361 | raise Exception('method "%s" is not supported' % methodName) |
---|
362 | |
---|
363 | |
---|
364 | def _marshaled_dispatch(self, data, dispatch_method = None, path = None, client_address=None): |
---|
365 | self.allow_none=True |
---|
366 | try: |
---|
367 | params, method = xmlrpclib.loads(data) |
---|
368 | params=(client_address,)+params |
---|
369 | |
---|
370 | if dispatch_method is not None: |
---|
371 | response = dispatch_method(method, params) |
---|
372 | else: |
---|
373 | |
---|
374 | response = self._dispatch(method, params) |
---|
375 | |
---|
376 | response = (response,) |
---|
377 | response = xmlrpclib.dumps(response, methodresponse=1,allow_none=self.allow_none, encoding=self.encoding) |
---|
378 | |
---|
379 | except Fault, fault: |
---|
380 | response = xmlrpclib.dumps(fault, allow_none=self.allow_none,encoding=self.encoding) |
---|
381 | except: |
---|
382 | |
---|
383 | exc_type, exc_value, exc_tb = sys.exc_info() |
---|
384 | response = xmlrpclib.dumps(xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),encoding=self.encoding, allow_none=self.allow_none,) |
---|
385 | |
---|
386 | return response |
---|
387 | |
---|
388 | |
---|
389 | |
---|
390 | if __name__ == '__main__': |
---|
391 | |
---|
392 | obj=imp.load_source("core","/usr/share/n4d/xmlrpc-server/core.py") |
---|
393 | l=obj.Core() |
---|
394 | server_address = ('', 9779) # (address, port) |
---|
395 | server = SecureDocXMLRPCServer(l, server_address, DEFAULTKEYFILE, DEFAULTCERTFILE) |
---|
396 | #server.register_introspection_functions() |
---|
397 | ''' |
---|
398 | for plugin in l.cm.plugins: |
---|
399 | for method in plugin.function: |
---|
400 | server.register_function(getattr(l.objects[plugin.class_name],method)) |
---|
401 | ''' |
---|
402 | |
---|
403 | |
---|
404 | sa = server.socket.getsockname() |
---|
405 | print "[SERVER] HTTPS on", sa[0], "port", sa[1] |
---|
406 | server.startup() |
---|
407 | |
---|
408 | |
---|