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