-
Notifications
You must be signed in to change notification settings - Fork 0
/
selexorweb.py
380 lines (309 loc) · 12 KB
/
selexorweb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""
<Program Name>
selexorweb.py
<Started>
July 7, 2012
<Author>
Leonard Law
<Purpose>
Implements a server for SeleXor that allows users to access SeleXor through a
web interface.
<Usage>
Simply start this program via the command line with these parameters:
$ python selexorweb.py [instance_name]
The instance name can be anything; this is not visible by the user. There
should be a file named instance_name.conf that contains the configurations
that are to be used by SeleXor. A default configuration file is included.
"""
import BaseHTTPServer
import SocketServer
import selexorserver
import selexorexceptions
import os
import sys
import threading
import seattleclearinghouse_xmlrpc # Needed for checking auth. exceptions
import logging
import traceback
import selexorhelper
import ssl
import settings
import substitutions
# Raised when we cannot connect to the clearinghouse XMLRPC server
import xmlrpclib
import time
# We need to translate first, then import separately
# This is so that they do not overwrite python's open()
import repyhelper
# Used for serializing objects to comm. with clients
repyhelper.translate('serialize.repy')
# Used to read the nodestate transition key
repyhelper.translate('rsa.repy')
import serialize_repy
import rsa_repy
# This is a fix for slow response times for python's base http server.
# See: http://bugs.python.org/issue6085
def _bare_address_string(self):
host, port = self.client_address[:2]
return str(host)
BaseHTTPServer.BaseHTTPRequestHandler.address_string = _bare_address_string
# End slow respond time fix for python's base http server.
TEMPLATE_INDEX_FN = 'web_ui_template.html'
INDEX_FN = 'index.html'
WEB_PATH = './web/'
def main():
global logger
# Needed so that the event handler for the HTTP server can see the selexor server
global selexor_server
logger = selexorhelper.setup_logging("selexorweb")
# Generate the index file
_generate_request_form()
http_server = SelexorHTTPServer((settings.http_ip_addr, settings.http_port), SelexorHandler)
http_thread = threading.Thread(target=http_server.serve_forever)
nodestate_transition_key = rsa_repy.rsa_file_to_publickey(settings.path_to_nodestate_transition_key)
selexor_server = selexorserver.SelexorServer()
http_thread.start()
print "Listening for connections on", settings.http_ip_addr + ':' + str(settings.http_port)
# Run indefinitely until CTRL+C is pressed.
try:
while True:
time.sleep(1.0)
except KeyboardInterrupt, e:
pass
print "Stopping web server..."
http_server.shutdown()
print "Stopping SeleXor server..."
selexor_server.shutdown()
print "Shutdown Complete."
# Browsers now perform pre-connections. This means that they will spawn multiple connections
# to the web server in order for pages to load faster. This is bad for us because the
# HTTP server is single-threaded by default. If we happen to handle one of the preconnect connections
# and don't receive any data from it (browser is sending request information on another connection)
# we end up blocking until the preconnect connection times out. We use the ThreadingMixIn to handle
# all of these connections simultaneously, so that the browser can actually react to the initial page
# request and send more requests along the preconnects.
class SelexorHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def __init__(self, address_tuple, handler_class):
BaseHTTPServer.HTTPServer.__init__(self, address_tuple, handler_class)
if settings.enable_https:
# Enable SSL support
self.socket = ssl.wrap_socket(
self.socket,
certfile=settings.path_to_ssl_certificate,
keyfile=settings.path_to_ssl_key,
server_side=True)
def _generate_request_form():
'''
<Purpose>
Takes the web index file and replaces every instance of [[ VAR_NAME ]] with its
substitution, found within config. The outputted file will be index.html.
<Parameters>
config: A dictionary obtained from _modify_config_with_file().
<Exceptions>
IOError
<Side Effects>
Generates an index file for this instance of selexor and places it in the current directory.
<Return>
None
'''
# This is the source file to parse
srcfile = open(os.path.normpath(WEB_PATH + TEMPLATE_INDEX_FN), 'r')
# This is the file that will contain the outputted data.
destfile = open(os.path.normpath(WEB_PATH + INDEX_FN), 'w')
data = srcfile.readline()
lineno = 0
while data:
while '}}' in data:
# For every '{{ x }}' pair, check what x is and replace it with the
# corresponding values.
before, remainder = data.split('{{', 1)
token, remainder = remainder.split('}}', 1)
token = token.strip()
# We have no way of determining ahead of time which substitutions will
# be defined without having to list them here... If it isn't defined,
# then let's just allow the exception to terminate the program.
replacement = getattr(substitutions, token)
result = before + replacement
destfile.write(result)
# Check the remainder
data = remainder
# Write anything left over
destfile.write(data)
data = srcfile.readline()
lineno += 1
srcfile.close()
destfile.close()
class SelexorHandler(BaseHTTPServer.BaseHTTPRequestHandler):
'''
<Purpose>
Selexor handler for use with the BaseHTTPServer class.
<Side Effects>
Will serve requests pointing to files in the WEB_PATH directory.
Also, will communicate with the selexorserver to perform user authentication
and host requests.
<Example Use>
http_server = BaseHTTPServer.HTTPServer(IP, PORT), SelexorHandler)
'''
def do_GET(self):
''' Serves files that are needed for the SeleXor web client. '''
# Check what the client is requesting
if self.path[1:]:
# Requesting specific file
filepath = self.path[1:]
else:
# Requesting index
filepath = INDEX_FN
filepath = WEB_PATH + filepath
# Write the header
dataFile = None
try:
# Image files are binary, reading them in line mode causes corruption
dataFile = open(filepath, 'rb')
# How long is this file?
dataFile.seek(0, 2)
data_length = dataFile.tell()
dataFile.seek(0, 0)
# Set up webpage headers
self.send_response(200)
self.send_header("Content-type", self._get_mime_type_from_path(filepath))
self.send_header("Content-Length", str(data_length))
self.send_header("Connection", "Close")
except IOError, e:
# Cannot find file
logger.error(str(e))
# We can't find the file, send HTTP 404 NOT FOUND error message
self.send_response(404)
finally:
self.end_headers()
# Writing to self.wfile MUST occur after ending the headers.
if dataFile:
# Put the file's contents to the write buffer
# Read file in increments to avoid memory overflow
chunksize = 1000
data = dataFile.read(chunksize)
while data:
self.wfile.write(data)
data = dataFile.read(chunksize)
dataFile.close()
def do_POST(self):
'''
<Purpose>
Responds to POST messages and handles them accordingly. Expects data in
the JSON format.
<Arguments>
None
<Exception>
Exceptions thrown by _parse_post_data().
<Side Effects>
Calls a handler depending on the type of message received.
<Returns>
None
'''
# The handlers should take the following parameters:
# data: The data expected by the handler.
# remoteip: The IP address of the remote machine.
#
# Whatever that is returned by the handler will be put into the 'data'
# key of the response dictionary.
self.action_handlers = {
'check_available': self._check_available_vessels,
'request': self._handle_host_request,
'query': self._handle_status_query,
'release': self._release_vessel,
}
remoteip = self.client_address[0]
# Only read the amount of data that is specified.
rawdata = self.rfile.read(int(self.headers.getheader("Content-Length")))
response = {}
try:
postdict = serialize_repy.serialize_deserializedata(rawdata)
action = postdict.keys()[0]
response['action'] = action + "_response"
if action in self.action_handlers:
data_to_send = self.action_handlers[action](postdict[action], remoteip)
else:
raise selexorexceptions.SelexorInvalidRequest("Unknown Action: " + action)
response['status'] = 'ok'
response['data'] = data_to_send
output = serialize_repy.serialize_serializedata(response)
except:
# Catch all exceptions/errors that happen and log them.
# Then tell the user an internal error occurred.
logger.error("Unknown error occurred while serving request.\n" + traceback.format_exc())
errstr = "An internal error occurred."
data_to_send = None
response['status'] = 'error'
response['error'] = errstr
# Send HTTP 200 OK message since this is a good request
self.send_response(200)
self.send_header("Content-Length", str(len(output)))
self.send_header("Connection", "Close")
self.end_headers()
self.wfile.write(output)
def _check_available_vessels(self, data, remoteip):
'''
Connects to the clearinghouse and returns a response_dict containing
the following keys:
'status':
'ok' on success /'error' on failure
'max_hosts':
The remaining number of hosts that the user can acquire.
On failure, this is '?'.
'default_port':
The user's assigned userport.
'error':
A description of the error that occurred. This only exists if an error
happened.
'''
response_dict = {}
try:
client = selexorhelper.connect_to_clearinghouse(data)
accinfo = client.get_account_info()
acquired_resources = client.get_resource_info()
response_dict['status'] = 'ok'
response_dict['max_hosts'] = accinfo['max_vessels'] - len(acquired_resources)
response_dict['default_port'] = accinfo['user_port']
except seattleclearinghouse_xmlrpc.AuthenticationError, e:
response_dict['status'] = 'error'
response_dict['error'] = str(e)
response_dict['max_hosts'] = "?"
except xmlrpclib.ProtocolError, e:
response_dict['status'] = 'error'
response_dict['error'] = "SeleXor could not connect to the clearinghouse's XMLRPC server at this moment. Please try again later."
response_dict['max_hosts'] = "?"
except Exception, e:
logger.error("Unknown error while connecting to the XMLRPC server.\n"+traceback.format_exc())
response_dict['status'] = 'error'
response_dict['error'] = "An internal server error occurred."
response_dict['max_hosts'] = "?"
return response_dict
def _handle_host_request(self, data, remoteip):
''' Wrapper for selexor server's host request function. '''
return selexor_server.handle_request(data['userdata'], data['groups'], remoteip)
def _handle_status_query(self, data, remoteip):
''' Wrapper for selexor server's status query function. '''
return selexor_server.get_request_status(data['userdata'], remoteip)
def _release_vessel(self, data, remoteip):
''' Wrapper for selexor server's vessel release function.'''
return selexor_server.release_vessels(data['userdata'], data['vessels'], remoteip)
def _get_mime_type_from_path(self, path):
'''
Returns the MIME type for a file with the specified path.
Returns text/plain if the MIME type cannot be determined.
'''
if path.endswith(".png"):
return 'image/png'
if path.endswith('.gif'):
return 'image/gif'
if path.endswith('.ico'):
return 'image/vnd.microsoft.icon'
if path.endswith('.html'):
return 'application/xhtml+xml; charset=utf-8'
if path.endswith('.css'):
return 'text/css; charset=utf-8'
if path.endswith('.js'):
return 'text/javascript; charset=utf-8'
return 'text/plain; charset=utf-8'
if __name__ == "__main__":
main()