diff --git a/cashew.sln b/cashew.sln index fbd01a0..c217339 100644 --- a/cashew.sln +++ b/cashew.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 16.0.29230.47 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cashew", "cashew\cashew.csproj", "{1AC90E29-B37E-401C-9960-66BC66D4EFBF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Misc", "..\CC-Functions\Misc\Misc.csproj", "{B80D5E09-B935-4602-A173-BAF7C1974999}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,10 +15,6 @@ Global {1AC90E29-B37E-401C-9960-66BC66D4EFBF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AC90E29-B37E-401C-9960-66BC66D4EFBF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AC90E29-B37E-401C-9960-66BC66D4EFBF}.Release|Any CPU.Build.0 = Release|Any CPU - {B80D5E09-B935-4602-A173-BAF7C1974999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B80D5E09-B935-4602-A173-BAF7C1974999}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B80D5E09-B935-4602-A173-BAF7C1974999}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B80D5E09-B935-4602-A173-BAF7C1974999}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/cashew/App.config b/cashew/App.config index f034a35..7891161 100644 --- a/cashew/App.config +++ b/cashew/App.config @@ -1,7 +1,7 @@  - - + + @@ -19,8 +19,24 @@ - + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/cashew/Lib/BaseHTTPServer.py b/cashew/Lib/BaseHTTPServer.py new file mode 100644 index 0000000..3df3323 --- /dev/null +++ b/cashew/Lib/BaseHTTPServer.py @@ -0,0 +1,614 @@ +"""HTTP server base class. + +Note: the class in this module doesn't implement any HTTP request; see +SimpleHTTPServer for simple implementations of GET, HEAD and POST +(including CGI scripts). It does, however, optionally implement HTTP/1.1 +persistent connections, as of version 0.3. + +Contents: + +- BaseHTTPRequestHandler: HTTP request handler base class +- test: test function + +XXX To do: + +- log requests even later (to capture byte count) +- log user-agent header and other interesting goodies +- send error log to separate file +""" + + +# See also: +# +# HTTP Working Group T. Berners-Lee +# INTERNET-DRAFT R. T. Fielding +# H. Frystyk Nielsen +# Expires September 8, 1995 March 8, 1995 +# +# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt +# +# and +# +# Network Working Group R. Fielding +# Request for Comments: 2616 et al +# Obsoletes: 2068 June 1999 +# Category: Standards Track +# +# URL: http://www.faqs.org/rfcs/rfc2616.html + +# Log files +# --------- +# +# Here's a quote from the NCSA httpd docs about log file format. +# +# | The logfile format is as follows. Each line consists of: +# | +# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb +# | +# | host: Either the DNS name or the IP number of the remote client +# | rfc931: Any information returned by identd for this person, +# | - otherwise. +# | authuser: If user sent a userid for authentication, the user name, +# | - otherwise. +# | DD: Day +# | Mon: Month (calendar name) +# | YYYY: Year +# | hh: hour (24-hour format, the machine's timezone) +# | mm: minutes +# | ss: seconds +# | request: The first line of the HTTP request as sent by the client. +# | ddd: the status code returned by the server, - if not available. +# | bbbb: the total number of bytes sent, +# | *not including the HTTP/1.0 header*, - if not available +# | +# | You can determine the name of the file accessed through request. +# +# (Actually, the latter is only true if you know the server configuration +# at the time the request was made!) + +__version__ = "0.3" + +__all__ = ["HTTPServer", "BaseHTTPRequestHandler"] + +import sys +import time +import socket # For gethostbyaddr() +from warnings import filterwarnings, catch_warnings +with catch_warnings(): + if sys.py3kwarning: + filterwarnings("ignore", ".*mimetools has been removed", + DeprecationWarning) + import mimetools +import SocketServer + +# Default error message template +DEFAULT_ERROR_MESSAGE = """\ + +Error response + + +

Error response

+

Error code %(code)d. +

Message: %(message)s. +

