27 |
27 |
28 import re |
28 import re |
29 |
29 |
30 from google.appengine.api import appinfo_errors |
30 from google.appengine.api import appinfo_errors |
31 from google.appengine.api import validation |
31 from google.appengine.api import validation |
|
32 from google.appengine.api import yaml_builder |
32 from google.appengine.api import yaml_listener |
33 from google.appengine.api import yaml_listener |
33 from google.appengine.api import yaml_builder |
|
34 from google.appengine.api import yaml_object |
34 from google.appengine.api import yaml_object |
35 |
35 |
36 |
36 |
37 _URL_REGEX = r'(?!\^)/|\.|(\(.).*(?!\$).' |
37 _URL_REGEX = r'(?!\^)/|\.|(\(.).*(?!\$).' |
38 _FILES_REGEX = r'(?!\^).*(?!\$).' |
38 _FILES_REGEX = r'(?!\^).*(?!\$).' |
39 |
39 |
40 _DELTA_REGEX = r'([1-9][0-9]*)([DdHhMm]|[sS]?)' |
40 _DELTA_REGEX = r'([1-9][0-9]*)([DdHhMm]|[sS]?)' |
41 _EXPIRATION_REGEX = r'\s*(%s)(\s+%s)*\s*' % (_DELTA_REGEX, _DELTA_REGEX) |
41 _EXPIRATION_REGEX = r'\s*(%s)(\s+%s)*\s*' % (_DELTA_REGEX, _DELTA_REGEX) |
42 |
42 |
|
43 _SERVICE_RE_STRING = r'(mail|xmpp_message)' |
|
44 |
43 _EXPIRATION_CONVERSIONS = { |
45 _EXPIRATION_CONVERSIONS = { |
44 'd': 60 * 60 * 24, |
46 'd': 60 * 60 * 24, |
45 'h': 60 * 60, |
47 'h': 60 * 60, |
46 'm': 60, |
48 'm': 60, |
47 's': 1, |
49 's': 1, |
48 } |
50 } |
49 |
51 |
50 APP_ID_MAX_LEN = 100 |
52 APP_ID_MAX_LEN = 100 |
51 MAJOR_VERSION_ID_MAX_LEN = 100 |
53 MAJOR_VERSION_ID_MAX_LEN = 100 |
52 MAX_URL_MAPS = 100 |
54 MAX_URL_MAPS = 100 |
99 RUNTIME = 'runtime' |
101 RUNTIME = 'runtime' |
100 API_VERSION = 'api_version' |
102 API_VERSION = 'api_version' |
101 HANDLERS = 'handlers' |
103 HANDLERS = 'handlers' |
102 DEFAULT_EXPIRATION = 'default_expiration' |
104 DEFAULT_EXPIRATION = 'default_expiration' |
103 SKIP_FILES = 'skip_files' |
105 SKIP_FILES = 'skip_files' |
|
106 SERVICES = 'inbound_services' |
104 |
107 |
105 |
108 |
106 class URLMap(validation.Validated): |
109 class URLMap(validation.Validated): |
107 """Mapping from URLs to handlers. |
110 """Mapping from URLs to handlers. |
108 |
111 |
174 upload: images/(.*) |
177 upload: images/(.*) |
175 """ |
178 """ |
176 |
179 |
177 ATTRIBUTES = { |
180 ATTRIBUTES = { |
178 |
181 |
179 URL: validation.Optional(_URL_REGEX), |
182 URL: validation.Optional(_URL_REGEX), |
180 LOGIN: validation.Options(LOGIN_OPTIONAL, |
183 LOGIN: validation.Options(LOGIN_OPTIONAL, |
181 LOGIN_REQUIRED, |
184 LOGIN_REQUIRED, |
182 LOGIN_ADMIN, |
185 LOGIN_ADMIN, |
183 default=LOGIN_OPTIONAL), |
186 default=LOGIN_OPTIONAL), |
184 |
187 |
185 SECURE: validation.Options(SECURE_HTTP, |
188 SECURE: validation.Options(SECURE_HTTP, |
186 SECURE_HTTPS, |
189 SECURE_HTTPS, |
187 SECURE_HTTP_OR_HTTPS, |
190 SECURE_HTTP_OR_HTTPS, |
188 default=SECURE_HTTP), |
191 default=SECURE_HTTP), |
189 |
192 |
190 |
193 |
191 |
194 |
192 HANDLER_STATIC_FILES: validation.Optional(_FILES_REGEX), |
195 HANDLER_STATIC_FILES: validation.Optional(_FILES_REGEX), |
193 UPLOAD: validation.Optional(_FILES_REGEX), |
196 UPLOAD: validation.Optional(_FILES_REGEX), |
194 |
197 |
195 |
198 |
196 HANDLER_STATIC_DIR: validation.Optional(_FILES_REGEX), |
199 HANDLER_STATIC_DIR: validation.Optional(_FILES_REGEX), |
197 |
200 |
198 |
201 |
199 MIME_TYPE: validation.Optional(str), |
202 MIME_TYPE: validation.Optional(str), |
200 EXPIRATION: validation.Optional(_EXPIRATION_REGEX), |
203 EXPIRATION: validation.Optional(_EXPIRATION_REGEX), |
201 |
204 |
202 |
205 |
203 HANDLER_SCRIPT: validation.Optional(_FILES_REGEX), |
206 HANDLER_SCRIPT: validation.Optional(_FILES_REGEX), |
204 |
207 |
205 REQUIRE_MATCHING_FILE: validation.Optional(bool), |
208 REQUIRE_MATCHING_FILE: validation.Optional(bool), |
206 } |
209 } |
207 |
210 |
208 COMMON_FIELDS = set([URL, LOGIN, SECURE]) |
211 COMMON_FIELDS = set([URL, LOGIN, SECURE]) |
209 |
212 |
210 ALLOWED_FIELDS = { |
213 ALLOWED_FIELDS = { |
211 HANDLER_STATIC_FILES: (MIME_TYPE, UPLOAD, EXPIRATION, |
214 HANDLER_STATIC_FILES: (MIME_TYPE, UPLOAD, EXPIRATION, |
212 REQUIRE_MATCHING_FILE), |
215 REQUIRE_MATCHING_FILE), |
213 HANDLER_STATIC_DIR: (MIME_TYPE, EXPIRATION, REQUIRE_MATCHING_FILE), |
216 HANDLER_STATIC_DIR: (MIME_TYPE, EXPIRATION, REQUIRE_MATCHING_FILE), |
214 HANDLER_SCRIPT: (), |
217 HANDLER_SCRIPT: (), |
215 } |
218 } |
216 |
219 |
217 def GetHandler(self): |
220 def GetHandler(self): |
218 """Get handler for mapping. |
221 """Get handler for mapping. |
219 |
222 |
251 for attribute in self.ATTRIBUTES.iterkeys(): |
254 for attribute in self.ATTRIBUTES.iterkeys(): |
252 if (getattr(self, attribute) is not None and |
255 if (getattr(self, attribute) is not None and |
253 not (attribute in allowed_fields or |
256 not (attribute in allowed_fields or |
254 attribute in URLMap.COMMON_FIELDS or |
257 attribute in URLMap.COMMON_FIELDS or |
255 attribute == mapping_type)): |
258 attribute == mapping_type)): |
256 raise appinfo_errors.UnexpectedHandlerAttribute( |
259 raise appinfo_errors.UnexpectedHandlerAttribute( |
257 'Unexpected attribute "%s" for mapping type %s.' % |
260 'Unexpected attribute "%s" for mapping type %s.' % |
258 (attribute, mapping_type)) |
261 (attribute, mapping_type)) |
259 |
262 |
260 if mapping_type == HANDLER_STATIC_FILES and not self.upload: |
263 if mapping_type == HANDLER_STATIC_FILES and not self.upload: |
261 raise appinfo_errors.MissingHandlerAttribute( |
264 raise appinfo_errors.MissingHandlerAttribute( |
262 'Missing "%s" attribute for URL "%s".' % (UPLOAD, self.url)) |
265 'Missing "%s" attribute for URL "%s".' % (UPLOAD, self.url)) |
263 |
266 |
307 """ |
310 """ |
308 |
311 |
309 ATTRIBUTES = { |
312 ATTRIBUTES = { |
310 |
313 |
311 |
314 |
312 APPLICATION: APPLICATION_RE_STRING, |
315 APPLICATION: APPLICATION_RE_STRING, |
313 VERSION: VERSION_RE_STRING, |
316 VERSION: VERSION_RE_STRING, |
314 RUNTIME: RUNTIME_RE_STRING, |
317 RUNTIME: RUNTIME_RE_STRING, |
315 |
318 |
316 |
319 |
317 API_VERSION: API_VERSION_RE_STRING, |
320 API_VERSION: API_VERSION_RE_STRING, |
318 HANDLERS: validation.Optional(validation.Repeated(URLMap)), |
321 HANDLERS: validation.Optional(validation.Repeated(URLMap)), |
319 DEFAULT_EXPIRATION: validation.Optional(_EXPIRATION_REGEX), |
322 |
320 SKIP_FILES: validation.RegexStr(default=DEFAULT_SKIP_FILES) |
323 SERVICES: validation.Optional(validation.Repeated( |
|
324 validation.Regex(_SERVICE_RE_STRING))), |
|
325 DEFAULT_EXPIRATION: validation.Optional(_EXPIRATION_REGEX), |
|
326 SKIP_FILES: validation.RegexStr(default=DEFAULT_SKIP_FILES) |
321 } |
327 } |
322 |
328 |
323 def CheckInitialized(self): |
329 def CheckInitialized(self): |
324 """Ensures that at least one url mapping is provided. |
330 """Ensures that at least one url mapping is provided. |
325 |
331 |
347 |
353 |
348 Returns: |
354 Returns: |
349 An instance of AppInfoExternal as loaded from a YAML file. |
355 An instance of AppInfoExternal as loaded from a YAML file. |
350 |
356 |
351 Raises: |
357 Raises: |
352 EmptyConfigurationFile when there are no documents in YAML file. |
358 ValueError: if a specified service is not valid. |
353 MultipleConfigurationFile when there is more than one document in YAML |
359 EmptyConfigurationFile: when there are no documents in YAML file. |
|
360 MultipleConfigurationFile: when there is more than one document in YAML |
354 file. |
361 file. |
355 """ |
362 """ |
356 builder = yaml_object.ObjectBuilder(AppInfoExternal) |
363 builder = yaml_object.ObjectBuilder(AppInfoExternal) |
357 handler = yaml_builder.BuilderHandler(builder) |
364 handler = yaml_builder.BuilderHandler(builder) |
358 listener = yaml_listener.EventListener(handler) |
365 listener = yaml_listener.EventListener(handler) |
411 is valid. |
418 is valid. |
412 """ |
419 """ |
413 if _file_path_positive_re.match(filename) is None: |
420 if _file_path_positive_re.match(filename) is None: |
414 return 'Invalid character in filename: %s' % filename |
421 return 'Invalid character in filename: %s' % filename |
415 if _file_path_negative_1_re.search(filename) is not None: |
422 if _file_path_negative_1_re.search(filename) is not None: |
416 return ('Filename cannot contain "." or ".." or start with "-": %s' % |
423 return ('Filename cannot contain "." or ".." ' |
|
424 'or start with "-" or "_ah/": %s' % |
417 filename) |
425 filename) |
418 if _file_path_negative_2_re.search(filename) is not None: |
426 if _file_path_negative_2_re.search(filename) is not None: |
419 return 'Filename cannot have trailing / or contain //: %s' % filename |
427 return 'Filename cannot have trailing / or contain //: %s' % filename |
420 if _file_path_negative_3_re.search(filename) is not None: |
428 if _file_path_negative_3_re.search(filename) is not None: |
421 return 'Any spaces must be in the middle of a filename: %s' % filename |
429 return 'Any spaces must be in the middle of a filename: %s' % filename |