gruyere.py
  1  #!/usr/bin/env python2.7
  2  
  3  """Gruyere - a web application with holes.
  4  
  5  Copyright 2017 Google Inc. All rights reserved.
  6  
  7  This code is licensed under the
  8  https://creativecommons.org/licenses/by-nd/3.0/us/
  9  Creative Commons Attribution-No Derivative Works 3.0 United States license.
 10  
 11  DO NOT COPY THIS CODE!
 12  
 13  This application is a small self-contained web application with numerous
 14  security holes. It is provided for use with the Web Application Exploits and
 15  Defenses codelab. You may modify the code for your own use while doing the
 16  codelab but you may not distribute the modified code. Brief excerpts of this
 17  code may be used for educational or instructional purposes provided this
 18  notice is kept intact. By using Gruyere you agree to the Terms of Service
 19  https://www.google.com/intl/en/policies/terms/
 20  """
 21  
 22  __author__ = 'Bruce Leban'
 23  
 24  # system modules
 25  from BaseHTTPServer import BaseHTTPRequestHandler
 26  from BaseHTTPServer import HTTPServer
 27  import cgi
 28  import cPickle
 29  import os
 30  import random
 31  import sys
 32  import threading
 33  import urllib
 34  from urlparse import urlparse
 35  
 36  try:
 37    sys.dont_write_bytecode = True
 38  except AttributeError:
 39    pass
 40  
 41  # our modules
 42  import data
 43  import gtl
 44  
 45  
 46  DB_FILE = '/stored-data.txt'
 47  SECRET_FILE = '/secret.txt'
 48  
 49  INSTALL_PATH = '.'
 50  RESOURCE_PATH = 'resources'
 51  
 52  SPECIAL_COOKIE = '_cookie'
 53  SPECIAL_PROFILE = '_profile'
 54  SPECIAL_DB = '_db'
 55  SPECIAL_PARAMS = '_params'
 56  SPECIAL_UNIQUE_ID = '_unique_id'
 57  
 58  COOKIE_UID = 'uid'
 59  COOKIE_ADMIN = 'is_admin'
 60  COOKIE_AUTHOR = 'is_author'
 61  
 62  
 63  # Set to True to cause the server to exit after processing the current url.
 64  quit_server = False
 65  
 66  # A global copy of the database so that _GetDatabase can access it.
 67  stored_data = None
 68  
 69  # The HTTPServer object.
 70  http_server = None
 71  
 72  # A secret value used to generate hashes to protect cookies from tampering.
 73  cookie_secret = ''
 74  
 75  # File extensions of resource files that we recognize.
 76  RESOURCE_CONTENT_TYPES = {
 77      '.css': 'text/css',
 78      '.gif': 'image/gif',
 79      '.htm': 'text/html',
 80      '.html': 'text/html',
 81      '.js': 'application/javascript',
 82      '.jpeg': 'image/jpeg',
 83      '.jpg': 'image/jpeg',
 84      '.png': 'image/png',
 85      '.ico': 'image/x-icon',
 86      '.text': 'text/plain',
 87      '.txt': 'text/plain',
 88  }
 89  
 90  
 91  def main():
 92    _SetWorkingDirectory()
 93  
 94    global quit_server
 95    quit_server = False
 96  
 97    # Normally, Gruyere only accepts connections to/from localhost. If you
 98    # would like to allow access from other ip addresses, you can change to
 99    # operate in a less secure mode. Set insecure_mode to True to serve on the