Error code explanation: %(code)s = %(explain)s. + +""" + +DEFAULT_ERROR_CONTENT_TYPE = "text/html" + +def _quote_html(html): + return html.replace("&", "&").replace("<", "<").replace(">", ">") + +class HTTPServer(SocketServer.TCPServer): + + allow_reuse_address = 1 # Seems to make sense in testing environment + + def server_bind(self): + """Override server_bind to store the server name.""" + SocketServer.TCPServer.server_bind(self) + host, port = self.socket.getsockname()[:2] + self.server_name = socket.getfqdn(host) + self.server_port = port + + +class BaseHTTPRequestHandler(SocketServer.StreamRequestHandler): + + """HTTP request handler base class. + + The following explanation of HTTP serves to guide you through the + code as well as to expose any misunderstandings I may have about + HTTP (so you don't need to read the code to figure out I'm wrong + :-). + + HTTP (HyperText Transfer Protocol) is an extensible protocol on + top of a reliable stream transport (e.g. TCP/IP). The protocol + recognizes three parts to a request: + + 1. One line identifying the request type and path + 2. An optional set of RFC-822-style headers + 3. An optional data part + + The headers and data are separated by a blank line. + + The first line of the request has the form + + + + where is a (case-sensitive) keyword such as GET or POST, + is a string containing path information for the request, + and should be the string "HTTP/1.0" or "HTTP/1.1". + is encoded using the URL encoding scheme (using %xx to signify + the ASCII character with hex code xx). + + The specification specifies that lines are separated by CRLF but + for compatibility with the widest range of clients recommends + servers also handle LF. Similarly, whitespace in the request line + is treated sensibly (allowing multiple spaces between components + and allowing trailing whitespace). + + Similarly, for output, lines ought to be separated by CRLF pairs + but most clients grok LF characters just fine. + + If the first line of the request has the form + + + + (i.e. is left out) then this is assumed to be an HTTP + 0.9 request; this form has no optional headers and data part and + the reply consists of just the data. + + The reply form of the HTTP 1.x protocol again has three parts: + + 1. One line giving the response code + 2. An optional set of RFC-822-style headers + 3. The data + + Again, the headers and data are separated by a blank line. + + The response code line has the form + + + + where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), + is a 3-digit response code indicating success or + failure of the request, and is an optional + human-readable string explaining what the response code means. + + This server parses the request and the headers, and then calls a + function specific to the request type (). Specifically, + a request SPAM will be handled by a method do_SPAM(). If no + such method exists the server sends an error response to the + client. If it exists, it is called with no arguments: + + do_SPAM() + + Note that the request name is case sensitive (i.e. SPAM and spam + are different requests). + + The various request details are stored in instance variables: + + - client_address is the client IP address in the form (host, + port); + + - command, path and version are the broken-down request line; + + - headers is an instance of mimetools.Message (or a derived + class) containing the header information; + + - rfile is a file object open for reading positioned at the + start of the optional input data part; + + - wfile is a file object open for writing. + + IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! + + The first thing to be written must be the response line. Then + follow 0 or more header lines, then a blank line, and then the + actual data (if any). The meaning of the header lines depends on + the command executed by the server; in most cases, when data is + returned, there should be at least one header line of the form + + Content-type: / + + where and should be registered MIME types, + e.g. "text/html" or "text/plain". + + """ + + # The Python system version, truncated to its first component. + sys_version = "Python/" + sys.version.split()[0] + + # The server software version. You may want to override this. + # The format is multiple whitespace-separated strings, + # where each string is of the form name[/version]. + server_version = "BaseHTTP/" + __version__ + + # The default request version. This only affects responses up until + # the point where the request line is parsed, so it mainly decides what + # the client gets back when sending a malformed request line. + # Most web servers default to HTTP 0.9, i.e. don't send a status line. + default_request_version = "HTTP/0.9" + + def parse_request(self): + """Parse a request (internal). + + The request should be stored in self.raw_requestline; the results + are in self.command, self.path, self.request_version and + self.headers. + + Return True for success, False for failure; on failure, an + error is sent back. + + """ + self.command = None # set in case of error on the first line + self.request_version = version = self.default_request_version + self.close_connection = 1 + requestline = self.raw_requestline + requestline = requestline.rstrip('\r\n') + self.requestline = requestline + words = requestline.split() + if len(words) == 3: + command, path, version = words + if version[:5] != 'HTTP/': + self.send_error(400, "Bad request version (%r)" % version) + return False + try: + base_version_number = version.split('/', 1)[1] + version_number = base_version_number.split(".") + # RFC 2145 section 3.1 says there can be only one "." and + # - major and minor numbers MUST be treated as + # separate integers; + # - HTTP/2.4 is a lower version than HTTP/2.13, which in + # turn is lower than HTTP/12.3; + # - Leading zeros MUST be ignored by recipients. + if len(version_number) != 2: + raise ValueError + version_number = int(version_number[0]), int(version_number[1]) + except (ValueError, IndexError): + self.send_error(400, "Bad request version (%r)" % version) + return False + if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": + self.close_connection = 0 + if version_number >= (2, 0): + self.send_error(505, + "Invalid HTTP Version (%s)" % base_version_number) + return False + elif len(words) == 2: + command, path = words + self.close_connection = 1 + if command != 'GET': + self.send_error(400, + "Bad HTTP/0.9 request type (%r)" % command) + return False + elif not words: + return False + else: + self.send_error(400, "Bad request syntax (%r)" % requestline) + return False + self.command, self.path, self.request_version = command, path, version + + # Examine the headers and look for a Connection directive + self.headers = self.MessageClass(self.rfile, 0) + + conntype = self.headers.get('Connection', "") + if conntype.lower() == 'close': + self.close_connection = 1 + elif (conntype.lower() == 'keep-alive' and + self.protocol_version >= "HTTP/1.1"): + self.close_connection = 0 + return True + + def handle_one_request(self): + """Handle a single HTTP request. + + You normally don't need to override this method; see the class + __doc__ string for information on how to handle specific HTTP + commands such as GET and POST. + + """ + try: + self.raw_requestline = self.rfile.readline(65537) + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + self.send_error(414) + return + if not self.raw_requestline: + self.close_connection = 1 + return + if not self.parse_request(): + # An error code has been sent, just exit + return + mname = 'do_' + self.command + if not hasattr(self, mname): + self.send_error(501, "Unsupported method (%r)" % self.command) + return + method = getattr(self, mname) + method() + self.wfile.flush() #actually send the response if not already done. + except socket.timeout, e: + #a read or a write timed out. Discard this connection + self.log_error("Request timed out: %r", e) + self.close_connection = 1 + return + + def handle(self): + """Handle multiple requests if necessary.""" + self.close_connection = 1 + + self.handle_one_request() + while not self.close_connection: + self.handle_one_request() + + def send_error(self, code, message=None): + """Send and log an error reply. + + Arguments are the error code, and a detailed message. + The detailed message defaults to the short entry matching the + response code. + + This sends an error response (so it must be called before any + output has been generated), logs the error, and finally sends + a piece of HTML explaining the error to the user. + + """ + + try: + short, long = self.responses[code] + except KeyError: + short, long = '???', '???' + if message is None: + message = short + explain = long + self.log_error("code %d, message %s", code, message) + self.send_response(code, message) + self.send_header('Connection', 'close') + + # Message body is omitted for cases described in: + # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified) + # - RFC7231: 6.3.6. 205(Reset Content) + content = None + if code >= 200 and code not in (204, 205, 304): + # HTML encode to prevent Cross Site Scripting attacks + # (see bug #1100201) + content = (self.error_message_format % { + 'code': code, + 'message': _quote_html(message), + 'explain': explain + }) + self.send_header("Content-Type", self.error_content_type) + self.end_headers() + + if self.command != 'HEAD' and content: + self.wfile.write(content) + + error_message_format = DEFAULT_ERROR_MESSAGE + error_content_type = DEFAULT_ERROR_CONTENT_TYPE + + def send_response(self, code, message=None): + """Send the response header and log the response code. + + Also send two standard headers with the server software + version and the current date. + + """ + self.log_request(code) + if message is None: + if code in self.responses: + message = self.responses[code][0] + else: + message = '' + if self.request_version != 'HTTP/0.9': + self.wfile.write("%s %d %s\r\n" % + (self.protocol_version, code, message)) + # print (self.protocol_version, code, message) + self.send_header('Server', self.version_string()) + self.send_header('Date', self.date_time_string()) + + def send_header(self, keyword, value): + """Send a MIME header.""" + if self.request_version != 'HTTP/0.9': + self.wfile.write("%s: %s\r\n" % (keyword, value)) + + if keyword.lower() == 'connection': + if value.lower() == 'close': + self.close_connection = 1 + elif value.lower() == 'keep-alive': + self.close_connection = 0 + + def end_headers(self): + """Send the blank line ending the MIME headers.""" + if self.request_version != 'HTTP/0.9': + self.wfile.write("\r\n") + + def log_request(self, code='-', size='-'): + """Log an accepted request. + + This is called by send_response(). + + """ + + self.log_message('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, format, *args): + """Log an error. + + This is called when a request cannot be fulfilled. By + default it passes the message on to log_message(). + + Arguments are the same as for log_message(). + + XXX This should go to the separate error log. + + """ + + self.log_message(format, *args) + + def log_message(self, format, *args): + """Log an arbitrary message. + + This is used by all other logging functions. Override + it if you have specific logging wishes. + + The first argument, FORMAT, is a format string for the + message to be logged. If the format string contains + any % escapes requiring parameters, they should be + specified as subsequent arguments (it's just like + printf!). + + The client ip address and current date/time are prefixed to every + message. + + """ + + sys.stderr.write("%s - - [%s] %s\n" % + (self.client_address[0], + self.log_date_time_string(), + format%args)) + + def version_string(self): + """Return the server software version string.""" + return self.server_version + ' ' + self.sys_version + + def date_time_string(self, timestamp=None): + """Return the current date and time formatted for a message header.""" + if timestamp is None: + timestamp = time.time() + year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) + s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + self.weekdayname[wd], + day, self.monthname[month], year, + hh, mm, ss) + return s + + def log_date_time_string(self): + """Return the current time formatted for logging.""" + now = time.time() + year, month, day, hh, mm, ss, x, y, z = time.localtime(now) + s = "%02d/%3s/%04d %02d:%02d:%02d" % ( + day, self.monthname[month], year, hh, mm, ss) + return s + + weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + monthname = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + def address_string(self): + """Return the client address formatted for logging. + + This version looks up the full hostname using gethostbyaddr(), + and tries to find a name that contains at least one dot. + + """ + + host, port = self.client_address[:2] + return socket.getfqdn(host) + + # Essentially static class variables + + # The version of the HTTP protocol we support. + # Set this to HTTP/1.1 to enable automatic keepalive + protocol_version = "HTTP/1.0" + + # The Message-like class used to parse headers + MessageClass = mimetools.Message + + # Table mapping response codes to messages; entries have the + # form {code: (shortmessage, longmessage)}. + # See RFC 2616. + responses = { + 100: ('Continue', 'Request received, please continue'), + 101: ('Switching Protocols', + 'Switching to new protocol; obey Upgrade header'), + + 200: ('OK', 'Request fulfilled, document follows'), + 201: ('Created', 'Document created, URL follows'), + 202: ('Accepted', + 'Request accepted, processing continues off-line'), + 203: ('Non-Authoritative Information', 'Request fulfilled from cache'), + 204: ('No Content', 'Request fulfilled, nothing follows'), + 205: ('Reset Content', 'Clear input form for further input.'), + 206: ('Partial Content', 'Partial content follows.'), + + 300: ('Multiple Choices', + 'Object has several resources -- see URI list'), + 301: ('Moved Permanently', 'Object moved permanently -- see URI list'), + 302: ('Found', 'Object moved temporarily -- see URI list'), + 303: ('See Other', 'Object moved -- see Method and URL list'), + 304: ('Not Modified', + 'Document has not changed since given time'), + 305: ('Use Proxy', + 'You must use proxy specified in Location to access this ' + 'resource.'), + 307: ('Temporary Redirect', + 'Object moved temporarily -- see URI list'), + + 400: ('Bad Request', + 'Bad request syntax or unsupported method'), + 401: ('Unauthorized', + 'No permission -- see authorization schemes'), + 402: ('Payment Required', + 'No payment -- see charging schemes'), + 403: ('Forbidden', + 'Request forbidden -- authorization will not help'), + 404: ('Not Found', 'Nothing matches the given URI'), + 405: ('Method Not Allowed', + 'Specified method is invalid for this resource.'), + 406: ('Not Acceptable', 'URI not available in preferred format.'), + 407: ('Proxy Authentication Required', 'You must authenticate with ' + 'this proxy before proceeding.'), + 408: ('Request Timeout', 'Request timed out; try again later.'), + 409: ('Conflict', 'Request conflict.'), + 410: ('Gone', + 'URI no longer exists and has been permanently removed.'), + 411: ('Length Required', 'Client must specify Content-Length.'), + 412: ('Precondition Failed', 'Precondition in headers is false.'), + 413: ('Request Entity Too Large', 'Entity is too large.'), + 414: ('Request-URI Too Long', 'URI is too long.'), + 415: ('Unsupported Media Type', 'Entity body in unsupported format.'), + 416: ('Requested Range Not Satisfiable', + 'Cannot satisfy request range.'), + 417: ('Expectation Failed', + 'Expect condition could not be satisfied.'), + + 500: ('Internal Server Error', 'Server got itself in trouble'), + 501: ('Not Implemented', + 'Server does not support this operation'), + 502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), + 503: ('Service Unavailable', + 'The server cannot process the request due to a high load'), + 504: ('Gateway Timeout', + 'The gateway server did not receive a timely response'), + 505: ('HTTP Version Not Supported', 'Cannot fulfill request.'), + } + + +def test(HandlerClass = BaseHTTPRequestHandler, + ServerClass = HTTPServer, protocol="HTTP/1.0"): + """Test the HTTP request handler class. + + This runs an HTTP server on port 8000 (or the first command line + argument). + + """ + + if sys.argv[1:]: + port = int(sys.argv[1]) + else: + port = 8000 + server_address = ('', port) + + HandlerClass.protocol_version = protocol + httpd = ServerClass(server_address, HandlerClass) + + sa = httpd.socket.getsockname() + print "Serving HTTP on", sa[0], "port", sa[1], "..." + httpd.serve_forever() + + +if __name__ == '__main__': + test() diff --git a/cashew/Lib/Bastion.py b/cashew/Lib/Bastion.py new file mode 100644 index 0000000..d0dddbf --- /dev/null +++ b/cashew/Lib/Bastion.py @@ -0,0 +1,180 @@ +"""Bastionification utility. + +A bastion (for another object -- the 'original') is an object that has +the same methods as the original but does not give access to its +instance variables. Bastions have a number of uses, but the most +obvious one is to provide code executing in restricted mode with a +safe interface to an object implemented in unrestricted mode. + +The bastionification routine has an optional second argument which is +a filter function. Only those methods for which the filter method +(called with the method name as argument) returns true are accessible. +The default filter method returns true unless the method name begins +with an underscore. + +There are a number of possible implementations of bastions. We use a +'lazy' approach where the bastion's __getattr__() discipline does all +the work for a particular method the first time it is used. This is +usually fastest, especially if the user doesn't call all available +methods. The retrieved methods are stored as instance variables of +the bastion, so the overhead is only occurred on the first use of each +method. + +Detail: the bastion class has a __repr__() discipline which includes +the repr() of the original object. This is precomputed when the +bastion is created. + +""" +from warnings import warnpy3k +warnpy3k("the Bastion module has been removed in Python 3.0", stacklevel=2) +del warnpy3k + +__all__ = ["BastionClass", "Bastion"] + +from types import MethodType + + +class BastionClass: + + """Helper class used by the Bastion() function. + + You could subclass this and pass the subclass as the bastionclass + argument to the Bastion() function, as long as the constructor has + the same signature (a get() function and a name for the object). + + """ + + def __init__(self, get, name): + """Constructor. + + Arguments: + + get - a function that gets the attribute value (by name) + name - a human-readable name for the original object + (suggestion: use repr(object)) + + """ + self._get_ = get + self._name_ = name + + def __repr__(self): + """Return a representation string. + + This includes the name passed in to the constructor, so that + if you print the bastion during debugging, at least you have + some idea of what it is. + + """ + return "" % self._name_ + + def __getattr__(self, name): + """Get an as-yet undefined attribute value. + + This calls the get() function that was passed to the + constructor. The result is stored as an instance variable so + that the next time the same attribute is requested, + __getattr__() won't be invoked. + + If the get() function raises an exception, this is simply + passed on -- exceptions are not cached. + + """ + attribute = self._get_(name) + self.__dict__[name] = attribute + return attribute + + +def Bastion(object, filter = lambda name: name[:1] != '_', + name=None, bastionclass=BastionClass): + """Create a bastion for an object, using an optional filter. + + See the Bastion module's documentation for background. + + Arguments: + + object - the original object + filter - a predicate that decides whether a function name is OK; + by default all names are OK that don't start with '_' + name - the name of the object; default repr(object) + bastionclass - class used to create the bastion; default BastionClass + + """ + + raise RuntimeError, "This code is not secure in Python 2.2 and later" + + # Note: we define *two* ad-hoc functions here, get1 and get2. + # Both are intended to be called in the same way: get(name). + # It is clear that the real work (getting the attribute + # from the object and calling the filter) is done in get1. + # Why can't we pass get1 to the bastion? Because the user + # would be able to override the filter argument! With get2, + # overriding the default argument is no security loophole: + # all it does is call it. + # Also notice that we can't place the object and filter as + # instance variables on the bastion object itself, since + # the user has full access to all instance variables! + + def get1(name, object=object, filter=filter): + """Internal function for Bastion(). See source comments.""" + if filter(name): + attribute = getattr(object, name) + if type(attribute) == MethodType: + return attribute + raise AttributeError, name + + def get2(name, get1=get1): + """Internal function for Bastion(). See source comments.""" + return get1(name) + + if name is None: + name = repr(object) + return bastionclass(get2, name) + + +def _test(): + """Test the Bastion() function.""" + class Original: + def __init__(self): + self.sum = 0 + def add(self, n): + self._add(n) + def _add(self, n): + self.sum = self.sum + n + def total(self): + return self.sum + o = Original() + b = Bastion(o) + testcode = """if 1: + b.add(81) + b.add(18) + print "b.total() =", b.total() + try: + print "b.sum =", b.sum, + except: + print "inaccessible" + else: + print "accessible" + try: + print "b._add =", b._add, + except: + print "inaccessible" + else: + print "accessible" + try: + print "b._get_.func_defaults =", map(type, b._get_.func_defaults), + except: + print "inaccessible" + else: + print "accessible" + \n""" + exec testcode + print '='*20, "Using rexec:", '='*20 + import rexec + r = rexec.RExec() + m = r.add_module('__main__') + m.b = b + r.r_exec(testcode) + + +if __name__ == '__main__': + _test() diff --git a/cashew/Lib/CGIHTTPServer.py b/cashew/Lib/CGIHTTPServer.py new file mode 100644 index 0000000..5620083 --- /dev/null +++ b/cashew/Lib/CGIHTTPServer.py @@ -0,0 +1,378 @@ +"""CGI-savvy HTTP Server. + +This module builds on SimpleHTTPServer by implementing GET and POST +requests to cgi-bin scripts. + +If the os.fork() function is not present (e.g. on Windows), +os.popen2() is used as a fallback, with slightly altered semantics; if +that function is not present either (e.g. on Macintosh), only Python +scripts are supported, and they are executed by the current process. + +In all cases, the implementation is intentionally naive -- all +requests are executed sychronously. + +SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL +-- it may execute arbitrary Python code or external programs. + +Note that status code 200 is sent prior to execution of a CGI script, so +scripts cannot send other status codes such as 302 (redirect). +""" + + +__version__ = "0.4" + +__all__ = ["CGIHTTPRequestHandler"] + +import os +import sys +import urllib +import BaseHTTPServer +import SimpleHTTPServer +import select +import copy + + +class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + + """Complete HTTP server with GET, HEAD and POST commands. + + GET and HEAD also support running CGI scripts. + + The POST command is *only* implemented for CGI scripts. + + """ + + # Determine platform specifics + have_fork = hasattr(os, 'fork') + have_popen2 = hasattr(os, 'popen2') + have_popen3 = hasattr(os, 'popen3') + + # Make rfile unbuffered -- we need to read one line and then pass + # the rest to a subprocess, so we can't use buffered input. + rbufsize = 0 + + def do_POST(self): + """Serve a POST request. + + This is only implemented for CGI scripts. + + """ + + if self.is_cgi(): + self.run_cgi() + else: + self.send_error(501, "Can only POST to CGI scripts") + + def send_head(self): + """Version of send_head that support CGI scripts""" + if self.is_cgi(): + return self.run_cgi() + else: + return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) + + def is_cgi(self): + """Test whether self.path corresponds to a CGI script. + + Returns True and updates the cgi_info attribute to the tuple + (dir, rest) if self.path requires running a CGI script. + Returns False otherwise. + + If any exception is raised, the caller should assume that + self.path was rejected as invalid and act accordingly. + + The default implementation tests whether the normalized url + path begins with one of the strings in self.cgi_directories + (and the next character is a '/' or the end of the string). + """ + collapsed_path = _url_collapse_path(self.path) + dir_sep = collapsed_path.find('/', 1) + head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] + if head in self.cgi_directories: + self.cgi_info = head, tail + return True + return False + + cgi_directories = ['/cgi-bin', '/htbin'] + + def is_executable(self, path): + """Test whether argument path is an executable file.""" + return executable(path) + + def is_python(self, path): + """Test whether argument path is a Python script.""" + head, tail = os.path.splitext(path) + return tail.lower() in (".py", ".pyw") + + def run_cgi(self): + """Execute a CGI script.""" + dir, rest = self.cgi_info + path = dir + '/' + rest + i = path.find('/', len(dir)+1) + while i >= 0: + nextdir = path[:i] + nextrest = path[i+1:] + + scriptdir = self.translate_path(nextdir) + if os.path.isdir(scriptdir): + dir, rest = nextdir, nextrest + i = path.find('/', len(dir)+1) + else: + break + + # find an explicit query string, if present. + rest, _, query = rest.partition('?') + + # dissect the part after the directory name into a script name & + # a possible additional path, to be stored in PATH_INFO. + i = rest.find('/') + if i >= 0: + script, rest = rest[:i], rest[i:] + else: + script, rest = rest, '' + + scriptname = dir + '/' + script + scriptfile = self.translate_path(scriptname) + if not os.path.exists(scriptfile): + self.send_error(404, "No such CGI script (%r)" % scriptname) + return + if not os.path.isfile(scriptfile): + self.send_error(403, "CGI script is not a plain file (%r)" % + scriptname) + return + ispy = self.is_python(scriptname) + if not ispy: + if not (self.have_fork or self.have_popen2 or self.have_popen3): + self.send_error(403, "CGI script is not a Python script (%r)" % + scriptname) + return + if not self.is_executable(scriptfile): + self.send_error(403, "CGI script is not executable (%r)" % + scriptname) + return + + # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html + # XXX Much of the following could be prepared ahead of time! + env = copy.deepcopy(os.environ) + env['SERVER_SOFTWARE'] = self.version_string() + env['SERVER_NAME'] = self.server.server_name + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['SERVER_PROTOCOL'] = self.protocol_version + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_METHOD'] = self.command + uqrest = urllib.unquote(rest) + env['PATH_INFO'] = uqrest + env['PATH_TRANSLATED'] = self.translate_path(uqrest) + env['SCRIPT_NAME'] = scriptname + if query: + env['QUERY_STRING'] = query + host = self.address_string() + if host != self.client_address[0]: + env['REMOTE_HOST'] = host + env['REMOTE_ADDR'] = self.client_address[0] + authorization = self.headers.getheader("authorization") + if authorization: + authorization = authorization.split() + if len(authorization) == 2: + import base64, binascii + env['AUTH_TYPE'] = authorization[0] + if authorization[0].lower() == "basic": + try: + authorization = base64.decodestring(authorization[1]) + except binascii.Error: + pass + else: + authorization = authorization.split(':') + if len(authorization) == 2: + env['REMOTE_USER'] = authorization[0] + # XXX REMOTE_IDENT + if self.headers.typeheader is None: + env['CONTENT_TYPE'] = self.headers.type + else: + env['CONTENT_TYPE'] = self.headers.typeheader + length = self.headers.getheader('content-length') + if length: + env['CONTENT_LENGTH'] = length + referer = self.headers.getheader('referer') + if referer: + env['HTTP_REFERER'] = referer + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in "\t\n\r ": + accept.append(line.strip()) + else: + accept = accept + line[7:].split(',') + env['HTTP_ACCEPT'] = ','.join(accept) + ua = self.headers.getheader('user-agent') + if ua: + env['HTTP_USER_AGENT'] = ua + co = filter(None, self.headers.getheaders('cookie')) + if co: + env['HTTP_COOKIE'] = ', '.join(co) + # XXX Other HTTP_* headers + # Since we're setting the env in the parent, provide empty + # values to override previously set values + for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', + 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): + env.setdefault(k, "") + + self.send_response(200, "Script output follows") + + decoded_query = query.replace('+', ' ') + + if self.have_fork: + # Unix -- fork as we should + args = [script] + if '=' not in decoded_query: + args.append(decoded_query) + nobody = nobody_uid() + self.wfile.flush() # Always flush before forking + pid = os.fork() + if pid != 0: + # Parent + pid, sts = os.waitpid(pid, 0) + # throw away additional data [see bug #427345] + while select.select([self.rfile], [], [], 0)[0]: + if not self.rfile.read(1): + break + if sts: + self.log_error("CGI script exit status %#x", sts) + return + # Child + try: + try: + os.setuid(nobody) + except os.error: + pass + os.dup2(self.rfile.fileno(), 0) + os.dup2(self.wfile.fileno(), 1) + os.execve(scriptfile, args, env) + except: + self.server.handle_error(self.request, self.client_address) + os._exit(127) + + else: + # Non Unix - use subprocess + import subprocess + cmdline = [scriptfile] + if self.is_python(scriptfile): + interp = sys.executable + if interp.lower().endswith("w.exe"): + # On Windows, use python.exe, not pythonw.exe + interp = interp[:-5] + interp[-4:] + cmdline = [interp, '-u'] + cmdline + if '=' not in query: + cmdline.append(query) + + self.log_message("command: %s", subprocess.list2cmdline(cmdline)) + try: + nbytes = int(length) + except (TypeError, ValueError): + nbytes = 0 + p = subprocess.Popen(cmdline, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + env = env + ) + if self.command.lower() == "post" and nbytes > 0: + data = self.rfile.read(nbytes) + else: + data = None + # throw away additional data [see bug #427345] + while select.select([self.rfile._sock], [], [], 0)[0]: + if not self.rfile._sock.recv(1): + break + stdout, stderr = p.communicate(data) + self.wfile.write(stdout) + if stderr: + self.log_error('%s', stderr) + p.stderr.close() + p.stdout.close() + status = p.returncode + if status: + self.log_error("CGI script exit status %#x", status) + else: + self.log_message("CGI script exited OK") + + +def _url_collapse_path(path): + """ + Given a URL path, remove extra '/'s and '.' path elements and collapse + any '..' references and returns a colllapsed path. + + Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. + The utility of this function is limited to is_cgi method and helps + preventing some security attacks. + + Returns: The reconstituted URL, which will always start with a '/'. + + Raises: IndexError if too many '..' occur within the path. + + """ + # Query component should not be involved. + path, _, query = path.partition('?') + path = urllib.unquote(path) + + # Similar to os.path.split(os.path.normpath(path)) but specific to URL + # path semantics rather than local operating system semantics. + path_parts = path.split('/') + head_parts = [] + for part in path_parts[:-1]: + if part == '..': + head_parts.pop() # IndexError if more '..' than prior parts + elif part and part != '.': + head_parts.append( part ) + if path_parts: + tail_part = path_parts.pop() + if tail_part: + if tail_part == '..': + head_parts.pop() + tail_part = '' + elif tail_part == '.': + tail_part = '' + else: + tail_part = '' + + if query: + tail_part = '?'.join((tail_part, query)) + + splitpath = ('/' + '/'.join(head_parts), tail_part) + collapsed_path = "/".join(splitpath) + + return collapsed_path + + +nobody = None + +def nobody_uid(): + """Internal routine to get nobody's uid""" + global nobody + if nobody: + return nobody + try: + import pwd + except ImportError: + return -1 + try: + nobody = pwd.getpwnam('nobody')[2] + except KeyError: + nobody = 1 + max(map(lambda x: x[2], pwd.getpwall())) + return nobody + + +def executable(path): + """Test for executable file.""" + try: + st = os.stat(path) + except os.error: + return False + return st.st_mode & 0111 != 0 + + +def test(HandlerClass = CGIHTTPRequestHandler, + ServerClass = BaseHTTPServer.HTTPServer): + SimpleHTTPServer.test(HandlerClass, ServerClass) + + +if __name__ == '__main__': + test() diff --git a/cashew/Lib/ConfigParser.py b/cashew/Lib/ConfigParser.py new file mode 100644 index 0000000..7e6cdbc --- /dev/null +++ b/cashew/Lib/ConfigParser.py @@ -0,0 +1,753 @@ +"""Configuration file parser. + +A setup file consists of sections, lead by a "[section]" header, +and followed by "name: value" entries, with continuations and such in +the style of RFC 822. + +The option values can contain format strings which refer to other values in +the same section, or values in a special [DEFAULT] section. + +For example: + + something: %(dir)s/whatever + +would resolve the "%(dir)s" to the value of dir. All reference +expansions are done late, on demand. + +Intrinsic defaults can be specified by passing them into the +ConfigParser constructor as a dictionary. + +class: + +ConfigParser -- responsible for parsing a list of + configuration files, and managing the parsed database. + + methods: + + __init__(defaults=None) + create the parser and specify a dictionary of intrinsic defaults. The + keys must be strings, the values must be appropriate for %()s string + interpolation. Note that `__name__' is always an intrinsic default; + its value is the section's name. + + sections() + return all the configuration section names, sans DEFAULT + + has_section(section) + return whether the given section exists + + has_option(section, option) + return whether the given option exists in the given section + + options(section) + return list of configuration options for the named section + + read(filenames) + read and parse the list of named configuration files, given by + name. A single filename is also allowed. Non-existing files + are ignored. Return list of successfully read files. + + readfp(fp, filename=None) + read and parse one configuration file, given as a file object. + The filename defaults to fp.name; it is only used in error + messages (if fp has no `name' attribute, the string `' is used). + + get(section, option, raw=False, vars=None) + return a string value for the named option. All % interpolations are + expanded in the return values, based on the defaults passed into the + constructor and the DEFAULT section. Additional substitutions may be + provided using the `vars' argument, which must be a dictionary whose + contents override any pre-existing defaults. + + getint(section, options) + like get(), but convert value to an integer + + getfloat(section, options) + like get(), but convert value to a float + + getboolean(section, options) + like get(), but convert value to a boolean (currently case + insensitively defined as 0, false, no, off for False, and 1, true, + yes, on for True). Returns False or True. + + items(section, raw=False, vars=None) + return a list of tuples with (name, value) for each option + in the section. + + remove_section(section) + remove the given file section and all its options + + remove_option(section, option) + remove the given option from the given section + + set(section, option, value) + set the given option + + write(fp) + write the configuration state in .ini format +""" + +try: + from collections import OrderedDict as _default_dict +except ImportError: + # fallback for setup.py which hasn't yet built _collections + _default_dict = dict + +import re + +__all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError", + "InterpolationError", "InterpolationDepthError", + "InterpolationSyntaxError", "ParsingError", + "MissingSectionHeaderError", + "ConfigParser", "SafeConfigParser", "RawConfigParser", + "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH"] + +DEFAULTSECT = "DEFAULT" + +MAX_INTERPOLATION_DEPTH = 10 + + + +# exception classes +class Error(Exception): + """Base class for ConfigParser exceptions.""" + + def _get_message(self): + """Getter for 'message'; needed only to override deprecation in + BaseException.""" + return self.__message + + def _set_message(self, value): + """Setter for 'message'; needed only to override deprecation in + BaseException.""" + self.__message = value + + # BaseException.message has been deprecated since Python 2.6. To prevent + # DeprecationWarning from popping up over this pre-existing attribute, use + # a new property that takes lookup precedence. + message = property(_get_message, _set_message) + + def __init__(self, msg=''): + self.message = msg + Exception.__init__(self, msg) + + def __repr__(self): + return self.message + + __str__ = __repr__ + +class NoSectionError(Error): + """Raised when no section matches a requested option.""" + + def __init__(self, section): + Error.__init__(self, 'No section: %r' % (section,)) + self.section = section + self.args = (section, ) + +class DuplicateSectionError(Error): + """Raised when a section is multiply-created.""" + + def __init__(self, section): + Error.__init__(self, "Section %r already exists" % section) + self.section = section + self.args = (section, ) + +class NoOptionError(Error): + """A requested option was not found.""" + + def __init__(self, option, section): + Error.__init__(self, "No option %r in section: %r" % + (option, section)) + self.option = option + self.section = section + self.args = (option, section) + +class InterpolationError(Error): + """Base class for interpolation-related exceptions.""" + + def __init__(self, option, section, msg): + Error.__init__(self, msg) + self.option = option + self.section = section + self.args = (option, section, msg) + +class InterpolationMissingOptionError(InterpolationError): + """A string substitution required a setting which was not available.""" + + def __init__(self, option, section, rawval, reference): + msg = ("Bad value substitution:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\tkey : %s\n" + "\trawval : %s\n" + % (section, option, reference, rawval)) + InterpolationError.__init__(self, option, section, msg) + self.reference = reference + self.args = (option, section, rawval, reference) + +class InterpolationSyntaxError(InterpolationError): + """Raised when the source text into which substitutions are made + does not conform to the required syntax.""" + +class InterpolationDepthError(InterpolationError): + """Raised when substitutions are nested too deeply.""" + + def __init__(self, option, section, rawval): + msg = ("Value interpolation too deeply recursive:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\trawval : %s\n" + % (section, option, rawval)) + InterpolationError.__init__(self, option, section, msg) + self.args = (option, section, rawval) + +class ParsingError(Error): + """Raised when a configuration file does not follow legal syntax.""" + + def __init__(self, filename): + Error.__init__(self, 'File contains parsing errors: %s' % filename) + self.filename = filename + self.errors = [] + self.args = (filename, ) + + def append(self, lineno, line): + self.errors.append((lineno, line)) + self.message += '\n\t[line %2d]: %s' % (lineno, line) + +class MissingSectionHeaderError(ParsingError): + """Raised when a key-value pair is found before any section header.""" + + def __init__(self, filename, lineno, line): + Error.__init__( + self, + 'File contains no section headers.\nfile: %s, line: %d\n%r' % + (filename, lineno, line)) + self.filename = filename + self.lineno = lineno + self.line = line + self.args = (filename, lineno, line) + + +class RawConfigParser: + def __init__(self, defaults=None, dict_type=_default_dict, + allow_no_value=False): + self._dict = dict_type + self._sections = self._dict() + self._defaults = self._dict() + if allow_no_value: + self._optcre = self.OPTCRE_NV + else: + self._optcre = self.OPTCRE + if defaults: + for key, value in defaults.items(): + self._defaults[self.optionxform(key)] = value + + def defaults(self): + return self._defaults + + def sections(self): + """Return a list of section names, excluding [DEFAULT]""" + # self._sections will never have [DEFAULT] in it + return self._sections.keys() + + def add_section(self, section): + """Create a new section in the configuration. + + Raise DuplicateSectionError if a section by the specified name + already exists. Raise ValueError if name is DEFAULT or any of it's + case-insensitive variants. + """ + if section.lower() == "default": + raise ValueError, 'Invalid section name: %s' % section + + if section in self._sections: + raise DuplicateSectionError(section) + self._sections[section] = self._dict() + + def has_section(self, section): + """Indicate whether the named section is present in the configuration. + + The DEFAULT section is not acknowledged. + """ + return section in self._sections + + def options(self, section): + """Return a list of option names for the given section name.""" + try: + opts = self._sections[section].copy() + except KeyError: + raise NoSectionError(section) + opts.update(self._defaults) + if '__name__' in opts: + del opts['__name__'] + return opts.keys() + + def read(self, filenames): + """Read and parse a filename or a list of filenames. + + Files that cannot be opened are silently ignored; this is + designed so that you can specify a list of potential + configuration file locations (e.g. current directory, user's + home directory, systemwide directory), and all existing + configuration files in the list will be read. A single + filename may also be given. + + Return list of successfully read files. + """ + if isinstance(filenames, basestring): + filenames = [filenames] + read_ok = [] + for filename in filenames: + try: + fp = open(filename) + except IOError: + continue + self._read(fp, filename) + fp.close() + read_ok.append(filename) + return read_ok + + def readfp(self, fp, filename=None): + """Like read() but the argument must be a file-like object. + + The `fp' argument must have a `readline' method. Optional + second argument is the `filename', which if not given, is + taken from fp.name. If fp has no `name' attribute, `' is + used. + + """ + if filename is None: + try: + filename = fp.name + except AttributeError: + filename = '' + self._read(fp, filename) + + def get(self, section, option): + opt = self.optionxform(option) + if section not in self._sections: + if section != DEFAULTSECT: + raise NoSectionError(section) + if opt in self._defaults: + return self._defaults[opt] + else: + raise NoOptionError(option, section) + elif opt in self._sections[section]: + return self._sections[section][opt] + elif opt in self._defaults: + return self._defaults[opt] + else: + raise NoOptionError(option, section) + + def items(self, section): + try: + d2 = self._sections[section] + except KeyError: + if section != DEFAULTSECT: + raise NoSectionError(section) + d2 = self._dict() + d = self._defaults.copy() + d.update(d2) + if "__name__" in d: + del d["__name__"] + return d.items() + + def _get(self, section, conv, option): + return conv(self.get(section, option)) + + def getint(self, section, option): + return self._get(section, int, option) + + def getfloat(self, section, option): + return self._get(section, float, option) + + _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False} + + def getboolean(self, section, option): + v = self.get(section, option) + if v.lower() not in self._boolean_states: + raise ValueError, 'Not a boolean: %s' % v + return self._boolean_states[v.lower()] + + def optionxform(self, optionstr): + return optionstr.lower() + + def has_option(self, section, option): + """Check for the existence of a given option in a given section.""" + if not section or section == DEFAULTSECT: + option = self.optionxform(option) + return option in self._defaults + elif section not in self._sections: + return False + else: + option = self.optionxform(option) + return (option in self._sections[section] + or option in self._defaults) + + def set(self, section, option, value=None): + """Set an option.""" + if not section or section == DEFAULTSECT: + sectdict = self._defaults + else: + try: + sectdict = self._sections[section] + except KeyError: + raise NoSectionError(section) + sectdict[self.optionxform(option)] = value + + def write(self, fp): + """Write an .ini-format representation of the configuration state.""" + if self._defaults: + fp.write("[%s]\n" % DEFAULTSECT) + for (key, value) in self._defaults.items(): + fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for (key, value) in self._sections[section].items(): + if key == "__name__": + continue + if (value is not None) or (self._optcre == self.OPTCRE): + key = " = ".join((key, str(value).replace('\n', '\n\t'))) + fp.write("%s\n" % (key)) + fp.write("\n") + + def remove_option(self, section, option): + """Remove an option.""" + if not section or section == DEFAULTSECT: + sectdict = self._defaults + else: + try: + sectdict = self._sections[section] + except KeyError: + raise NoSectionError(section) + option = self.optionxform(option) + existed = option in sectdict + if existed: + del sectdict[option] + return existed + + def remove_section(self, section): + """Remove a file section.""" + existed = section in self._sections + if existed: + del self._sections[section] + return existed + + # + # Regular expressions for parsing section headers and options. + # + SECTCRE = re.compile( + r'\[' # [ + r'(?P

[^]]+)' # very permissive! + r'\]' # ] + ) + OPTCRE = re.compile( + r'(?P