1.9.30 sync.
[gae.git] / python / google / appengine / api / dispatchinfo.py
blob9088500098059fe8a291691c348a3359a1715421
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
19 """Dispatch configuration tools.
21 Library for parsing dispatch.yaml files and working with these in memory.
22 """
31 import re
33 from google.appengine.api import appinfo
34 from google.appengine.api import validation
35 from google.appengine.api import yaml_builder
36 from google.appengine.api import yaml_listener
37 from google.appengine.api import yaml_object
39 _URL_SPLITTER_RE = re.compile(r'^([^/]+)(/.*)$')
46 _URL_HOST_EXACT_PATTERN_RE = re.compile(r"""
47 # 0 or more . terminated hostname segments (may not start or end in -).
48 ^([a-z0-9]([a-z0-9\-]*[a-z0-9])*\.)*
49 # followed by a host name segment.
50 ([a-z0-9]([a-z0-9\-]*[a-z0-9])*)$""", re.VERBOSE)
52 _URL_IP_V4_ADDR_RE = re.compile(r"""
53 #4 1-3 digit numbers separated by .
54 ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$""", re.VERBOSE)
58 _URL_HOST_SUFFIX_PATTERN_RE = re.compile(r"""
59 # Single star or
60 ^([*]|
61 # Host prefix with no ., Ex '*-a' or
62 [*][a-z0-9\-]*[a-z0-9]|
63 # Host prefix with ., Ex '*-a.b-c.d'
64 [*](\.|[a-z0-9\-]*[a-z0-9]\.)([a-z0-9]([a-z0-9\-]*[a-z0-9])*\.)*
65 ([a-z0-9]([a-z0-9\-]*[a-z0-9])*))$
66 """, re.VERBOSE)
68 APPLICATION = 'application'
69 DISPATCH = 'dispatch'
70 URL = 'url'
71 MODULE = 'module'
74 class Error(Exception):
75 """Base class for errors in this module."""
78 class MalformedDispatchConfigurationError(Error):
79 """Configuration file for dispatch is malformed."""
82 class DispatchEntryURLValidator(validation.Validator):
83 """Validater for URL patterns."""
85 def Validate(self, value, unused_key=None):
86 """Validates an URL pattern."""
87 if value is None:
88 raise validation.MissingAttribute('url must be specified')
89 if not isinstance(value, basestring):
90 raise validation.ValidationError('url must be a string, not \'%r\'' %
91 type(value))
93 url_holder = ParsedURL(value)
94 if url_holder.host_exact:
95 _ValidateMatch(_URL_HOST_EXACT_PATTERN_RE, url_holder.host,
96 'invalid host_pattern \'%s\'' % url_holder.host)
99 _ValidateNotIpV4Address(url_holder.host)
100 else:
101 _ValidateMatch(_URL_HOST_SUFFIX_PATTERN_RE, url_holder.host_pattern,
102 'invalid host_pattern \'%s\'' % url_holder.host_pattern)
106 return value
109 class ParsedURL(object):
110 """Dispath Entry URL holder class.
112 Attributes:
113 host_pattern: The host pattern component of the URL pattern.
114 host_exact: True iff the host pattern does not start with a *.
115 host: host_pattern with any leading * removed.
116 path_pattern: The path pattern component of the URL pattern.
117 path_exact: True iff path_pattern does not end with a *.
118 path: path_pattern with any trailing * removed.
121 def __init__(self, url_pattern):
122 """Initializes this ParsedURL with an URL pattern value.
124 Args:
125 url_pattern: An URL pattern that conforms to the regular expression
126 '^([^/]+)(/.*)$'.
128 Raises:
129 validation.ValidationError: When url_pattern does not match the required
130 regular expression.
132 split_matcher = _ValidateMatch(_URL_SPLITTER_RE, url_pattern,
133 'invalid url \'%s\'' % url_pattern)
134 self.host_pattern, self.path_pattern = split_matcher.groups()
135 if self.host_pattern.startswith('*'):
136 self.host_exact = False
137 self.host = self.host_pattern[1:]
138 else:
139 self.host_exact = True
140 self.host = self.host_pattern
142 if self.path_pattern.endswith('*'):
143 self.path_exact = False
144 self.path = self.path_pattern[:-1]
145 else:
146 self.path_exact = True
147 self.path = self.path_pattern
150 def _ValidateMatch(regex, value, message):
151 """Validate value matches regex."""
152 matcher = regex.match(value)
153 if not matcher:
154 raise validation.ValidationError(message)
155 return matcher
158 def _ValidateNotIpV4Address(host):
159 """Validate host is not an IPV4 address."""
160 matcher = _URL_IP_V4_ADDR_RE.match(host)
161 if matcher and sum(1 for x in matcher.groups() if int(x) <= 255) == 4:
162 raise validation.ValidationError('Host may not match an ipv4 address \'%s\''
163 % host)
164 return matcher
167 class DispatchEntry(validation.Validated):
168 """A Dispatch entry describes a mapping from a URL pattern to a module."""
169 ATTRIBUTES = {
170 URL: DispatchEntryURLValidator(),
171 MODULE: validation.Regex(appinfo.MODULE_ID_RE_STRING),
175 class DispatchInfoExternal(validation.Validated):
176 """Describes the format of a dispatch.yaml file."""
177 ATTRIBUTES = {
178 APPLICATION: validation.Optional(appinfo.APPLICATION_RE_STRING),
179 DISPATCH: validation.Optional(validation.Repeated(DispatchEntry)),
183 def LoadSingleDispatch(dispatch_info, open_fn=None):
184 """Load a dispatch.yaml file or string and return a DispatchInfoExternal.
186 Args:
187 dispatch_info: The contents of a dispatch.yaml file as a string, or an open
188 file object.
189 open_fn: Function for opening files. Unused here, needed to provide
190 a polymorphic API used by appcfg.py yaml parsing.
192 Returns:
193 A DispatchInfoExternal instance which represents the contents of the parsed
194 yaml file.
196 Raises:
197 MalformedDispatchConfigurationError: The yaml file contains multiple
198 dispatch sections.
199 yaml_errors.EventError: An error occured while parsing the yaml file.
201 builder = yaml_object.ObjectBuilder(DispatchInfoExternal)
202 handler = yaml_builder.BuilderHandler(builder)
203 listener = yaml_listener.EventListener(handler)
204 listener.Parse(dispatch_info)
206 parsed_yaml = handler.GetResults()
207 if not parsed_yaml:
208 return DispatchInfoExternal()
209 if len(parsed_yaml) > 1:
210 raise MalformedDispatchConfigurationError('Multiple dispatch: sections '
211 'in configuration.')
212 return parsed_yaml[0]