100    # hostname instead of localhost and add the addresses of the other machines
101    # to allowed_ips below.
102  
103    insecure_mode = False
104  
105    # WARNING! DO NOT CHANGE THE FOLLOWING SECTION OF CODE!
106  
107    # This application is very exploitable. It takes several precautions to
108    # limit the risk from a real attacker:
109    #   (1) Serve requests on localhost so that it will not be accessible
110    # from other machines.
111    #   (2) If a request is received from any IP other than localhost, quit.
112    # (This protection is implemented in do_GET/do_POST.)
113    #   (3) Inject a random identifier as the first part of the path and
114    # quit if a request is received without this identifier (except for an
115    # empty path which redirects and /favicon.ico).
116    #   (4) Automatically exit after 2 hours (7200 seconds) to mitigate against
117    # accidentally leaving the server running.
118  
119    quit_timer = threading.Timer(7200, lambda: _Exit('Timeout'))   # DO NOT CHANGE
120    quit_timer.start()                                             # DO NOT CHANGE
121  
122    if insecure_mode:                                              # DO NOT CHANGE
123      server_name = os.popen('hostname').read().replace('\n', '')  # DO NOT CHANGE
124    else:                                                          # DO NOT CHANGE
125      server_name = '127.0.0.1'                                    # DO NOT CHANGE
126    server_port = 8008                                             # DO NOT CHANGE
127  
128    # The unique id is created from a CSPRNG.
129    try:                                                           # DO NOT CHANGE
130      r = random.SystemRandom()                                    # DO NOT CHANGE
131    except NotImplementedError:                                    # DO NOT CHANGE
132      _Exit('Could not obtain a CSPRNG source')                    # DO NOT CHANGE
133  
134    global server_unique_id                                        # DO NOT CHANGE
135    server_unique_id = str(r.randint(2**128, 2**(128+1)))          # DO NOT CHANGE
136  
137    # END WARNING!
138  
139    global http_server
140    http_server = HTTPServer((server_name, server_port),
141                             GruyereRequestHandler)
142  
143    print >>sys.stderr, '''
144        Gruyere started...
145            http://%s:%d/
146            http://%s:%d/%s/''' % (
147                server_name, server_port, server_name, server_port,
148                server_unique_id)
149  
150    global stored_data
151    stored_data = _LoadDatabase()
152  
153    while not quit_server:
154      try:
155        http_server.handle_request()
156        _SaveDatabase(stored_data)
157      except KeyboardInterrupt:
158        print >>sys.stderr, '\nReceived KeyboardInterrupt'
159        quit_server = True
160  
161    print >>sys.stderr, '\nClosing'
162    http_server.socket.close()
163    _Exit('quit_server')
164  
165  
166  def _Exit(reason):
167    # use os._exit instead of sys.exit because this can't be trapped
168    print >>sys.stderr, '\nExit: ' + reason
169    os._exit(0)
170  
171  
172  def _SetWorkingDirectory():
173    """Set the working directory to the directory containing this file."""
174    if sys.path[0]:
175      os.chdir(sys.path[0])
176  
177  
178  def _LoadDatabase():
179    """Load the database from stored-data.txt.
180  
181    Returns:
182      The loaded database.
183    """
184  
185    try:
186      f = _Open(INSTALL_PATH, DB_FILE)
187      stored_data = cPickle.load(f)
188      f.close()
189    except (IOError, ValueError):
190      _Log('Couldn\'t load data; expected the first time Gruyere is run')
191      stored_data = None
192  
193    f = _Open(INSTALL_PATH, SECRET_FILE)
194    global cookie_secret
195    cookie_secret = f.readline()
196    f.close()
197  
198    return stored_data
199  
200  
201  def _SaveDatabase(save_database):
202    """Save the database to stored-data.txt.
203  
204    Args:
205      save_database: the database to save.
206    """
207  
208    try:
209      f = _Open(INSTALL_PATH, DB_FILE, 'w')
210      cPickle.dump(save_database, f)
211      f.close()
212    except IOError:
213      _Log('Couldn\'t save data')
214  
215  
216  def _Open(location, filename, mode='rb'):
217    """Open a file from a specific location.
218  
219    Args:
220      location: The directory containing the file.
221      filename: The name of the file.
222      mode: File mode for open().
223  
224    Returns:
225      A file object.
226    """
227    return open(location + filename, mode)
228  
229  
230  class GruyereRequestHandler(BaseHTTPRequestHandler):
231    """Handle a http request."""
232  
233    # An empty cookie
234    NULL_COOKIE = {COOKIE_UID: None, COOKIE_ADMIN: False, COOKIE_AUTHOR: False}
235  
236    # Urls that can only be accessed by administrators.
237    _PROTECTED_URLS = [
238        '/quit',
239        '/reset'
240    ]
241  
242    def _GetDatabase(self):
243      """Gets the database."""
244      global stored_data
245      if not stored_data:
246        stored_data = data.DefaultData()
247      return stored_data
248  
249    def _ResetDatabase(self):
250      """Reset the database."""
251      # global stored_data
252      stored_data = data.DefaultData()
253  
254    def _DoLogin(self, cookie, specials, params):
255      """Handles the /login url: validates the user and creates a cookie.
256  
257      Args:
258        cookie: The cookie for this request.
259        specials: Other special values for this request.
260        params: Cgi parameters.
261      """
262      database = self._GetDatabase()
263      message = ''
264      if 'uid' in params and 'pw' in params:
265        uid = self._GetParameter(params, 'uid')
266        if uid in database:
267          if database[uid]['pw'] == self._GetParameter(params, 'pw'):
268            (cookie, new_cookie_text) = (
269                self._CreateCookie('GRUYERE', uid))
270            self._DoHome(cookie, specials, params, new_cookie_text)
271            return
272        message = 'Invalid user name or password.'
273      # not logged in
274      specials['_message'] = message
275      self._SendTemplateResponse('/login.gtl', specials, params)
276  
277    def _DoLogout(self, cookie, specials, params):
278      """Handles the /logout url: clears the cookie.
279  
280      Args:
281        cookie: The cookie for this request.
282        specials: Other special values for this request.
283        params: Cgi parameters.
284      """
285      (cookie, new_cookie_text) = (
286          self._CreateCookie('GRUYERE', None))
287      self._DoHome(cookie, specials, params, new_cookie_text)
288  
289    def _Do(self, cookie, specials, params):
290      """Handles the home page (http://localhost/).
291  
292      Args:
293        cookie: The cookie for this request.
294        specials: Other special values for this request.
295        params: Cgi parameters.
296      """
297      self._DoHome(cookie, specials, params)
298  
299    def _DoHome(self, cookie, specials, params, new_cookie_text=None):
300      """Renders the home page.
301  
302      Args:
303        cookie: The cookie for this request.
304        specials: Other special values for this request.
305        params: Cgi parameters.
306        new_cookie_text: New cookie.
307      """
308      database = self._GetDatabase()
309      specials[SPECIAL_COOKIE] = cookie
310      if cookie and cookie.get(COOKIE_UID):
311        specials[SPECIAL_PROFILE] = database.get(cookie[COOKIE_UID])
312      else:
313        specials.pop(SPECIAL_PROFILE, None)
314      self._SendTemplateResponse(
315          '/home.gtl', specials, params, new_cookie_text)
316  
317    def _DoBadUrl(self, path, cookie, specials, params):
318      """Handles invalid urls: displays an appropriate error message.
319  
320      Args:
321        path: The invalid url.
322        cookie: The cookie for this request.
323        specials: Other special values for this request.
324        params: Cgi parameters.
325      """
326      self._SendError('Invalid request: %s' % (path,), cookie, specials, params)
327  
328    def _DoQuitserver(self, cookie, specials, params):
329      """Handles the /quitserver url for administrators to quit the server.
330  
331      Args:
332        cookie: The cookie for this request. (unused)
333        specials: Other special values for this request. (unused)
334        params: Cgi parameters. (unused)
335      """
336      global quit_server
337      quit_server = True
338      self._SendTextResponse('Server quit.', None)
339  
340    def _AddParameter(self, name, params, data_dict, default=None):
341      """Transfers a value (with a default) from the parameters to the data."""
342      if params.get(name):
343        data_dict[name] = params[name][0]
344      elif default is not None:
345        data_dict[name] = default
346  
347    def _GetParameter(self, params, name, default=None):
348      """Gets a parameter value with a default."""
349      if params.get(name):
350        return params[name][0]
351      return default
352  
353    def _GetSnippets(self, cookie, specials, create=False):
354      """Returns all of the user's snippets."""
355      database = self._GetDatabase()
356      try:
357        profile = database[cookie[COOKIE_UID]]
358        if create and 'snippets' not in profile:
359          profile['snippets'] = []
360        snippets = profile['snippets']
361      except (KeyError, TypeError):
362        _Log('Error getting snippets')
363        return None
364      return snippets
365  
366    def _DoNewsnippet2(self, cookie, specials, params):
367      """Handles the /newsnippet2 url: actually add the snippet.
368  
369      Args:
370        cookie: The cookie for this request.
371        specials: Other special values for this request.
372        params: Cgi parameters.
373      """
374      snippet = self._GetParameter(params, 'snippet')
375      if not snippet:
376        self._SendError('No snippet!', cookie, specials, params)
377      else:
378        snippets = self._GetSnippets(cookie, specials, True)
379        if snippets is not None:
380          snippets.insert(0, snippet)
381      self._SendRedirect('/snippets.gtl', specials[SPECIAL_UNIQUE_ID])
382  
383    def _DoDeletesnippet(self, cookie, specials, params):
384      """Handles the /deletesnippet url: delete the indexed snippet.
385  
386      Args:
387        cookie: The cookie for this request.
388        specials: Other special values for this request.
389        params: Cgi parameters.
390      """
391      index = self._GetParameter(params, 'index')
392      snippets = self._GetSnippets(cookie, specials)
393      try:
394        del snippets[int(index)]
395      except (IndexError, TypeError, ValueError):
396        self._SendError(
397            'Invalid index (%s)' % (index,),
398            cookie, specials, params)
399        return
400      self._SendRedirect('/snippets.gtl', specials[SPECIAL_UNIQUE_ID])
401  
402    def _DoSaveprofile(self, cookie, specials, params):
403      """Saves the user's profile.
404  
405      Args:
406        cookie: The cookie for this request.
407        specials: Other special values for this request.
408        params: Cgi parameters.
409  
410      If the 'action' cgi parameter is 'new', then this is creating a new user
411      and it's an error if the user already exists. If action is 'update', then
412      this is editing an existing user's profile and it's an error if the user
413      does not exist.
414      """
415  
416      # build new profile
417      profile_data = {}
418      uid = self._GetParameter(params, 'uid', cookie[COOKIE_UID])
419      newpw = self._GetParameter(params, 'pw')
420      self._AddParameter('name', params, profile_data, uid)
421      self._AddParameter('pw', params, profile_data)
422      self._AddParameter('is_author', params, profile_data)
423      self._AddParameter('is_admin', params, profile_data)
424      self._AddParameter('private_snippet', params, profile_data)
425      self._AddParameter('icon', params, profile_data)
426      self._AddParameter('web_site', params, profile_data)
427      self._AddParameter('color', params, profile_data)
428  
429      # Each case below has to set either error or redirect
430      database = self._GetDatabase()
431      message = None
432      new_cookie_text = None
433      action = self._GetParameter(params, 'action')
434      if action == 'new':
435        if uid in database:
436          message = 'User already exists.'
437        else:
438          profile_data['pw'] = newpw
439          database[uid] = profile_data
440          (cookie, new_cookie_text) = self._CreateCookie('GRUYERE', uid)
441          message = 'Account created.'  # error message can also indicates success
442      elif action == 'update':
443        if uid not in database:
444          message = 'User does not exist.'
445        elif (newpw and database[uid]['pw'] != self._GetParameter(params, 'oldpw')
446              and not cookie.get(COOKIE_ADMIN)):
447          # must be admin or supply old pw to change password
448          message = 'Incorrect password.'
449        else:
450          if newpw:
451            profile_data['pw'] = newpw
452          database[uid].update(profile_data)
453          redirect = '/'
454      else:
455        message = 'Invalid request'
456      _Log('SetProfile(%s, %s): %s' %(str(uid), str(action), str(message)))
457      if message:
458        self._SendError(message, cookie, specials, params, new_cookie_text)
459      else:
460        self._SendRedirect(redirect, specials[SPECIAL_UNIQUE_ID])
461  
462    def _SendHtmlResponse(self, html, new_cookie_text=None):
463      """Sends the provided html response with appropriate headers.
464  
465      Args:
466        html: The response.
467        new_cookie_text: New cookie to set.
468      """
469      self.send_response(200)
470      self.send_header('Content-type', 'text/html')
471      self.send_header('Pragma', 'no-cache')
472      if new_cookie_text:
473        self.send_header('Set-Cookie', new_cookie_text)
474      self.send_header('X-XSS-Protection', '0')
475      self.end_headers()
476      self.wfile.write(html)
477  
478    def _SendTextResponse(self, text, new_cookie_text=None):
479      """Sends a verbatim text response."""
480  
481      self._SendHtmlResponse('<pre>' + cgi.escape(text) + '</pre>',
482                             new_cookie_text)
483  
484    def _SendTemplateResponse(self, filename, specials, params,
485                              new_cookie_text=None):
486      """Sends a response using a gtl template.
487  
488      Args:
489        filename: The template file.
490        specials: Other special values for this request.
491        params: Cgi parameters.
492        new_cookie_text: New cookie to set.
493      """
494      f = None
495      try:
496        f = _Open(RESOURCE_PATH, filename)
497        template = f.read()
498      finally:
499        if f: f.close()
500      self._SendHtmlResponse(
501          gtl.ExpandTemplate(template, specials, params),
502          new_cookie_text)
503  
504    def _SendFileResponse(self, filename, cookie, specials, params):
505      """Sends the contents of a file.
506  
507      Args:
508        filename: The file to send.
509        cookie: The cookie for this request.
510        specials: Other special values for this request.
511        params: Cgi parameters.
512      """
513      content_type = None
514      if filename.endswith('.gtl'):
515        self._SendTemplateResponse(filename, specials, params)
516        return
517  
518      name_only = filename[filename.rfind('/'):]
519      extension = name_only[name_only.rfind('.'):]
520      if '.' not in extension:
521        content_type = 'text/plain'
522      elif extension in RESOURCE_CONTENT_TYPES:
523        content_type = RESOURCE_CONTENT_TYPES[extension]
524      else:
525        self._SendError(
526            'Unrecognized file type (%s).' % (filename,),
527            cookie, specials, params)
528        return
529      f = None
530      try:
531        f = _Open(RESOURCE_PATH, filename, 'rb')
532        self.send_response(200)
533        self.send_header('Content-type', content_type)
534        # Always cache static resources
535        self.send_header('Cache-control', 'public, max-age=7200')
536        self.send_header('X-XSS-Protection', '0')
537        self.end_headers()
538        self.wfile.write(f.read())
539      finally:
540        if f: f.close()
541  
542    def _SendError(self, message, cookie, specials, params, new_cookie_text=None):
543      """Sends an error message (using the error.gtl template).
544  
545      Args:
546        message: The error to display.
547        cookie: The cookie for this request. (unused)
548        specials: Other special values for this request.
549        params: Cgi parameters.
550        new_cookie_text: New cookie to set.
551      """
552      specials['_message'] = message
553      self._SendTemplateResponse(
554          '/error.gtl', specials, params, new_cookie_text)
555  
556    def _CreateCookie(self, cookie_name, uid):
557      """Creates a cookie for this user.
558  
559      Args:
560        cookie_name: Cookie to create.
561        uid: The user.
562  
563      Returns:
564        (cookie, new_cookie_text).
565  
566      The cookie contains all the information we need to know about
567      the user for normal operations, including whether or not the user
568      should have access to the authoring pages or the admin pages.
569      The cookie is signed with a hash function.
570      """
571      if uid is None:
572        return (self.NULL_COOKIE, cookie_name + '=; path=/')
573      database = self._GetDatabase()
574      profile = database[uid]
575      if profile.get('is_author', False):
576        is_author = 'author'
577      else:
578        is_author = ''
579      if profile.get('is_admin', False):
580        is_admin = 'admin'
581      else:
582        is_admin = ''
583  
584      c = {COOKIE_UID: uid, COOKIE_ADMIN: is_admin, COOKIE_AUTHOR: is_author}
585      c_data = '%s|%s|%s' % (uid, is_admin, is_author)
586  
587      # global cookie_secret; only use positive hash values
588      h_data = str(hash(cookie_secret + c_data) & 0x7FFFFFF)
589      c_text = '%s=%s|%s; path=/' % (cookie_name, h_data, c_data)
590      return (c, c_text)
591  
592    def _GetCookie(self, cookie_name):
593      """Reads, verifies and parses the cookie.
594  
595      Args:
596        cookie_name: The cookie to get.
597  
598      Returns:
599        a dict containing user, is_admin, and is_author if the cookie
600        is present and valid. Otherwise, None.
601      """
602      cookies = self.headers.get('Cookie')
603      if isinstance(cookies, str):
604        for c in cookies.split(';'):
605          matched_cookie = self._MatchCookie(cookie_name, c)
606          if matched_cookie:
607            return self._ParseCookie(matched_cookie)
608      return self.NULL_COOKIE
609  
610    def _MatchCookie(self, cookie_name, cookie):
611      """Matches the cookie.
612  
613      Args:
614        cookie_name: The name of the cookie.
615        cookie: The full cookie (name=value).
616  
617      Returns:
618        The cookie if it matches or None if it doesn't match.
619      """
620      try:
621        (cn, cd) = cookie.strip().split('=', 1)
622        if cn != cookie_name:
623          return None
624      except (IndexError, ValueError):
625        return None
626      return cd
627  
628    def _ParseCookie(self, cookie):
629      """Parses the cookie and returns NULL_COOKIE if it's invalid.
630  
631      Args:
632        cookie: The text of the cookie.
633  
634      Returns:
635        A map containing the values in the cookie.
636      """
637      try:
638        (hashed, cookie_data) = cookie.split('|', 1)
639        # global cookie_secret
640        if hashed != str(hash(cookie_secret + cookie_data) & 0x7FFFFFF):
641          return self.NULL_COOKIE
642        values = cookie_data.split('|')
643        return {
644            COOKIE_UID: values[0],
645            COOKIE_ADMIN: values[1] == 'admin',
646            COOKIE_AUTHOR: values[2] == 'author',
647        }
648      except (IndexError, ValueError):
649        return self.NULL_COOKIE
650  
651    def _DoReset(self, cookie, specials, params):  # debug only; resets this db
652      """Handles the /reset url for administrators to reset the database.
653  
654      Args:
655        cookie: The cookie for this request. (unused)
656        specials: Other special values for this request. (unused)
657        params: Cgi parameters. (unused)
658      """
659      self._ResetDatabase()
660      self._SendTextResponse('Server reset to default values...', None)
661  
662    def _DoUpload2(self, cookie, specials, params):
663      """Handles the /upload2 url: finish the upload and save the file.
664  
665      Args:
666        cookie: The cookie for this request.
667        specials: Other special values for this request.
668        params: Cgi parameters. (unused)
669      """
670      (filename, file_data) = self._ExtractFileFromRequest()
671      directory = self._MakeUserDirectory(cookie[COOKIE_UID])
672  
673      message = None
674      url = None
675      try:
676        f = _Open(directory, filename, 'wb')
677        f.write(file_data)
678        f.close()
679        (host, port) = http_server.server_address
680        url = 'http://%s:%d/%s/%s/%s' % (
681            host, port, specials[SPECIAL_UNIQUE_ID], cookie[COOKIE_UID], filename)
682      except IOError, ex:
683        message = 'Couldn\'t write file %s: %s' % (filename, ex.message)
684        _Log(message)
685  
686      specials['_message'] = message
687      self._SendTemplateResponse(
688          '/upload2.gtl', specials,
689          {'url': url})
690  
691    def _ExtractFileFromRequest(self):
692      """Extracts the file from an upload request.
693  
694      Returns:
695        (filename, file_data)
696      """
697      form = cgi.FieldStorage(
698          fp=self.rfile,
699          headers=self.headers,
700          environ={'REQUEST_METHOD': 'POST',
701                   'CONTENT_TYPE': self.headers.getheader('content-type')})
702  
703      upload_file = form['upload_file']
704      file_data = upload_file.file.read()
705      return (upload_file.filename, file_data)
706  
707    def _MakeUserDirectory(self, uid):
708      """Creates a separate directory for each user to avoid upload conflicts.
709  
710      Args:
711        uid: The user to create a directory for.
712  
713      Returns:
714        The new directory path (/uid/).
715      """
716  
717      directory = RESOURCE_PATH + os.sep + str(uid) + os.sep
718      try:
719        print 'mkdir: ', directory
720        os.mkdir(directory)
721        # throws an exception if directory already exists,
722        # however exception type varies by platform
723      except Exception:
724        pass  # just ignore it if it already exists
725      return directory
726  
727    def _SendRedirect(self, url, unique_id):
728      """Sends a 302 redirect.
729  
730      Automatically adds the unique_id.
731  
732      Args:
733        url: The location to redirect to which must start with '/'.
734        unique_id: The unique id to include in the url.
735      """
736      if not url:
737        url = '/'
738      url = '/' + unique_id + url
739      self.send_response(302)
740      self.send_header('Location', url)
741      self.send_header('Pragma', 'no-cache')
742      self.send_header('Content-type', 'text/html')
743      self.send_header('X-XSS-Protection', '0')
744      self.end_headers()
745      self.wfile.write(
746          '''<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML//EN'>
747          <html><body>
748          <title>302 Redirect</title>
749          Redirected <a href="%s">here</a>
750          </body></html>'''
751          % (url,))
752  
753    def _GetHandlerFunction(self, path):
754      try:
755        return getattr(GruyereRequestHandler, '_Do' + path[1:].capitalize())
756      except AttributeError:
757        return None
758  
759    def do_POST(self):  # part of BaseHTTPRequestHandler interface
760      self.DoGetOrPost()
761  
762    def do_GET(self):  # part of BaseHTTPRequestHandler interface
763      self.DoGetOrPost()
764  
765    def DoGetOrPost(self):
766      """Validate an http get or post request and call HandleRequest."""
767  
768      url = urlparse(self.path)
769      path = url[2]
770      query = url[4]
771  
772      # Normally, Gruyere only accepts connections to/from localhost. If you
773      # would like to allow access from other ip addresses, add the addresses
774      # of the other machines to allowed_ips and change insecure_mode to True
775      # above. This makes the application more vulnerable to a real attack so
776      # you should only add ips of machines you completely control and make
777      # sure that you are not using them to access any other web pages while
778      # you are using Gruyere.
779  
780      allowed_ips = ['127.0.0.1']
781  
782      # WARNING! DO NOT CHANGE THE FOLLOWING SECTION OF CODE!
783  
784      # This application is very exploitable. See main for details. What we're
785      # doing here is (2) and (3) on the previous list:
786      #   (2) If a request is received from any IP other than localhost, quit.
787      # An external attacker could still mount an attack on this IP by putting
788      # an attack on an external web page, e.g., a web page that redirects to
789      # a vulnerable url on 127.0.0.1 (which is why we use a random number).
790      #   (3) Inject a random identifier as the first part of the path and
791      # quit if a request is received without this identifier (except for an
792      # empty path which redirects and /favicon.ico).
793  
794      request_ip = self.client_address[0]                      # DO NOT CHANGE
795      if request_ip not in allowed_ips:                        # DO NOT CHANGE
796        print >>sys.stderr, (                                  # DO NOT CHANGE
797            'DANGER! Request from bad ip: ' + request_ip)      # DO NOT CHANGE
798        _Exit('bad_ip')                                        # DO NOT CHANGE
799  
800      if (server_unique_id not in path                         # DO NOT CHANGE
801          and path != '/favicon.ico'):                         # DO NOT CHANGE
802        if path == '' or path == '/':                          # DO NOT CHANGE
803          self._SendRedirect('/', server_unique_id)            # DO NOT CHANGE
804          return                                               # DO NOT CHANGE
805        else:                                                  # DO NOT CHANGE
806          print >>sys.stderr, (                                # DO NOT CHANGE
807              'DANGER! Request without unique id: ' + path)    # DO NOT CHANGE
808          _Exit('bad_id')                                      # DO NOT CHANGE
809  
810      path = path.replace('/' + server_unique_id, '', 1)       # DO NOT CHANGE
811  
812      # END WARNING!
813  
814      self.HandleRequest(path, query, server_unique_id)
815  
816    def HandleRequest(self, path, query, unique_id):
817      """Handles an http request.
818  
819      Args:
820        path: The path part of the url, with leading slash.
821        query: The query part of the url, without leading question mark.
822        unique_id: The unique id from the url.
823      """
824  
825      path = urllib.unquote(path)
826  
827      if not path:
828        self._SendRedirect('/', server_unique_id)
829        return
830      params = cgi.parse_qs(query)  # url.query
831      specials = {}
832      cookie = self._GetCookie('GRUYERE')
833      database = self._GetDatabase()
834      specials[SPECIAL_COOKIE] = cookie
835      specials[SPECIAL_DB] = database
836      specials[SPECIAL_PROFILE] = database.get(cookie.get(COOKIE_UID))
837      specials[SPECIAL_PARAMS] = params
838      specials[SPECIAL_UNIQUE_ID] = unique_id
839  
840      if path in self._PROTECTED_URLS and not cookie[COOKIE_ADMIN]:
841        self._SendError('Invalid request', cookie, specials, params)
842        return
843  
844      try:
845        handler = self._GetHandlerFunction(path)
846        if callable(handler):
847          (handler)(self, cookie, specials, params)
848        else:
849          try:
850            self._SendFileResponse(path, cookie, specials, params)
851          except IOError:
852            self._DoBadUrl(path, cookie, specials, params)
853      except KeyboardInterrupt:
854        _Exit('KeyboardInterrupt')
855  
856  
857  def _Log(message):
858    print >>sys.stderr, message
859  
860  
861  if __name__ == '__main__':
862    main()