1#
2# Copyright (C) 2018 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import datetime
18import httplib
19import logging
20import os
21import urlparse
22
23from google.appengine.api import users
24import stripe
25import webapp2
26from webapp2_extras import jinja2 as wa2_jinja2
27from webapp2_extras import sessions
28
29import errors
30from webapp.src.utils import datetime_util
31
32
33class BaseHandler(webapp2.RequestHandler):
34    """BaseHandler for all requests."""
35
36    def initialize(self, request, response):
37        """Initializes this request handler."""
38        webapp2.RequestHandler.initialize(self, request, response)
39        self.session_backend = 'datastore'
40
41    def verify_origin(self):
42        """This function will check the request is comming from the same domain."""
43        server_host = os.environ.get('ENDPOINTS_SERVICE_NAME')
44        request_host = self.request.headers.get('Host')
45        request_referer = self.request.headers.get('Referer')
46        if request_referer:
47            request_referer = urlparse.urlsplit(request_referer)[1]
48        else:
49            request_referer = request_host
50        logging.info('server: %s, request: %s', server_host, request_referer)
51        if server_host and request_referer and server_host != request_referer:
52            raise errors.Error(httplib.FORBIDDEN)
53
54    def dispatch(self):
55        """Dispatch the request.
56
57        This will first check if there's a handler_method defined
58        in the matched route, and if not it'll use the method correspondent to
59        the request method (get(), post() etc).
60        """
61        self.session_store = sessions.get_store(request=self.request)
62        # Forwards the method for RESTful support.
63        self.forward_method()
64        # Security headers.
65        # https://www.owasp.org/index.php/List_of_useful_HTTP_headers
66        self.response.headers['x-content-type-options'] = 'nosniff'
67        self.response.headers['x-frame-options'] = 'SAMEORIGIN'
68        self.response.headers['x-xss-protection'] = '1; mode=block'
69        try:
70            webapp2.RequestHandler.dispatch(self)
71        finally:
72            self.session_store.save_sessions(self.response)
73        # Disabled for now because host is appspot.com in production.
74        #self.verify_origin()
75
76    @webapp2.cached_property
77    def session(self):
78        # Returns a session using the default cookie key.
79        return self.session_store.get_session()
80
81    def handle_exception(self, exception, debug=False):
82        """Render the exception as HTML."""
83        logging.exception(exception)
84
85        # Create response dictionary and status defaults.
86        tpl = 'error.html'
87        status = httplib.INTERNAL_SERVER_ERROR
88        resp_dict = {
89            'message': 'A server error occurred.',
90        }
91        url_parts = self.urlsplit()
92        redirect_url = '%s?%s' % (url_parts[2], url_parts[4])
93
94        # Use error code if a HTTPException, or generic 500.
95        if isinstance(exception, webapp2.HTTPException):
96            status = exception.code
97            resp_dict['message'] = exception.detail
98        elif isinstance(exception, errors.FormValidationError):
99            status = exception.code
100            resp_dict['message'] = exception.msg
101            resp_dict['errors'] = exception.errors
102            self.session['form_errors'] = exception.errors
103            # Redirect user to current view URL.
104            return self.redirect(redirect_url)
105        elif isinstance(exception, stripe.StripeError):
106            status = exception.http_status
107            resp_dict['errors'] = exception.json_body['error']['message']
108            self.session['form_errors'] = [
109                exception.json_body['error']['message']
110            ]
111            return self.redirect(redirect_url)
112        elif isinstance(exception, (errors.Error, errors.AclError)):
113            status = exception.code
114            resp_dict['message'] = exception.msg
115
116        resp_dict['status'] = status
117
118        # Render output.
119        self.response.status_int = status
120        self.response.status_message = httplib.responses[status]
121        # Render the exception response into the error template.
122        self.response.write(self.jinja2.render_template(tpl, **resp_dict))
123
124    # @Override
125    def get(self, *args, **kwargs):
126        self.abort(httplib.NOT_IMPLEMENTED)
127
128    # @Override
129    def post(self, *args, **kwargs):
130        self.abort(httplib.NOT_IMPLEMENTED)
131
132    # @Override
133    def put(self, *args, **kwargs):
134        self.abort(httplib.NOT_IMPLEMENTED)
135
136    # @Override
137    def delete(self, *args, **kwargs):
138        self.abort(httplib.NOT_IMPLEMENTED)
139
140    # @Override
141    def head(self, *args, **kwargs):
142        pass
143
144    def urlsplit(self):
145        """Return a tuple of the URL."""
146        return urlparse.urlsplit(self.request.url)
147
148    def path(self):
149        """Returns the path of the current URL."""
150        return self.urlsplit()[2]
151
152    def forward_method(self):
153        """Check for a method override param and change in the request."""
154        valid = (None, 'get', 'post', 'put', 'delete', 'head', 'options')
155        method = self.request.POST.get('__method__')
156        if not method:  # Backbone's _method parameter.
157            method = self.request.POST.get('_method')
158        if method not in valid:
159            logging.debug('Invalid method %s requested!', method)
160            method = None
161        logging.debug('Method being changed from %s to %s by request',
162                      self.request.route.handler_method, method)
163        self.request.route.handler_method = method
164
165    def render(self, resp, status=httplib.OK):
166        """Render the response as HTML."""
167        user = users.get_current_user()
168        if user:
169            url = users.create_logout_url(self.request.uri)
170            url_linktext = "Logout"
171        else:
172            url = users.create_login_url(self.request.uri)
173            url_linktext = "Login"
174
175        resp.update({
176            # Defaults go here.
177            'now': datetime.datetime.now(),
178            'dest_url': str(self.request.get('dest_url', '')),
179            'form_errors': self.session.pop('form_errors', []),
180            'user': user,
181            'url': url,
182            'url_linktext': url_linktext,
183            "convert_time": datetime_util.GetTimeWithTimezone
184        })
185
186        if 'preload' not in resp:
187            resp['preload'] = {}
188
189        self.response.status_int = status
190        self.response.status_message = httplib.responses[status]
191        self.response.write(self.jinja2.render_template(self.template, **resp))
192
193    @webapp2.cached_property
194    def jinja2(self):
195        """Returns a Jinja2 renderer cached in the app registry."""
196        jinja_config = {
197            'template_path':
198            os.path.join(os.path.dirname(__file__), "../../static"),
199            'compiled_path':
200            None,
201            'force_compiled':
202            False,
203            'environment_args': {
204                'autoescape': True,
205                'extensions': [
206                    'jinja2.ext.autoescape',
207                    'jinja2.ext.with_',
208                ],
209            },
210            'globals':
211            None,
212            'filters':
213            None,
214        }
215        return wa2_jinja2.Jinja2(app=self.app, config=jinja_config)
216