Advanced CGI Scripts ==================== With cookies we can write more clever cgi scripts. Cookies ------- Cookies enable personalized browsing. With cookies we can store information about a client. For example, about previous selections made, in passing data from one script to another. We can store personal information such as identification and passwords; or information from previous visits to the site. Potential applications include customizing displayed content, storing encrypted password for fast access, personalized pricing ... Cookies are data * stored by web server on client computer, * managed by the browser. The session below illustrates the use of the ``cookies`` module. :: >>> from http import cookies >>> c = cookies.SimpleCookie() >>> c['L'] = 35 >>> c['data'] = 'Fri 8 Apr 2016' >>> print(c) Set-Cookie: L=35 Set-Cookie: data="Fri 8 Apr 2016" Cookies are objects like dictionaries. Two keys are reserved: ``expires`` and ``path``. To compute with values stored in cookies, observe the following: * The value of cookie is the attribute ``value``. * The type of the ``value`` is a string. To update the number of visits stored in the cookie ``cnt``, consider :: >>> from http import cookies >>> cnt = cookies.SimpleCookie() >>> cnt['visits'] = 0 >>> cnt['visits'] >>> cnt['visits'].value '0' >>> cnt['visits'] = str(1 + int(cnt['visits'].value)) >>> cnt['visits'].value '1' A simple cookie stores all data as strings. We can initialize a cookie with keys and values: :: >>> from http.cookies import SimpleCookie >>> c = SimpleCookie('x=1; y=2') >>> c['x'].value '1' >>> c['y'].value '2' The environment variable ``HTTP_COOKIE`` is set by the web server. When we rerun a web page the cookies are retrieved as follows: :: >>> from os import environ >>> environ['HTTP_COOKIE'] = 'z = 3' >>> d = SimpleCookie(environ['HTTP_COOKIE']) >>> d['z'].value '3' To illustrate an application of cookies, let us count the number of visits made to a site. We will write a script to 1. retrieve cookie, initialize counter to zero, or 2. increment the value of counter by one, and then 3. display the counter value on the page. The name of the script is ``cookie_counter.py``. The browser settings must accept cookies. Note that each browser has its own cookies. A screenshot of the running of the script is shown in :numref:`figcookiecounter`. .. _figcookiecounter: .. figure:: ./figcookiecounter.png :align: center Running a script to count the number of visits. The listing of the code for ``cookie_counter.py`` is below. We start with the function to increment the counter. :: #!/usr/bin/python import os from http.cookies import SimpleCookie def increment(): """ Retrieves cookie, either initializes counter, or increments the counter by one. """ if 'HTTP_COOKIE' in os.environ: cnt = SimpleCookie(os.environ['HTTP_COOKIE']) else: cnt = SimpleCookie() if 'visits' not in cnt: cnt['visits'] = 0 else: cnt['visits'] = str(1 + int(cnt['visits'].value)) return cnt The main function in ``cookie_counter.py`` is then :: def main(): """ Retrieves a cookie and writes the value of counter to the page, after incrementing the counter. """ counter = increment() print(counter) print("Content-Type: text/plain\n") print("counter: %s" % counter['visits'].value) main() Password Encryption ------------------- We can use cookies for login names and passwords. For security purposes, we store only the encrypted passwords. A secure hash algorithm is often called a *trapdoor* function, as it is computationally infeasible to compute its inverse. As a hash function, the collision rate is low, which means that there is a very low chance that two different messages will generate the same key. The server encrypts the password before sending it to the client. The authentication happens by comparing encrypted passwords. A session with the ``hashlib`` module is illustrated below. :: >>> from hashlib import sha1 >>> m = 'this is me' >>> h = sha1(m.encode()) >>> h >>> h.hexdigest() '99cf08cddcc3ae19fae7ec8f53f5179786b11406' As an example, we consider the application of cookies to remember data from login forms. Accessing ``cookie_login.py`` for the first time: 1. The user submits login name and password. 2. The submitted data is processed by ``cookie_userpass.py``. 3. The cookie stores the login name and encrypted password. Connecting to ``cookie_login.py`` a second time: 1. The cookie is retrieved. 2. The login name is displayed if not empty. 3. The user must type no password if in cookie. The main function in the script ``cookie_login.py`` is listed below. :: #!/usr/bin/python import cgitb cgitb.enable() from http.cookies import SimpleCookie from os import environ def main(): """ Form to process login. """ clp = get_cookie() print(clp) print("Content-type: text/html\n") print("\n") ask_name(clp) print("\n") The main function calls two functions: ``get_cookie()`` and ``ask_name()``. The function ``get_cookie()`` is defined below. :: def get_cookie(): """ Retrieves cookie, and initializes it. """ if 'HTTP_COOKIE' in environ: sim = SimpleCookie(environ['HTTP_COOKIE']) else: sim = SimpleCookie() if 'login' not in sim: sim['login'] = '' sim['passw'] = '' return sim The function ``ask_name()`` is defined next. :: def ask_name(cki): """ Form to enter user name, using cookie cki to show user name. """ print("""
""") vlog = cki['login'].value vpas = cki['passw'].value if vlog == '': print("""

Login Name: """) else: print("""

Login Name: """ % vlog) if vpas == '': print("""

Password: """) print("""

