author | Pawel Solyga <Pawel.Solyga@gmail.com> |
Mon, 01 Jun 2009 20:31:38 +0200 | |
changeset 2373 | 05ab9393303d |
parent 2273 | e4cb9c53db3e |
child 2864 | 2e0b0af889be |
permissions | -rwxr-xr-x |
#!/usr/bin/env python # # Copyright 2007 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """AppInfo tools Library for working with AppInfo records in memory, store and load from configuration files. """ import re from google.appengine.api import appinfo_errors from google.appengine.api import validation from google.appengine.api import yaml_listener from google.appengine.api import yaml_builder from google.appengine.api import yaml_object _URL_REGEX = r'(?!\^)/|\.|(\(.).*(?!\$).' _FILES_REGEX = r'(?!\^).*(?!\$).' _DELTA_REGEX = r'([1-9][0-9]*)([DdHhMm]|[sS]?)' _EXPIRATION_REGEX = r'\s*(%s)(\s+%s)*\s*' % (_DELTA_REGEX, _DELTA_REGEX) _EXPIRATION_CONVERSIONS = { 'd': 60 * 60 * 24, 'h': 60 * 60, 'm': 60, 's': 1, } APP_ID_MAX_LEN = 100 MAJOR_VERSION_ID_MAX_LEN = 100 MAX_URL_MAPS = 100 APPLICATION_RE_STRING = r'(?!-)[a-z\d\-]{1,%d}' % APP_ID_MAX_LEN VERSION_RE_STRING = r'(?!-)[a-z\d\-]{1,%d}' % MAJOR_VERSION_ID_MAX_LEN RUNTIME_RE_STRING = r'[a-z]{1,30}' API_VERSION_RE_STRING = r'[\w.]{1,32}' HANDLER_STATIC_FILES = 'static_files' HANDLER_STATIC_DIR = 'static_dir' HANDLER_SCRIPT = 'script' LOGIN_OPTIONAL = 'optional' LOGIN_REQUIRED = 'required' LOGIN_ADMIN = 'admin' SECURE_HTTP = 'never' SECURE_HTTPS = 'always' SECURE_HTTP_OR_HTTPS = 'optional' REQUIRE_MATCHING_FILE = 'require_matching_file' DEFAULT_SKIP_FILES = (r"^(.*/)?(" r"(app\.yaml)|" r"(app\.yml)|" r"(index\.yaml)|" r"(index\.yml)|" r"(#.*#)|" r"(.*~)|" r"(.*\.py[co])|" r"(.*/RCS/.*)|" r"(\..*)|" r")$") LOGIN = 'login' SECURE = 'secure' URL = 'url' STATIC_FILES = 'static_files' UPLOAD = 'upload' STATIC_DIR = 'static_dir' MIME_TYPE = 'mime_type' SCRIPT = 'script' EXPIRATION = 'expiration' APPLICATION = 'application' VERSION = 'version' RUNTIME = 'runtime' API_VERSION = 'api_version' HANDLERS = 'handlers' DEFAULT_EXPIRATION = 'default_expiration' SKIP_FILES = 'skip_files' class URLMap(validation.Validated): """Mapping from URLs to handlers. This class acts like something of a union type. Its purpose is to describe a mapping between a set of URLs and their handlers. What handler type a given instance has is determined by which handler-id attribute is used. Each mapping can have one and only one handler type. Attempting to use more than one handler-id attribute will cause an UnknownHandlerType to be raised during validation. Failure to provide any handler-id attributes will cause MissingHandlerType to be raised during validation. The regular expression used by the url field will be used to match against the entire URL path and query string of the request. This means that partial maps will not be matched. Specifying a url, say /admin, is the same as matching against the regular expression '^/admin$'. Don't begin your matching url with ^ or end them with $. These regular expressions won't be accepted and will raise ValueError. Attributes: login: Whether or not login is required to access URL. Defaults to 'optional'. secure: Restriction on the protocol which can be used to serve this URL/handler (HTTP, HTTPS or either). url: Regular expression used to fully match against the request URLs path. See Special Cases for using static_dir. static_files: Handler id attribute that maps URL to the appropriate file. Can use back regex references to the string matched to url. upload: Regular expression used by the application configuration program to know which files are uploaded as blobs. It's very difficult to determine this using just the url and static_files so this attribute must be included. Required when defining a static_files mapping. A matching file name must fully match against the upload regex, similar to how url is matched against the request path. Do not begin upload with ^ or end it with $. static_dir: Handler id that maps the provided url to a sub-directory within the application directory. See Special Cases. mime_type: When used with static_files and static_dir the mime-type of files served from those directories are overridden with this value. script: Handler id that maps URLs to scipt handler within the application directory that will run using CGI. expiration: When used with static files and directories, the time delta to use for cache expiration. Has the form '4d 5h 30m 15s', where each letter signifies days, hours, minutes, and seconds, respectively. The 's' for seconds may be omitted. Only one amount must be specified, combining multiple amounts is optional. Example good values: '10', '1d 6h', '1h 30m', '7d 7d 7d', '5m 30'. Special cases: When defining a static_dir handler, do not use a regular expression in the url attribute. Both the url and static_dir attributes are automatically mapped to these equivalents: <url>/(.*) <static_dir>/\1 For example: url: /images static_dir: images_folder Is the same as this static_files declaration: url: /images/(.*) static_files: images/\1 upload: images/(.*) """ ATTRIBUTES = { URL: validation.Optional(_URL_REGEX), LOGIN: validation.Options(LOGIN_OPTIONAL, LOGIN_REQUIRED, LOGIN_ADMIN, default=LOGIN_OPTIONAL), SECURE: validation.Options(SECURE_HTTP, SECURE_HTTPS, SECURE_HTTP_OR_HTTPS, default=SECURE_HTTP), HANDLER_STATIC_FILES: validation.Optional(_FILES_REGEX), UPLOAD: validation.Optional(_FILES_REGEX), HANDLER_STATIC_DIR: validation.Optional(_FILES_REGEX), MIME_TYPE: validation.Optional(str), EXPIRATION: validation.Optional(_EXPIRATION_REGEX), HANDLER_SCRIPT: validation.Optional(_FILES_REGEX), REQUIRE_MATCHING_FILE: validation.Optional(bool), } COMMON_FIELDS = set([URL, LOGIN, SECURE]) ALLOWED_FIELDS = { HANDLER_STATIC_FILES: (MIME_TYPE, UPLOAD, EXPIRATION, REQUIRE_MATCHING_FILE), HANDLER_STATIC_DIR: (MIME_TYPE, EXPIRATION, REQUIRE_MATCHING_FILE), HANDLER_SCRIPT: (), } def GetHandler(self): """Get handler for mapping. Returns: Value of the handler (determined by handler id attribute). """ return getattr(self, self.GetHandlerType()) def GetHandlerType(self): """Get handler type of mapping. Returns: Handler type determined by which handler id attribute is set. Raises: UnknownHandlerType when none of the no handler id attributes are set. UnexpectedHandlerAttribute when an unexpected attribute is set for the discovered handler type. HandlerTypeMissingAttribute when the handler is missing a required attribute for its handler type. """ for id_field in URLMap.ALLOWED_FIELDS.iterkeys(): if getattr(self, id_field) is not None: mapping_type = id_field break else: raise appinfo_errors.UnknownHandlerType( 'Unknown url handler type.\n%s' % str(self)) allowed_fields = URLMap.ALLOWED_FIELDS[mapping_type] for attribute in self.ATTRIBUTES.iterkeys(): if (getattr(self, attribute) is not None and not (attribute in allowed_fields or attribute in URLMap.COMMON_FIELDS or attribute == mapping_type)): raise appinfo_errors.UnexpectedHandlerAttribute( 'Unexpected attribute "%s" for mapping type %s.' % (attribute, mapping_type)) if mapping_type == HANDLER_STATIC_FILES and not self.upload: raise appinfo_errors.MissingHandlerAttribute( 'Missing "%s" attribute for URL "%s".' % (UPLOAD, self.url)) return mapping_type def CheckInitialized(self): """Adds additional checking to make sure handler has correct fields. In addition to normal ValidatedCheck calls GetHandlerType which validates all the handler fields are configured properly. Raises: UnknownHandlerType when none of the no handler id attributes are set. UnexpectedHandlerAttribute when an unexpected attribute is set for the discovered handler type. HandlerTypeMissingAttribute when the handler is missing a required attribute for its handler type. """ super(URLMap, self).CheckInitialized() self.GetHandlerType() class AppInfoExternal(validation.Validated): """Class representing users application info. This class is passed to a yaml_object builder to provide the validation for the application information file format parser. Attributes: application: Unique identifier for application. version: Application's major version number. runtime: Runtime used by application. api_version: Which version of APIs to use. handlers: List of URL handlers. default_expiration: Default time delta to use for cache expiration for all static files, unless they have their own specific 'expiration' set. See the URLMap.expiration field's documentation for more information. skip_files: An re object. Files that match this regular expression will not be uploaded by appcfg.py. For example: skip_files: | .svn.*| #.*# """ ATTRIBUTES = { APPLICATION: APPLICATION_RE_STRING, VERSION: VERSION_RE_STRING, RUNTIME: RUNTIME_RE_STRING, API_VERSION: API_VERSION_RE_STRING, HANDLERS: validation.Optional(validation.Repeated(URLMap)), DEFAULT_EXPIRATION: validation.Optional(_EXPIRATION_REGEX), SKIP_FILES: validation.RegexStr(default=DEFAULT_SKIP_FILES) } def CheckInitialized(self): """Ensures that at least one url mapping is provided. Raises: MissingURLMapping when no URLMap objects are present in object. TooManyURLMappings when there are too many URLMap entries. """ super(AppInfoExternal, self).CheckInitialized() if not self.handlers: raise appinfo_errors.MissingURLMapping( 'No URLMap entries found in application configuration') if len(self.handlers) > MAX_URL_MAPS: raise appinfo_errors.TooManyURLMappings( 'Found more than %d URLMap entries in application configuration' % MAX_URL_MAPS) def LoadSingleAppInfo(app_info): """Load a single AppInfo object where one and only one is expected. Args: app_info: A file-like object or string. If it is a string, parse it as a configuration file. If it is a file-like object, read in data and parse. Returns: An instance of AppInfoExternal as loaded from a YAML file. Raises: EmptyConfigurationFile when there are no documents in YAML file. MultipleConfigurationFile when there is more than one document in YAML file. """ builder = yaml_object.ObjectBuilder(AppInfoExternal) handler = yaml_builder.BuilderHandler(builder) listener = yaml_listener.EventListener(handler) listener.Parse(app_info) app_infos = handler.GetResults() if len(app_infos) < 1: raise appinfo_errors.EmptyConfigurationFile() if len(app_infos) > 1: raise appinfo_errors.MultipleConfigurationFile() return app_infos[0] def ParseExpiration(expiration): """Parses an expiration delta string. Args: expiration: String that matches _DELTA_REGEX. Returns: Time delta in seconds. """ delta = 0 for match in re.finditer(_DELTA_REGEX, expiration): amount = int(match.group(1)) units = _EXPIRATION_CONVERSIONS.get(match.group(2).lower(), 1) delta += amount * units return delta _file_path_positive_re = re.compile(r'^[ 0-9a-zA-Z\._\+/\$-]{1,256}$') _file_path_negative_1_re = re.compile(r'\.\.|^\./|\.$|/\./|^-') _file_path_negative_2_re = re.compile(r'//|/$') _file_path_negative_3_re = re.compile(r'^ | $|/ | /') def ValidFilename(filename): """Determines if filename is valid. filename must be a valid pathname. - It must contain only letters, numbers, _, +, /, $, ., and -. - It must be less than 256 chars. - It must not contain "/./", "/../", or "//". - It must not end in "/". - All spaces must be in the middle of a directory or file name. Args: filename: The filename to validate. Returns: An error string if the filename is invalid. Returns '' if the filename is valid. """ if _file_path_positive_re.match(filename) is None: return 'Invalid character in filename: %s' % filename if _file_path_negative_1_re.search(filename) is not None: return ('Filename cannot contain "." or ".." or start with "-": %s' % filename) if _file_path_negative_2_re.search(filename) is not None: return 'Filename cannot have trailing / or contain //: %s' % filename if _file_path_negative_3_re.search(filename) is not None: return 'Any spaces must be in the middle of a filename: %s' % filename return ''