VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/webservergluebase.py@ 92892

Last change on this file since 92892 was 86974, checked in by vboxsync, 4 years ago

testmanager: Attempted python 3 upload fix. bugref:9788

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 23.3 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: webservergluebase.py 86974 2020-11-25 14:38:52Z vboxsync $
3
4"""
5Test Manager Core - Web Server Abstraction Base Class.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2020 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 86974 $"
30
31
32# Standard python imports.
33import cgitb
34import codecs;
35import os
36import sys
37
38# Validation Kit imports.
39from common import webutils, utils;
40from testmanager import config;
41
42
43class WebServerGlueException(Exception):
44 """
45 For exceptions raised by glue code.
46 """
47 pass; # pylint: disable=unnecessary-pass
48
49
50class WebServerGlueBase(object):
51 """
52 Web server interface abstraction and some HTML utils.
53 """
54
55 ## Enables more debug output.
56 kfDebugInfoEnabled = True;
57
58 ## The maximum number of characters to cache.
59 kcchMaxCached = 65536;
60
61 ## Special getUserName return value.
62 ksUnknownUser = 'Unknown User';
63
64 ## HTTP status codes and their messages.
65 kdStatusMsgs = {
66 100: 'Continue',
67 101: 'Switching Protocols',
68 102: 'Processing',
69 103: 'Early Hints',
70 200: 'OK',
71 201: 'Created',
72 202: 'Accepted',
73 203: 'Non-Authoritative Information',
74 204: 'No Content',
75 205: 'Reset Content',
76 206: 'Partial Content',
77 207: 'Multi-Status',
78 208: 'Already Reported',
79 226: 'IM Used',
80 300: 'Multiple Choices',
81 301: 'Moved Permantently',
82 302: 'Found',
83 303: 'See Other',
84 304: 'Not Modified',
85 305: 'Use Proxy',
86 306: 'Switch Proxy',
87 307: 'Temporary Redirect',
88 308: 'Permanent Redirect',
89 400: 'Bad Request',
90 401: 'Unauthorized',
91 402: 'Payment Required',
92 403: 'Forbidden',
93 404: 'Not Found',
94 405: 'Method Not Allowed',
95 406: 'Not Acceptable',
96 407: 'Proxy Authentication Required',
97 408: 'Request Timeout',
98 409: 'Conflict',
99 410: 'Gone',
100 411: 'Length Required',
101 412: 'Precondition Failed',
102 413: 'Payload Too Large',
103 414: 'URI Too Long',
104 415: 'Unsupported Media Type',
105 416: 'Range Not Satisfiable',
106 417: 'Expectation Failed',
107 418: 'I\'m a teapot',
108 421: 'Misdirection Request',
109 422: 'Unprocessable Entity',
110 423: 'Locked',
111 424: 'Failed Dependency',
112 425: 'Too Early',
113 426: 'Upgrade Required',
114 428: 'Precondition Required',
115 429: 'Too Many Requests',
116 431: 'Request Header Fields Too Large',
117 451: 'Unavailable For Legal Reasons',
118 500: 'Internal Server Error',
119 501: 'Not Implemented',
120 502: 'Bad Gateway',
121 503: 'Service Unavailable',
122 504: 'Gateway Timeout',
123 505: 'HTTP Version Not Supported',
124 506: 'Variant Also Negotiates',
125 507: 'Insufficient Storage',
126 508: 'Loop Detected',
127 510: 'Not Extended',
128 511: 'Network Authentication Required',
129 };
130
131
132 def __init__(self, sValidationKitDir, fHtmlDebugOutput = True):
133 self._sValidationKitDir = sValidationKitDir;
134
135 # Debug
136 self.tsStart = utils.timestampNano();
137 self._fHtmlDebugOutput = fHtmlDebugOutput; # For trace
138 self._oDbgFile = sys.stderr;
139 if config.g_ksSrvGlueDebugLogDst is not None and config.g_kfSrvGlueDebug is True:
140 self._oDbgFile = open(config.g_ksSrvGlueDebugLogDst, 'a');
141 if config.g_kfSrvGlueCgiDumpArgs:
142 self._oDbgFile.write('Arguments: %s\nEnvironment:\n' % (sys.argv,));
143 if config.g_kfSrvGlueCgiDumpEnv:
144 for sVar in sorted(os.environ):
145 self._oDbgFile.write(' %s=\'%s\' \\\n' % (sVar, os.environ[sVar],));
146
147 self._afnDebugInfo = [];
148
149 # HTTP header.
150 self._fHeaderWrittenOut = False;
151 self._dHeaderFields = \
152 { \
153 'Content-Type': 'text/html; charset=utf-8',
154 };
155
156 # Body.
157 self._sBodyType = None;
158 self._dParams = dict();
159 self._sHtmlBody = '';
160 self._cchCached = 0;
161 self._cchBodyWrittenOut = 0;
162
163 # Output.
164 if sys.version_info[0] >= 3:
165 self.oOutputRaw = sys.stdout.detach(); # pylint: disable=no-member
166 sys.stdout = None; # Prevents flush_std_files() from complaining on stderr during sys.exit().
167 else:
168 self.oOutputRaw = sys.stdout;
169 self.oOutputText = codecs.getwriter('utf-8')(self.oOutputRaw);
170
171
172 #
173 # Get stuff.
174 #
175
176 def getParameters(self):
177 """
178 Returns a dictionary with the query parameters.
179
180 The parameter name is the key, the values are given as lists. If a
181 parameter is given more than once, the value is appended to the
182 existing dictionary entry.
183 """
184 return dict();
185
186 def getClientAddr(self):
187 """
188 Returns the client address, as a string.
189 """
190 raise WebServerGlueException('getClientAddr is not implemented');
191
192 def getMethod(self):
193 """
194 Gets the HTTP request method.
195 """
196 return 'POST';
197
198 def getLoginName(self):
199 """
200 Gets login name provided by Apache.
201 Returns kUnknownUser if not logged on.
202 """
203 return WebServerGlueBase.ksUnknownUser;
204
205 def getUrlScheme(self):
206 """
207 Gets scheme name (aka. access protocol) from request URL, i.e. 'http' or 'https'.
208 See also urlparse.scheme.
209 """
210 return 'http';
211
212 def getUrlNetLoc(self):
213 """
214 Gets the network location (server host name / ip) from the request URL.
215 See also urlparse.netloc.
216 """
217 raise WebServerGlueException('getUrlNetLoc is not implemented');
218
219 def getUrlPath(self):
220 """
221 Gets the hirarchical path (relative to server) from the request URL.
222 See also urlparse.path.
223 Note! This includes the leading slash.
224 """
225 raise WebServerGlueException('getUrlPath is not implemented');
226
227 def getUrlBasePath(self):
228 """
229 Gets the hirarchical base path (relative to server) from the request URL.
230 Note! This includes both a leading an trailing slash.
231 """
232 sPath = self.getUrlPath(); # virtual method # pylint: disable=assignment-from-no-return
233 iLastSlash = sPath.rfind('/');
234 if iLastSlash >= 0:
235 sPath = sPath[:iLastSlash];
236 sPath = sPath.rstrip('/');
237 return sPath + '/';
238
239 def getUrl(self):
240 """
241 Gets the URL being accessed, sans parameters.
242 For instance this will return, "http://localhost/testmanager/admin.cgi"
243 when "http://localhost/testmanager/admin.cgi?blah=blah" is being access.
244 """
245 return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlPath());
246
247 def getBaseUrl(self):
248 """
249 Gets the base URL (with trailing slash).
250 For instance this will return, "http://localhost/testmanager/" when
251 "http://localhost/testmanager/admin.cgi?blah=blah" is being access.
252 """
253 return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlBasePath());
254
255 def getUserAgent(self):
256 """
257 Gets the User-Agent field of the HTTP header, returning empty string
258 if not present.
259 """
260 return '';
261
262 def getContentType(self):
263 """
264 Gets the Content-Type field of the HTTP header, parsed into a type
265 string and a dictionary.
266 """
267 return ('text/html', {});
268
269 def getContentLength(self):
270 """
271 Gets the content length.
272 Returns int.
273 """
274 return 0;
275
276 def getBodyIoStream(self):
277 """
278 Returns file object for reading the HTML body.
279 """
280 raise WebServerGlueException('getUrlPath is not implemented');
281
282 def getBodyIoStreamBinary(self):
283 """
284 Returns file object for reading the binary HTML body.
285 """
286 raise WebServerGlueException('getBodyIoStreamBinary is not implemented');
287
288 #
289 # Output stuff.
290 #
291
292 def _writeHeader(self, sHeaderLine):
293 """
294 Worker function which child classes can override.
295 """
296 sys.stderr.write('_writeHeader: cch=%s "%s..."\n' % (len(sHeaderLine), sHeaderLine[0:10],))
297 self.oOutputText.write(sHeaderLine);
298 return True;
299
300 def flushHeader(self):
301 """
302 Flushes the HTTP header.
303 """
304 if self._fHeaderWrittenOut is False:
305 for sKey in self._dHeaderFields:
306 self._writeHeader('%s: %s\n' % (sKey, self._dHeaderFields[sKey]));
307 self._fHeaderWrittenOut = True;
308 self._writeHeader('\n'); # End of header indicator.
309 return None;
310
311 def setHeaderField(self, sField, sValue):
312 """
313 Sets a header field.
314 """
315 assert self._fHeaderWrittenOut is False;
316 self._dHeaderFields[sField] = sValue;
317 return True;
318
319 def setRedirect(self, sLocation, iCode = 302):
320 """
321 Sets up redirection of the page.
322 Raises an exception if called too late.
323 """
324 if self._fHeaderWrittenOut is True:
325 raise WebServerGlueException('setRedirect called after the header was written');
326 if iCode != 302:
327 raise WebServerGlueException('Redirection code %d is not supported' % (iCode,));
328
329 self.setHeaderField('Location', sLocation);
330 self.setHeaderField('Status', '302 Found');
331 return True;
332
333 def setStatus(self, iStatus, sMsg = None):
334 """ Sets the status code. """
335 if not sMsg:
336 sMsg = self.kdStatusMsgs[iStatus];
337 return self.setHeaderField('Status', '%u %s' % (iStatus, sMsg));
338
339 def setContentType(self, sType):
340 """ Sets the content type header field. """
341 return self.setHeaderField('Content-Type', sType);
342
343 def _writeWorker(self, sChunkOfHtml):
344 """
345 Worker function which child classes can override.
346 """
347 sys.stderr.write('_writeWorker: cch=%s "%s..."\n' % (len(sChunkOfHtml), sChunkOfHtml[0:10],))
348 self.oOutputText.write(sChunkOfHtml);
349 return True;
350
351 def write(self, sChunkOfHtml):
352 """
353 Writes chunk of HTML, making sure the HTTP header is flushed first.
354 """
355 if self._sBodyType is None:
356 self._sBodyType = 'html';
357 elif self._sBodyType != 'html':
358 raise WebServerGlueException('Cannot use writeParameter when body type is "%s"' % (self._sBodyType, ));
359
360 self._sHtmlBody += sChunkOfHtml;
361 self._cchCached += len(sChunkOfHtml);
362
363 if self._cchCached > self.kcchMaxCached:
364 self.flush();
365 return True;
366
367 def writeRaw(self, abChunk):
368 """
369 Writes a raw chunk the document. Can be binary or any encoding.
370 No caching.
371 """
372 if self._sBodyType is None:
373 self._sBodyType = 'raw';
374 elif self._sBodyType != 'raw':
375 raise WebServerGlueException('Cannot use writeRaw when body type is "%s"' % (self._sBodyType, ));
376
377 self.flushHeader();
378 if self._cchCached > 0:
379 self.flush();
380
381 sys.stderr.write('writeRaw: cb=%s\n' % (len(abChunk),))
382 self.oOutputRaw.write(abChunk);
383 return True;
384
385 def writeParams(self, dParams):
386 """
387 Writes one or more reply parameters in a form style response. The names
388 and values in dParams are unencoded, this method takes care of that.
389
390 Note! This automatically changes the content type to
391 'application/x-www-form-urlencoded', if the header hasn't been flushed
392 already.
393 """
394 if self._sBodyType is None:
395 if not self._fHeaderWrittenOut:
396 self.setHeaderField('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
397 elif self._dHeaderFields['Content-Type'] != 'application/x-www-form-urlencoded; charset=utf-8':
398 raise WebServerGlueException('Cannot use writeParams when content-type is "%s"' % \
399 (self._dHeaderFields['Content-Type'],));
400 self._sBodyType = 'form';
401
402 elif self._sBodyType != 'form':
403 raise WebServerGlueException('Cannot use writeParams when body type is "%s"' % (self._sBodyType, ));
404
405 for sKey in dParams:
406 sValue = str(dParams[sKey]);
407 self._dParams[sKey] = sValue;
408 self._cchCached += len(sKey) + len(sValue);
409
410 if self._cchCached > self.kcchMaxCached:
411 self.flush();
412
413 return True;
414
415 def flush(self):
416 """
417 Flush the output.
418 """
419 self.flushHeader();
420
421 if self._sBodyType == 'form':
422 sBody = webutils.encodeUrlParams(self._dParams);
423 self._writeWorker(sBody);
424
425 self._dParams = dict();
426 self._cchBodyWrittenOut += self._cchCached;
427
428 elif self._sBodyType == 'html':
429 self._writeWorker(self._sHtmlBody);
430
431 self._sHtmlBody = '';
432 self._cchBodyWrittenOut += self._cchCached;
433
434 self._cchCached = 0;
435 return None;
436
437 #
438 # Paths.
439 #
440
441 def pathTmWebUI(self):
442 """
443 Gets the path to the TM 'webui' directory.
444 """
445 return os.path.join(self._sValidationKitDir, 'testmanager', 'webui');
446
447 #
448 # Error stuff & Debugging.
449 #
450
451 def errorLog(self, sError, aXcptInfo, sLogFile):
452 """
453 Writes the error to a log file.
454 """
455 # Easy solution for log file size: Only one report.
456 try: os.unlink(sLogFile);
457 except: pass;
458
459 # Try write the log file.
460 fRc = True;
461 fSaved = self._fHtmlDebugOutput;
462
463 try:
464 oFile = open(sLogFile, 'w');
465 oFile.write(sError + '\n\n');
466 if aXcptInfo[0] is not None:
467 oFile.write(' B a c k t r a c e\n');
468 oFile.write('===================\n');
469 oFile.write(cgitb.text(aXcptInfo, 5));
470 oFile.write('\n\n');
471
472 oFile.write(' D e b u g I n f o\n');
473 oFile.write('=====================\n\n');
474 self._fHtmlDebugOutput = False;
475 self.debugDumpStuff(oFile.write);
476
477 oFile.close();
478 except:
479 fRc = False;
480
481 self._fHtmlDebugOutput = fSaved;
482 return fRc;
483
484 def errorPage(self, sError, aXcptInfo, sLogFile = None):
485 """
486 Displays a page with an error message.
487 """
488 if sLogFile is not None:
489 self.errorLog(sError, aXcptInfo, sLogFile);
490
491 # Reset buffering, hoping that nothing was flushed yet.
492 self._sBodyType = None;
493 self._sHtmlBody = '';
494 self._cchCached = 0;
495 if not self._fHeaderWrittenOut:
496 if self._fHtmlDebugOutput:
497 self.setHeaderField('Content-Type', 'text/html; charset=utf-8');
498 else:
499 self.setHeaderField('Content-Type', 'text/plain; charset=utf-8');
500
501 # Write the error page.
502 if self._fHtmlDebugOutput:
503 self.write('<html><head><title>Test Manage Error</title></head>\n' +
504 '<body><h1>Test Manager Error:</h1>\n' +
505 '<p>' + sError + '</p>\n');
506 else:
507 self.write(' Test Manage Error\n'
508 '===================\n'
509 '\n'
510 '' + sError + '\n\n');
511
512 if aXcptInfo[0] is not None:
513 if self._fHtmlDebugOutput:
514 self.write('<h1>Backtrace:</h1>\n');
515 self.write(cgitb.html(aXcptInfo, 5));
516 else:
517 self.write('Backtrace\n'
518 '---------\n'
519 '\n');
520 self.write(cgitb.text(aXcptInfo, 5));
521 self.write('\n\n');
522
523 if self.kfDebugInfoEnabled:
524 if self._fHtmlDebugOutput:
525 self.write('<h1>Debug Info:</h1>\n');
526 else:
527 self.write('Debug Info\n'
528 '----------\n'
529 '\n');
530 self.debugDumpStuff();
531
532 for fn in self._afnDebugInfo:
533 try:
534 fn(self, self._fHtmlDebugOutput);
535 except Exception as oXcpt:
536 self.write('\nDebug info callback %s raised exception: %s\n' % (fn, oXcpt));
537
538 if self._fHtmlDebugOutput:
539 self.write('</body></html>');
540
541 self.flush();
542
543 def debugInfoPage(self, fnWrite = None):
544 """
545 Dumps useful debug info.
546 """
547 if fnWrite is None:
548 fnWrite = self.write;
549
550 fnWrite('<html><head><title>Test Manage Debug Info</title></head>\n<body>\n');
551 self.debugDumpStuff(fnWrite = fnWrite);
552 fnWrite('</body></html>');
553 self.flush();
554
555 def debugDumpDict(self, sName, dDict, fSorted = True, fnWrite = None):
556 """
557 Dumps dictionary.
558 """
559 if fnWrite is None:
560 fnWrite = self.write;
561
562 asKeys = list(dDict.keys());
563 if fSorted:
564 asKeys.sort();
565
566 if self._fHtmlDebugOutput:
567 fnWrite('<h2>%s</h2>\n'
568 '<table border="1"><tr><th>name</th><th>value</th></tr>\n' % (sName,));
569 for sKey in asKeys:
570 fnWrite(' <tr><td>' + webutils.escapeElem(sKey) + '</td><td>' \
571 + webutils.escapeElem(str(dDict.get(sKey))) \
572 + '</td></tr>\n');
573 fnWrite('</table>\n');
574 else:
575 for i in range(len(sName) - 1):
576 fnWrite('%s ' % (sName[i],));
577 fnWrite('%s\n\n' % (sName[-1],));
578
579 fnWrite('%28s Value\n' % ('Name',));
580 fnWrite('------------------------------------------------------------------------\n');
581 for sKey in asKeys:
582 fnWrite('%28s: %s\n' % (sKey, dDict.get(sKey),));
583 fnWrite('\n');
584
585 return True;
586
587 def debugDumpList(self, sName, aoStuff, fnWrite = None):
588 """
589 Dumps array.
590 """
591 if fnWrite is None:
592 fnWrite = self.write;
593
594 if self._fHtmlDebugOutput:
595 fnWrite('<h2>%s</h2>\n'
596 '<table border="1"><tr><th>index</th><th>value</th></tr>\n' % (sName,));
597 for i, _ in enumerate(aoStuff):
598 fnWrite(' <tr><td>' + str(i) + '</td><td>' + webutils.escapeElem(str(aoStuff[i])) + '</td></tr>\n');
599 fnWrite('</table>\n');
600 else:
601 for ch in sName[:-1]:
602 fnWrite('%s ' % (ch,));
603 fnWrite('%s\n\n' % (sName[-1],));
604
605 fnWrite('Index Value\n');
606 fnWrite('------------------------------------------------------------------------\n');
607 for i, oStuff in enumerate(aoStuff):
608 fnWrite('%5u %s\n' % (i, str(oStuff)));
609 fnWrite('\n');
610
611 return True;
612
613 def debugDumpParameters(self, fnWrite):
614 """ Dumps request parameters. """
615 if fnWrite is None:
616 fnWrite = self.write;
617
618 try:
619 dParams = self.getParameters();
620 return self.debugDumpDict('Parameters', dParams);
621 except Exception as oXcpt:
622 if self._fHtmlDebugOutput:
623 fnWrite('<p>Exception %s while retriving parameters.</p>\n' % (oXcpt,))
624 else:
625 fnWrite('Exception %s while retriving parameters.\n' % (oXcpt,))
626 return False;
627
628 def debugDumpEnv(self, fnWrite = None):
629 """ Dumps os.environ. """
630 return self.debugDumpDict('Environment (os.environ)', os.environ, fnWrite = fnWrite);
631
632 def debugDumpArgv(self, fnWrite = None):
633 """ Dumps sys.argv. """
634 return self.debugDumpList('Arguments (sys.argv)', sys.argv, fnWrite = fnWrite);
635
636 def debugDumpPython(self, fnWrite = None):
637 """
638 Dump python info.
639 """
640 dInfo = {};
641 dInfo['sys.version'] = sys.version;
642 dInfo['sys.hexversion'] = sys.hexversion;
643 dInfo['sys.api_version'] = sys.api_version;
644 if hasattr(sys, 'subversion'):
645 dInfo['sys.subversion'] = sys.subversion; # pylint: disable=no-member
646 dInfo['sys.platform'] = sys.platform;
647 dInfo['sys.executable'] = sys.executable;
648 dInfo['sys.copyright'] = sys.copyright;
649 dInfo['sys.byteorder'] = sys.byteorder;
650 dInfo['sys.exec_prefix'] = sys.exec_prefix;
651 dInfo['sys.prefix'] = sys.prefix;
652 dInfo['sys.path'] = sys.path;
653 dInfo['sys.builtin_module_names'] = sys.builtin_module_names;
654 dInfo['sys.flags'] = sys.flags;
655
656 return self.debugDumpDict('Python Info', dInfo, fnWrite = fnWrite);
657
658
659 def debugDumpStuff(self, fnWrite = None):
660 """
661 Dumps stuff to the error page and debug info page.
662 Should be extended by child classes when possible.
663 """
664 self.debugDumpParameters(fnWrite);
665 self.debugDumpEnv(fnWrite);
666 self.debugDumpArgv(fnWrite);
667 self.debugDumpPython(fnWrite);
668 return True;
669
670 def dprint(self, sMessage):
671 """
672 Prints to debug log (usually apache error log).
673 """
674 if config.g_kfSrvGlueDebug is True:
675 if config.g_kfSrvGlueDebugTS is False:
676 self._oDbgFile.write(sMessage);
677 if not sMessage.endswith('\n'):
678 self._oDbgFile.write('\n');
679 else:
680 tsNow = utils.timestampMilli();
681 tsReq = tsNow - (self.tsStart / 1000000);
682 iPid = os.getpid();
683 for sLine in sMessage.split('\n'):
684 self._oDbgFile.write('%s/%03u,pid=%04x: %s\n' % (tsNow, tsReq, iPid, sLine,));
685
686 return True;
687
688 def registerDebugInfoCallback(self, fnDebugInfo):
689 """
690 Registers a debug info method for calling when the error page is shown.
691
692 The fnDebugInfo function takes two parameters. The first is this
693 object, the second is a boolean indicating html (True) or text (False)
694 output. The return value is ignored.
695 """
696 if self.kfDebugInfoEnabled:
697 self._afnDebugInfo.append(fnDebugInfo);
698 return True;
699
700 def unregisterDebugInfoCallback(self, fnDebugInfo):
701 """
702 Unregisters a debug info method previously registered by
703 registerDebugInfoCallback.
704 """
705 if self.kfDebugInfoEnabled:
706 try: self._afnDebugInfo.remove(fnDebugInfo);
707 except: pass;
708 return True;
709
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette