Skip to content

37. Practical Exercise: Version 17

Image

This new version introduces the following changes:

  • it will be ported to an Apache/Windows server;
  • To perform this port, version 17 contains all the dependencies it needs in its [impots/http-servers/12] folder. Note that previous versions retrieved their dependencies from various folders throughout the entire [python-flask-2020] project;

37.1. Relocation of application dependencies

Image

Note that application dependency management is handled in the [syspath] script. In the previous version, this script was as follows:

def configure(config: dict) -> dict:
    import os

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        #  main script folder
        script_dir,
        #  configs [database, layers, parameters, controllers, views]
        f"{script_dir}/../configs",
        #  controllers
        f"{script_dir}/../controllers",
        #  answers HTTP
        f"{script_dir}/../responses",
        #  view models
        f"{script_dir}/../models_for_views",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  we return the configuration
    return {
        "root_dir": root_dir,
        "script_dir": script_dir
    }

We need to relocate all dependencies whose absolute path depends on the [root_dir] variable in line 8, i.e., lines 13–26.

The [syspath] script for the new version will be as follows:

def configure(config: dict) -> dict:
    import os

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  dependencies
    absolute_dependencies = [
        #  application entities
        f"{script_dir}/../entities",
        #  layer [dao]
        f"{script_dir}/../layers/dao",
        #  business] layer
        f"{script_dir}/../layers/métier",
        #  utilities
        f"{script_dir}/../utilities",
        #  main script folder
        script_dir,
        #  configs [database, layers, parameters, controllers, views]
        f"{script_dir}/../configs",
        #  controllers
        f"{script_dir}/../controllers",
        #  answers HTTP
        f"{script_dir}/../responses",
        #  view models
        f"{script_dir}/../models_for_views",
    ]

    #  set the syspath
    import sys
    #  we add the project's absolute dependencies
    for directory in absolute_dependencies:
        #  we check the existence of the file
        existe = os.path.exists(directory) and os.path.isdir(directory)
        if not existe:
            #  the developer is notified
            raise BaseException(f"[set_syspath] le dossier du Python Path [{directory}] n'existe pas")
        else:
            #  insert the folder at the beginning of the syspath
            sys.path.insert(0, directory)

    #  we return the configuration
    return {
        "script_dir": script_dir,
    }
  • lines 8–27: all dependencies are now relative to the [script_dir] variable on line 5;
  • lines 42–45: the [root_dir] variable has been removed from the syspath configuration;
  • line 10: the application entities are in the [entities] folder [1];
  • line 12: the [dao] layer is in the [layers/dao] folder [2];
  • line 14: the [business] layer is in the [layers/business] folder [2];
  • line 16: the [Logger, SendMail] utilities are in the [utilities] folder [3];
  • lines 29–40: the application’s Python Path is calculated without importing the [myutils] module;

Image

37.2. Tests

At this point, version 17 should work. Verify it.

37.3. Porting a Python/Flask application to an Apache/Windows server

37.3.1. Sources

To port a Flask application to Apache/Windows, I had to search the Internet. Here is the link that helped me get started: [https://medium.com/@madumalt/flask-app-deployment-in-windows-apache-server-mod-wsgi-82e1cfeeb2ed];

I used the information from this link except for the Apache server configuration. For that, I used a sample Apache server configuration from Laragon.

37.3.2. Installing the Python mod_wsgi module

The Python/Flask application we developed used the WSGI (Web Server Gateway Interface) server [werkzeug] included with Flask. This server is described |here|. The link [https://www.fullstackpython.com/wsgi-servers.html] describes how WSGI servers work. There are various |WSGI servers|. One of these is the Apache server running in WSGI mode. This is the solution adopted here because Laragon, which we installed, comes with an Apache server.

In order for the Apache server to host a Python application, we need to install the Python module [mod_wsgi]. Installing this module is tricky because it involves a C++ compilation. To complete the installation successfully, you need a Microsoft C++ compiler. A simple solution is to install the latest version of Visual Studio Community [https://visualstudio.microsoft.com/fr/vs/community/].

If you don’t need Visual Studio for anything other than [mod_wsgi], you can limit the installation to the C++ environment:

Image

Once the C++ compiler is installed, the [mod_wsgi] module is installed in a PyCharm terminal:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>SET MOD_WSGI_APACHE_ROOTDIR=C:\MyPrograms\laragon\bin\apache\httpd-2.4.35-win64-VC15
 
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>pip install mod_wsgi
Collecting mod_wsgi
  Using cached mod_wsgi-4.7.1.tar.gz (498 kB)
Using legacy setup.py install for mod-wsgi, since package 'wheel' is not installed.
Installing collected packages: mod-wsgi
    Running setup.py install for mod-wsgi ... done
Successfully installed mod-wsgi-4.7.1
  • Line 1: We set the value of the [MOD_WSGI_APACHE_ROOTDIR] environment variable. This value is the location of the Apache server in the file system. Here, this location is [<laragon>\bin\apache\httpd-2.4.35-win64-VC15], where <laragon> is the Laragon installation folder. You can obtain this location in various ways. Here is one obtained using one of Laragon’s options:

Image

In [1-3], the [httpd.conf] file is the main configuration file for the Apache server. The file in question is then opened in a text editor (Notepad++ below):

Image

In [2], the Apache installation folder is the part preceding the string [conf\httpd.conf].

Let’s return to the installation of the [mod_wsgi] module:

  • Lines 3–9: installation of the [mod_wsgi] module;

37.3.3. Configuring the Laragon Apache Server

We will configure the Laragon Apache server. We start with its main configuration file [httpd.conf]:

Image

We go to the end of the [httpd.conf] file:



IncludeOptional "C:/MyPrograms/laragon/etc/apache2/alias/*.conf"
IncludeOptional "C:/MyPrograms/laragon/etc/apache2/sites-enabled/*.conf"
Include "C:/MyPrograms/laragon/etc/apache2/httpd-ssl.conf"
Include "C:/MyPrograms/laragon/etc/apache2/mod_php.conf"
 
# python mod_wsgi
LoadModule wsgi_module "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages/mod_wsgi/server/mod_wsgi.cp38-win_amd64.pyd"
  • Line 8 has been added to the existing [httpd.conf] file at the end of the file. It tells the Apache server where to find a component of the [mod_wsgi] module that we just installed;

An easy way to get the path for line 8 is to run the following command in a PyCharm terminal:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>mod_wsgi-express module-config
LoadModule wsgi_module "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages/mod_wsgi/server/mod_wsgi.cp38-win_amd64.pyd"
WSGIPythonHome "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv"

Some documentation states that lines 2 and 3 should be added to the end of the [httpd.conf] file. In my case, line 3 above caused an error (missing [encodings] module). Therefore, it was not included in the [httpd.conf] file. Only line 2 was added. The meanings of the various [mod_wsgi] module parameters that can be used in Apache configuration files are described |here|.

Next, we will enable the HTTPS protocol on the Apache server:

Image

  • In [1-4], we enable the HTTPS protocol on the Apache server;

From now on, we will be able to use [https://serveur/chemin] URLs;

To configure Apache to serve a Flask application, the link referenced |above|uses virtual hosts. Laragon also offers virtual host management:

Image

  • In [1-3], we ask Laragon to automatically create virtual hosts;

The next step is to create a web project with Laragon:

 
  • In [1-3], create an empty PHP project;
  • In [4-8], Laragon has created a virtual site named [auto.projet-test.test] configured by the file [auto.projet-test.test.conf] [8] in the [sites-enabled] folder [7]. This folder is located at [<laragon>\etc\apache2\sites-enabled], where [laragon] is Laragon’s installation folder;

Although this is not part of what we are doing right now, you might be curious to check out the [test-project] website we just created:

Image

  • In [1-5], an empty project was created. It is a PHP project located in the [<laragon>/www] folder, where [laragon] is the Laragon installation folder;

Now let’s examine the [auto.projet-test.test.conf] file generated by Laragon in the [<laragon>\etc\apache2\sites-enabled] folder:


define ROOT "C:/MyPrograms/laragon/www/projet-test/"
define SITE "projet-test.test"
 
<VirtualHost *:80> 
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
 
<VirtualHost *:443>
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
 
    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/MyPrograms/laragon/etc/ssl/laragon.key
 
</VirtualHost>
  • line 1: the root directory of the created project in the file system;
  • line 2: the name of the virtual server. URLs for this server will be in the form [http(s)://test-project.test/path];
  • lines 4–12: configuration of the virtual site for port 80 (line 4) and the HTTP protocol;
  • lines 14–27: configuration of the virtual host for port 443 (line 14) and the HTTPS protocol;

Let’s see how a virtual server works. First, let’s start the Apache and PHP servers:

Image

Then, using a browser, we request the URL [http://projet-test.test/]:

Image

  • in [1], the requested URL;
  • in [2], the HTTP protocol was used;
  • in [3], because the [projet-test] project is empty, we get the index of its folder (list of its contents), an empty index;

Now let’s request the secure URL [https://projet-test.test/]:

Image

  • In [1-2], we get the same response as before, but using the HTTPS protocol [1];

Creating the virtual server [test-project.test] created a new entry in the file [<windows>/system32/drivers/etc/hosts]:

Image


# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host
 
# localhost name resolution is handled within DNS itself.
#    127.0.0.1       localhost
#    ::1             localhost
 
127.0.0.1      projet-test.test     #laragon magic!   
  • Line 23: The IP address for the name [test-project.test] is 127.0.0.1, i.e., the address of [localhost] (line 20), the local machine. So when you type the URL [http://projet-test.test/chemin] into a browser, the request is sent to the address 127.0.0.1 on port 80. The Apache server on the local machine (localhost) then responds.

You might wonder why, when you type the request [http://projet-test.test/], the Apache server uses the configuration from the file [<laragon>\etc\apache2\sites-enabled\auto.projet-test.test.conf]:

Image

To understand this, we need to see what the browser sends to the Apache server when making this request. Let’s do this with Postman:

Image

  • In [1-3], we send an HTTPS request [1];
  • in [4], Postman indicates that it did not recognize the security certificate. The HTTPS protocol establishes an encrypted connection between the web client (here, Postman) and the Apache server. This encrypted connection is established through certificates exchanged between the client and the server. It is the server that initiates the process of establishing the encrypted connection by sending a security certificate to the client. For this certificate to be accepted by the client, it must be signed—in other words, purchased from companies authorized to issue security certificates. When we enabled Laragon’s HTTPS protocol, Laragon created the security certificate itself. The certificate is then said to be self-signed. Most web clients issue a warning when they receive a self-signed certificate. This is what Postman does in [4]. Most web clients then offer to disable verification of the security certificate sent by the server. This is what Postman offers in [5];

We click on the link [5] to disable SSL (Secure Sockets Layer) verification. SSL/TLS (Transport Layer Security) is a security protocol that creates a secure communication channel between two machines on the internet. This is the protocol used here by Apache. The response is as follows:

Image

We receive the same page as with a traditional browser. Now let’s look at the client/server dialogue in the Postman console (Ctrl-Alt-C):


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: d153f711-ad99-4e1d-93c3-61c25374d1be
Host: projet-test.test
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
 
HTTP/1.1 200 OK
Date: Fri, 14 Aug 2020 09:19:24 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19 mod_wsgi/4.7.1 Python/3.8
Content-Length: 161
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html;charset=UTF-8
  • Line 6: The HTTP [Host] header specifies the name of the server targeted by the web client. This is the principle behind virtual servers. At a single IP address (here 127.0.0.1), a web server can host multiple websites with different names. The HTTP [Host] header allows the client to specify which server (here, the one at address 127.0.0.1) it is addressing;

So what does Apache do?

When it starts up, Apache reads all configuration files found in the [[<laragon>\etc\apache2\sites-enabled]] directory:

Image

Each configuration file defines a virtual server. For example, in the file [auto.projet-test.test.conf], you will find the following line:


define ROOT "C:/MyPrograms/laragon/www/projet-test/"
define SITE "projet-test.test"

Line 2 defines the [test-project.test] virtual server. The [auto.projet-test.test.conf] file is the configuration for this virtual server. Because it reads all configuration files in the [<laragon>\etc\apache2\sites-enabled] folder at startup, the Apache server knows that a virtual server named [projet-test.test] exists. Thus, when it receives the following HTTPS request from the Postman client:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: d153f711-ad99-4e1d-93c3-61c25374d1be
Host: projet-test.test
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

It recognizes that the request is addressed to the virtual server [test-project.test] (line 6) and that it exists. It then uses the configuration of the virtual server [test-project.test] to respond to the Postman client.

37.4. Creating Your First Apache Virtual Server

Now that we know what virtual servers are used for and how to configure them, we’ll create one. It will be used to run a Python Flask application installed in an [Apache] folder of version 17 currently being deployed on the Apache server:

We have placed the application developed in the section |link|—a date/time web service—in the [http-servers/12/apache/example] folder:

Image

The [date_time_server.py] server is as follows:

#  imports
import os
import sys
import time

#  we need to put the modules folder in the Python Path
#  for porting to apache windows
sys.path.insert(0, "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages")

from flask import Flask, make_response, render_template

#  flask application
script_dir = os.path.dirname(os.path.abspath(__file__))
application = Flask(__name__, template_folder=f"{script_dir}")

#  Home URL
@application.route('/')
def index():
    #  dispatch time to customer
    #  time.localtime: number of milliseconds since 01/01/1970
    #  time.strftime formats time and date
    #  date-time display format
    #  d: 2-digit day
    #  m: 2-digit month
    #  y: 2-digit year
    #  H: hour 0.23
    #  M: minutes
    #  S: seconds

    #  current date / time
    time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
    #  generate the document to be sent to the customer
    page = {"date_heure": time_of_day}
    document = render_template("date_time_server.html", page=page)
    print("document", type(document), document)
    #  HTTP response to customer
    response = make_response(document)
    print("response", type(response), response)
    return response

#  hand only
if __name__ == '__main__':
    application.config.update(ENV="development", DEBUG=True)
    application.run()

The Flask application is referenced by the identifier [application] (lines 14, 43, 44). This name is required. If you reference the Flask application with a different identifier, the application will not work and will display an error message indicating that it cannot find the requested URL. This error message provides no indication of the source of the error. You must therefore be careful about this.

The HTML file referenced on line 34 is as follows:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
  • [date-time-server] will be the virtual server hosting this application. It will be configured via the file [<laragon>\etc\apache2\sites-enabled\date-time-server.conf] (note that this name is arbitrary—Apache reads all files in [sites-enabled] to discover the hosted virtual sites);

We first obtain this file by copying the [auto.projet-test.test.conf] file and then modify it.

Image

The [date-time-server.conf] file will look like this:

#  application python-flask script file
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache/exemple"

#  name of the website configured by this file
#  here it will be called date-time-server
#  the URL will be of type http(s)://date-time-server/path
define SITE "date-time-server"

#  set address IP 127.0.0.1 for site SITE in c:/windows/system32/drivers/etc/hosts

#  URL HTTP
<VirtualHost *:80>
    #  with the alias / the URL will take the form http(s)://date-time-server/path/...
    WSGIScriptAlias / "${ROOT}/date_time_server.py"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

#  URL secured with HTTPS
<VirtualHost *:443>
    #  with the alias / the URL will take the form http(s)://date-time-server/path/...
    WSGIScriptAlias / "${ROOT}/date_time_server.py"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>

    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/myprograms/laragon/etc/ssl/laragon.key

</VirtualHost>
  • Line 7: Give a name to the virtual server configured by the file;
  • line 2: sets the value of the [ROOT] variable used on lines 14 and 27;
  • Lines 14 and 27: Specify the path to the Python script that must be executed when the virtual server receives a request. Here, we specify that requests for the [date-time-server] server are handled by the Python script [date_time_server.py]. This difference from the [auto.projet-test.test.conf] file stems from the fact that the former configured a PHP server, whereas the [date-time-server.conf] file configures a Python server;
  • Lines 14 and 27: The [WSGIScriptAlias /] attribute specifies here that the root of the [date-time-server] server will be [/]. Thus, the application’s URLs will take the form [http(s)://date-time-server/path];
  • Lines 14 and 27: We can assign a different root to the application, for example [WSGIScriptAlias /show]. In this case, the application’s URLs will take the form [http(s)://show/date-time-server/path];

We also need to add a line to the [<windows>/system32/drivers/etc/hosts] file:


# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host
 
# localhost name resolution is handled within DNS itself.
#    127.0.0.1       localhost
#    ::1             localhost
 
127.0.0.1    flask-impots-withmysql        # cours python - appli impôts avec MySQL
127.0.0.1    flask-impots-withpgres        # cours python - appli impôts avec PostgreSQL
127.0.0.1    date-time-server            # cours python - heure et date du moment
127.0.0.1           projet-test.test                #laragon magic!

We add line 25 to assign the IP address [127.0.0.1] to the virtual server [date-time-server].

Let’s check everything. We start the Apache server:

Image

Next, we request the URL [https://date-time-server] using a browser:

  • in [1], the requested URL;
  • in [3], the server’s response;
  • in [2], the browser indicates that the HTTPS connection is not secure because it detected that the certificate sent by the Apache server was self-signed;

Now, in the [date-time-server.conf] file, let’s add an alias to lines 14 and 27:


    WSGIScriptAlias /show-date-time "${ROOT}/date_time_server.py"

The change is not immediately recognized by the Apache server. You must reload it:

Image

We then request the URL [https://date-time-server/show-date-time]. The server’s response is as follows:

Image

37.5. Porting the tax calculation application to Apache / Windows

The [apache] directory [2] is initially created by copying the [main] directory. It is important that they are at the same level so that the paths in the [syspath.py] script copied from [1] to [2] remain valid. To avoid interfering with the working [impots / http-servers/ 12] application, we place the configuration to be executed by the Apache server in [apache];

Image

  • the [config] file in [2] is the same as the [config] file in [1];
  • the [syspath] file in [2] is the same as the [syspath] file in [1];
  • the [main_withmysql] file in [2] is the [main] file from [1] with the following modifications:

The main script [main] received a [mysql / pgres] parameter that told it which DBMS to use. The [main_withmysql] script uses the MySQL DBMS:

#  a mysql or pgres parameter is expected
import os
import sys

#  configure the application with MySQL
import config
config = config.configure({'sgbd': "mysql"})

#  dependencies
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError

Line 7 sets the DBMS to MySQL.

  • The [main_withpgres] file from [2] is the [main] file from [1] with the following changes: it uses the PostgreSQL DBMS:
#  a mysql or pgres parameter is expected
import os
import sys

#  configure the application with MySQL
import config
config = config.configure({'sgbd': "pgres"})

#  dependencies
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError

On line 7, we set the DBMS to PostgreSQL.

Once this is done, we create the following script [main_withmysql.wsgi] (the suffix used does not matter):

#  folder of this file
import os
script_dir = os.path.dirname(os.path.abspath(__file__))

#  we add it to the syspath so that the following import is possible
import sys
sys.path.insert(0, script_dir)

#  we import the Flask application [app], giving it the name [application]
from main_withmysql import app as application

The script [main_withmysql.wsgi] will be the target executed by the Apache server in WSGI mode:

  • The Apache server’s target could have been the script [main_withmysql.py], as was done previously with the script [date_time_server.py]. But it would have required a slight modification:
    • unlike when running a console script, with Apache, the directory containing the target [main_withmysql.py] is not part of the Python Path. Therefore, line 6 of the [main_withmysql.py] script causes an error;
    • the second change that would have been necessary is that in [main_withmysql], the Flask application is referenced by the identifier [app]. We know that for Apache/WSGI, it must also be referenced by an identifier [application];
  • Instead of modifying [main_withmysql.py], we change the Apache target. It will now be the [main_withmysql.wsgi] script above:
    • lines 1–7: we add the script’s directory to the Python Path. As a result, line 6 of [main_withmysql.py] no longer causes an error;
    • lines 9–10: importing [main_withmysql.py] triggers its execution. Additionally, we reference the Flask application [app] found in [main_withmysql.py] using the [application] identifier required by Apache in WSGI mode;

We do the same with the [main_withpgres.wsgi] script:

#  folder of this file
import os
script_dir = os.path.dirname(os.path.abspath(__file__))

#  we add it to the syspath so that the following import is possible
import sys
sys.path.insert(0, script_dir)

#  we import the Flask application [app], giving it the name [application]
from main_withpgres import app as application

We now have the executable targets for the Apache server. We now need to create two virtual servers, one for each target.

In [<laragon>\etc\apache2\sites-enabled], we create the file [flask-impots-withmysql.conf] (the name doesn’t matter):

Image


# dossier du script .wsgi
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"
 
# nom du site web configuré par ce fichier
# ici il s'appellera flask-impots-withmysql
# les URL seront du type http(s)://flask-impots-withmysql/path
define SITE "flask-impots-withmysql"
 
# mettre l'adresse IP 127.0.0.1 pour site SITE dans c:/windows/system32/drivers/etc/hosts
 
# mettre ici les chemins des bibliothèques Python à utiliser - les séparer par des virgules
# ici les bibliothèques d'un environnement virtuel Python
WSGIPythonPath  "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages"
 
# Python Home - nécessaire uniquement s'il y a plusieurs versions de Python installées
# WSGIPythonHome "C:/Program Files/Python38"
 
# URL HTTP
<VirtualHost *:80>
    # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
    # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
    # où [prefixe_url] est défini dans parameters.py
    WSGIScriptAlias / "${ROOT}/main_withmysql.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
 
# URL sécurisées avec HTTPS
<VirtualHost *:443>
    # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
    # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
    # où [prefixe_url] est défini dans parameters.py
    WSGIScriptAlias / "${ROOT}/main_withmysql.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
 
    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/myprograms/laragon/etc/ssl/laragon.key
 
</VirtualHost>
  • line 2: the application root, the [apache] folder we created;
  • lines 23, 38: the target [main_withmysql.wsgi] that we created:
  • line 7: the virtual server will be named [flask-impots-withmysql];
  • line 13: the [WSGIPythonPath] directive allows us to add folders to the Python Path. Here, Apache is unaware that we used a virtual environment to develop the application and that all modules used by the application are in that virtual environment. Therefore, on line 13, we add the folder containing all modules from the virtual environment used. One option is to copy this directory to another location in the file system and reference that location. Another option is to add this directory to the Python Path directly within the [main_withmysql.wsgi] target (this is likely a better solution);
  • line 16: we can specify the Python installation directory in the file system to Apache. Normally, this is in the machine’s PATH, and often this line is unnecessary (which was the case here). However, there may be multiple Python installations on the machine, and the desired one may not be in the machine’s PATH. In that case, this line resolves the issue;

Similarly, create a [flask-impots-withpgres.conf] file:


# dossier du script .wsgi
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"
 
# nom du site web configuré par ce fichier
# ici il s'appellera flask-impots-withmysql
# les URL seront du type http(s)://flask-impots-withmysql/path
define SITE "flask-impots-withpgres"
 
# mettre l'adresse IP 127.0.0.1 pour site SITE dans c:/windows/system32/drivers/etc/hosts
 
# mettre ici les chemins des bibliothèques Python à utiliser - les séparer par des virgules
# ici les bibliothèques d'un environnement virtuel Python
WSGIPythonPath  "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages"
 
# Python Home - nécessaire uniquement s'il y a plusieurs versions de Python installées
# WSGIPythonHome "C:/Program Files/Python38"
 
# URL HTTP
<VirtualHost *:80>
    # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
    # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
    # où [prefixe_url] est défini dans parameters.py
    WSGIScriptAlias / "${ROOT}/main_withpgres.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
 
# URL sécurisées avec HTTPS
<VirtualHost *:443>
    # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
    # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
    # où [prefixe_url] est défini dans parameters.py
    WSGIScriptAlias / "${ROOT}/main_withpgres.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
 
    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/myprograms/laragon/etc/ssl/laragon.key
 
</VirtualHost>

We save all these files, start the Apache server and the MySQL and PostgreSQL databases. The application is configured with the URL prefix [/do] and [with_csrftoken=False] (no CSRF token) in [configs/parameters.py]. We request the URL [https://flask-impots-withmysql/do]. The server’s response is as follows:

Image

We now request the URL [https://flask-impots-pgres/do]. The response is as follows:

Image

Both applications are working normally.

Now let’s modify the [WSGIScriptAlias] parameter in [flask-impots-withmysql.conf]:

#  .wsgi script folder
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"



#  URL HTTP
<VirtualHost *:80>
    #  with the alias / the URL will have the form /{prefixe_url}/action/...
    #  with the alias /impots the URL will take the form /impots/{prefixe_url}/action/...
    #  where [prefixe_url] is defined in parameters.py
    WSGIScriptAlias /impots "${ROOT}/main_withmysql.wsgi"
    
</VirtualHost>

#  URL secured with HTTPS
<VirtualHost *:443>
    #  with the alias / the URL will have the form /{prefixe_url}/action/...
    #  with the alias /impots the URL will take the form /impots/{prefixe_url}/action/...
    #  where [prefixe_url] is defined in parameters.py
    WSGIScriptAlias /impots "${ROOT}/main_withmysql.wsgi"
    

</VirtualHost>
  • lines 11 and 20, the WSI alias is now [/impots];

We stop and restart the Apache server, then request the URL [https://flask-impots-withmysql/impots/do]. The server’s response is as follows:

Image

There is a crash. The URL [1] tells us the cause. It should have been [https://flask-impots-withmysql/impots/do/afficher-vue-authentification]. The WSGI alias is missing. This is an error in our application. It knows how to handle a URL prefix (/do is present). One might think that adding the prefix [/impots/do] to our application would solve the previous problem. But no. We then encounter other types of problems. The WSGI alias does not behave like a URL prefix.

Let’s try to understand what happened. We requested the URL [https://flask-impots-withmysql/impots/do]. We expected to see the authentication view. In [1] above, we see that the application requested to display it, but not with the correct URL. Let’s examine the path of the request [https://flask-impots-withmysql/impots/do].

First, the following route (configs/routes.py) was executed:


    # les routes de l'application Flask
    # racine de l'application
    app.add_url_rule(f'{prefix_url}/', methods=['GET'],
                     view_func=routes.index)

The route on line 3 is [https://flask-impots-withmysql/impots/do] in our example. We can see that the route has been stripped of the [https://flask-impots-withmysql/impots] part to become simply [/do]. As for the [https://flask-impots-withmysql] part, this is normal; the server name is not included in the route. But we can see that it also does not include the WSGI alias [/impots]. This is an important point. Even with a WSGI alias, our initial routes remain valid.

Now let’s see what the [index] function on line 4 (configs/routes_without_csrftoken) does:

1
2
3
4
#  application root
def index() -> tuple:
    #  redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

On line 4, we are redirected to the URL of the [init_session] function. In [configs/routes.py], this function has been associated with the route [/do/init-session/html]:


    # init-session
    app.add_url_rule(f'{prefix_url}/init-session/<string:type_response>{csrftoken_param}', methods=['GET'],
                     view_func=routes.init_session)

Line 2: In our test, [csrftoken_param] is the empty string. The application does not handle a CSRF token here.

The [init_session] function is defined as follows (configs/routes_without_csrftoken):

1
2
3
4
#  init-session
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Line 4: We start the action processing chain [init-session]. This chain ends as follows in [responses/HtmlResponse]:



# now it's time to generate the URL redirection, not forgetting the CSRF token if requested
        if config['parameters']['with_csrftoken']:
            csrf_token = f"/{generate_csrf()}"
        else:
            csrf_token = ""
 
        # redirect response
        return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND

The [init-session] action is an ADS (Action Do Something) action that ends with a redirect to a view, line 9. That is where the problem lies. The [redirect] function on line 9 does not automatically add the WSGI alias to the redirect URL. This is shown in the screenshot above. The /impots alias is missing from the redirect URL.

The following version provides a solution to the WSGI alias problem.