""") The main function in the script ``cookie_userpass.py`` is listed below. :: #!/usr/bin/python import cgi def main(): """ Form to process login. """ form = cgi.FieldStorage() cki = get_cookie(form) print(cki) print("Content-type: text/html\n") print("\n") error = process_name(form) if not error: process_pass(cki) print("\n") The function ``get\_cookie()`` gets the data from the form. :: def get_cookie(form): """ Retrieves cookie and uses form to update. """ if 'HTTP_COOKIE' in environ: sim = SimpleCookie(environ['HTTP_COOKIE']) else: sim = SimpleCookie() if 'login' in sim: if 'login' in form: sim['login'] = form['login'].value if 'passw' in sim: if 'passw' in form: vpw = form['passw'].value data = sha1(vpw).hexdigest() sim['passw'] = data return sim The processing the name is done by the function below. :: def process_name(form): """ Processes name of login form. Returns True if error, else False. """ error = False try: name = form['login'].value except KeyError: print("please enter your name") error = True if not error: print('welcome ' + name + '\n') return error Processing password is done by ``process_pass()``. :: def process_pass(cki): """ Processes password of login form, stored in the cookie. """ print("

your password is ") print(cki['passw'].value) In a real application, instead of printing the password, one would compare the retrieved password against the password on file. Authentication via Email ------------------------ We can restrict access to our web application via a third party email authentication. For example, we may grant access only to those with a valid UIC email account. The Simple Mail Transfer Protocol (SMTP) is an internet standard for email transmission. In Python we can send email using ``smtplib``. For example, to connect to the {\tt gmail} email server, consider the session below: :: >>> from smtplib import SMTP >>> s = SMTP('smtp.gmail.com', 587) >>> s.starttls() (220, b'2.0.0 Ready to start TLS') The Transport Layer Security (TLS) protocol provides data encryption for socket based communication. Consider the code for the script ``emailauth.py`` below. A user with a valid UIC netid and corresponding email address should be able to pass the authentication. :: from smtplib import SMTP from getpass import getpass try: SERVER = SMTP(host='mail.uic.edu', port=25) RESULT = SERVER.starttls() print(RESULT) except: print('Failed to connect to the server.') SUCCESS = False USER = input('Type your UIC netid : ') USER = USER + '@uic.edu' for _ in range(3): # allow for three attempts PASW = getpass('Type your UIC password : ') try: RESULT = SERVER.login(USER, PASW) print(RESULT) SUCCESS = True break except: print('Login failed, please try again.') Another application of ``smtplib`` is to send email. First we connect to the server and then go through the authentication process, as defined in the function below. :: HOSTNAME = 'mail.uic.edu' PORTNUMBER = 25 from smtplib import SMTP from email.mime.text import MIMEText from getpass import getpass def authenticate(hostname, portnumber): """ Returns the server and the email address of the user, after a successful authentication, otherwise None is returned. """ try: server = SMTP(host=hostname, port=portnumber) result = server.starttls() except: print('Failed to connect to the server.') success = False user = input('Type your UIC netid : ') user = user + '@uic.edu' for _ in range(3): # allow for three attempts pasw = getpass('Type your UIC password : ') try: result = server.login(user, pasw) success = True break except: print('Login failed, please try again.') if success: print('You are authenticated via UIC email.') return server, user else: print('Authentication via UIC email failed.') return None The function ``main()`` in the script is below: :: def main(): """ First we authenticate and then we send the email. """ srvusr = authenticate(HOSTNAME, PORTNUMBER) if srvusr != None: server, user = srvusr ret = sendanemail(server, user) if ret == {}: print('The message was accepted for delivery.') else: print('The message was rejected for delivery.') print(ret) Once connected and authenticated, we can compose a message. MIME (Multipurpose Internet Mail Extensions) is an internet standard to extend the format of email. In Python, ``email.mime.text`` represents ``text/*`` type MIME documents. :: >>> from email.mime.text import MIMEText >>> msg = MIMEText('This is a test.') >>> msg['Subject'] = 'hello' >>> msg['To'] = 'someuser@gmail.com' >>> print(msg.as_string()) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: hello To: someuser@gmail.com This is a test. >>> The sending of an email is defined by the function below. :: def sendanemail(server, user): """ Prompts the user for a destination, subject and a message body. """ dest = input('Give the destination email address : ') subj = input('Give the subject : ') body = input('Give the message : ') msg = MIMEText(body) msg['Subject'] = subj msg['To'] = dest result = server.sendmail(user, dest, msg.as_string()) return result An application of the sending of an email via a Python script is the automatic registration and activation of an account. The registration/activation is a two step process: 1. The user connects to our web site and submits a form with the email address of the user (which will be the name of the account) and a password, which is encrypted. The ``register.py`` script 1. stores the account information, 2. generates a random code for one time use, 3. sends the email to the user with link to the activate script that takes the random code as input. The email sent contains an URL of the following form: :: /cgi-bin/activate.py?login=%s&code=%s This login and the code are the email address and the code. 2. The activation process is executed by {\tt activate.py}. This script checks whether the two parameters in the activate script match what is stored in the database. If okay, if the access code gives a match for the login name, then the user can login via the login form. In this process, the server only sends email, does not read it. Exercises --------- 1. Extend ``cookie_counter.py`` so that a different HTML page is displayed based on whether the counter is zero or not. 2. Extend ``cookie_userpass.py`` for it to do proper password authentication: compare the given password with the one that was earlier entered and stored on file. 3. Consider an mysql table customers which contains as its fields the identification number, the name, and the email address of each customer. Write a script to email every customer a greeting. 4. Write code for the ``register.py`` described earlier. 5. Write code for the ``activate.py`` described earlier.