Source code for google.appengine.api.dispatchinfo

#!/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.
#


"""Dispatch configuration tools.

Library for parsing dispatch.yaml files and working with these in memory.
"""

from __future__ import absolute_import
from __future__ import unicode_literals







import os
import re
from google.appengine._internal import six_subset


if os.environ.get('APPENGINE_RUNTIME') == 'python27':
  from google.appengine.api import appinfo
  from google.appengine.api import validation
  from google.appengine.api import yaml_builder
  from google.appengine.api import yaml_listener
  from google.appengine.api import yaml_object
else:
  from google.appengine.api import appinfo
  from google.appengine.api import validation
  from google.appengine.api import yaml_builder
  from google.appengine.api import yaml_listener
  from google.appengine.api import yaml_object


_URL_SPLITTER_RE = re.compile(r'^([^/]+)(/.*)$')






_URL_HOST_EXACT_PATTERN_RE = re.compile(r"""
# 0 or more . terminated hostname segments (may not start or end in -).
^([a-z0-9]([a-z0-9\-]*[a-z0-9])*\.)*
# followed by a host name segment.
([a-z0-9]([a-z0-9\-]*[a-z0-9])*)$""", re.VERBOSE)

_URL_IP_V4_ADDR_RE = re.compile(r"""
#4 1-3 digit numbers separated by .
^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$""", re.VERBOSE)



_URL_HOST_SUFFIX_PATTERN_RE = re.compile(r"""
# Single star or
^([*]|
# Host prefix with no .,  Ex '*-a' or
[*][a-z0-9\-]*[a-z0-9]|
# Host prefix with ., Ex '*-a.b-c.d'
[*](\.|[a-z0-9\-]*[a-z0-9]\.)([a-z0-9]([a-z0-9\-]*[a-z0-9])*\.)*
([a-z0-9]([a-z0-9\-]*[a-z0-9])*))$
""", re.VERBOSE)

APPLICATION = 'application'
DISPATCH = 'dispatch'
URL = 'url'
MODULE = 'module'
SERVICE = 'service'


[docs]class Error(Exception): """Base class for errors in this module."""
[docs]class MalformedDispatchConfigurationError(Error): """Configuration file for dispatch is malformed."""
[docs]class DispatchEntryURLValidator(validation.Validator): """Validater for URL patterns."""
[docs] def Validate(self, value, unused_key=None): """Validates an URL pattern.""" if value is None: raise validation.MissingAttribute('url must be specified') if not isinstance(value, six_subset.string_types): raise validation.ValidationError('url must be a string, not \'%r\'' % type(value)) url_holder = ParsedURL(value) if url_holder.host_exact: _ValidateMatch(_URL_HOST_EXACT_PATTERN_RE, url_holder.host, 'invalid host_pattern \'%s\'' % url_holder.host) _ValidateNotIpV4Address(url_holder.host) else: _ValidateMatch(_URL_HOST_SUFFIX_PATTERN_RE, url_holder.host_pattern, 'invalid host_pattern \'%s\'' % url_holder.host_pattern) return value
[docs]class ParsedURL(object): """Dispath Entry URL holder class. Attributes: host_pattern: The host pattern component of the URL pattern. host_exact: True if the host pattern does not start with a *. host: host_pattern with any leading * removed. path_pattern: The path pattern component of the URL pattern. path_exact: True if the path_pattern does not end with a *. path: path_pattern with any trailing * removed. """ def __init__(self, url_pattern): """Initializes this ParsedURL with an URL pattern value. Args: url_pattern: An URL pattern that conforms to the regular expression '^([^/]+)(/.*)$'. Raises: validation.ValidationError: When url_pattern does not match the required regular expression. """ split_matcher = _ValidateMatch(_URL_SPLITTER_RE, url_pattern, 'invalid url \'%s\'' % url_pattern) self.host_pattern, self.path_pattern = split_matcher.groups() if self.host_pattern.startswith('*'): self.host_exact = False self.host = self.host_pattern[1:] else: self.host_exact = True self.host = self.host_pattern if self.path_pattern.endswith('*'): self.path_exact = False self.path = self.path_pattern[:-1] else: self.path_exact = True self.path = self.path_pattern
def _ValidateMatch(regex, value, message): """Validate value matches regex.""" matcher = regex.match(value) if not matcher: raise validation.ValidationError(message) return matcher def _ValidateNotIpV4Address(host): """Validate host is not an IPV4 address.""" matcher = _URL_IP_V4_ADDR_RE.match(host) if matcher and sum(1 for x in matcher.groups() if int(x) <= 255) == 4: raise validation.ValidationError('Host may not match an ipv4 address \'%s\'' % host) return matcher
[docs]class DispatchEntry(validation.Validated): """A Dispatch entry describes a mapping from a URL pattern to a module.""" ATTRIBUTES = { URL: DispatchEntryURLValidator(), MODULE: validation.Optional(appinfo.MODULE_ID_RE_STRING), SERVICE: validation.Optional(appinfo.MODULE_ID_RE_STRING) }
[docs]class DispatchInfoExternal(validation.Validated): """Describes the format of a dispatch.yaml file.""" ATTRIBUTES = { APPLICATION: validation.Optional(appinfo.APPLICATION_RE_STRING), DISPATCH: validation.Optional(validation.Repeated(DispatchEntry)), }
[docs]def LoadSingleDispatch(dispatch_info, open_fn=None): """Load a dispatch.yaml file or string and return a DispatchInfoExternal. Args: dispatch_info: The contents of a dispatch.yaml file as a string, or an open file object. open_fn: Function for opening files. Unused here, needed to provide a polymorphic API used by appcfg.py yaml parsing. Returns: A DispatchInfoExternal instance which represents the contents of the parsed yaml file. Raises: MalformedDispatchConfigurationError: The yaml file contains multiple dispatch sections or is missing a required value. yaml_errors.EventError: An error occured while parsing the yaml file. """ builder = yaml_object.ObjectBuilder(DispatchInfoExternal) handler = yaml_builder.BuilderHandler(builder) listener = yaml_listener.EventListener(handler) listener.Parse(dispatch_info) parsed_yaml = handler.GetResults() if not parsed_yaml: return DispatchInfoExternal() if len(parsed_yaml) > 1: raise MalformedDispatchConfigurationError('Multiple dispatch: sections ' 'in configuration.') dispatch_info_external = parsed_yaml[0] for dispatch in getattr(dispatch_info_external, DISPATCH) or []: if dispatch.module and dispatch.service: raise MalformedDispatchConfigurationError( 'Both module: and service: in dispatch entry. Please use only one.') if not (dispatch.module or dispatch.service): raise MalformedDispatchConfigurationError( "Missing required value 'service'.") dispatch.module = dispatch.module or dispatch.service dispatch.service = None return dispatch_info_external