One place for hosting & domains

      Applications

      Building Go Applications for Different Operating Systems and Architectures


      In software development, it is important to consider the operating system and underlying processor architecture that you would like to compile your binary for. Since it is often slow or impossible to run a binary on a different OS/architecture platform, it is a common practice to build your final binary for many different platforms to maximize your program’s audience. However, this can be difficult when the platform you are using for development is different from the platform you want to deploy your program to. In the past, for example, developing a program on Windows and deploying it to a Linux or a macOS machine would involve setting up build machines for each of the environments you wanted binaries for. You’d also need to keep your tooling in sync, in addition to other considerations that would add cost and make collaborative testing and distribution more difficult.

      Go solves this problem by building support for multiple platforms directly into the go build tool, as well as the rest of the Go toolchain. By using environment variables and build tags, you can control which OS and architecture your final binary is built for, in addition to putting together a workflow that can quickly toggle the inclusion of platform-dependent code without changing your codebase.

      In this tutorial, you will put together a sample application that joins strings together into a filepath, create and selectively include platform-dependent snippets, and build binaries for multiple operating systems and system architectures on your own system, showing you how to use this powerful capability of the Go programming language.

      Prerequisites

      To follow the example in this article, you will need:

      Possible Platforms for GOOS and GOARCH

      Before showing how to control the build process to build binaries for different platforms, let’s first inspect what kinds of platforms Go is capable of building for, and how Go references these platforms using the environment variables GOOS and GOARCH.

      The Go tooling has a command that can print a list of the possible platforms that Go can build on. This list can change with each new Go release, so the combinations discussed here might not be the same on another version of Go. At the time of writing this tutorial, the current Go release is 1.13.

      To find this list of possible platforms, run the following:

      You will receive an output similar to the following:

      Output

      aix/ppc64 freebsd/amd64 linux/mipsle openbsd/386 android/386 freebsd/arm linux/ppc64 openbsd/amd64 android/amd64 illumos/amd64 linux/ppc64le openbsd/arm android/arm js/wasm linux/s390x openbsd/arm64 android/arm64 linux/386 nacl/386 plan9/386 darwin/386 linux/amd64 nacl/amd64p32 plan9/amd64 darwin/amd64 linux/arm nacl/arm plan9/arm darwin/arm linux/arm64 netbsd/386 solaris/amd64 darwin/arm64 linux/mips netbsd/amd64 windows/386 dragonfly/amd64 linux/mips64 netbsd/arm windows/amd64 freebsd/386 linux/mips64le netbsd/arm64 windows/arm

      This output is a set of key-value pairs separated by a /. The first part of the combination, before the /, is the operating system. In Go, these operating systems are possible values for the environment variable GOOS, pronounced “goose”, which stands for Go Operating System. The second part, after the /, is the architecture. As before, these are all possible values for an environment variable: GOARCH. This is pronounced “gore-ch”, and stands for Go Architecture.

      Let’s break down one of these combinations to understand what it means and how it works, using linux/386 as an example. The key-value pair starts with the GOOS, which in this example would be linux, referring to the Linux OS. The GOARCH here would be 386, which stands for the Intel 80386 microprocessor.

      There are many platforms available with the go build command, but a majority of the time you’ll end up using linux , windows, or darwin as a value for GOOS. These cover the big three OS platforms: Linux, Windows, and macOS, which is based on the Darwin operating system and is thus called darwin. However, Go can also cover less mainstream platforms like nacl, which represents Google’s Native Client.

      When you run a command like go build, Go uses the current platform’s GOOS and GOARCH to determine how to build the binary. To find out what combination your platform is, you can use the go env command and pass GOOS and GOARCH as arguments:

      In testing this example, we ran this command on macOS on a machine with an AMD64 architecture, so we will receive the following output:

      Output

      darwin amd64

      Here the output of the command tells us that our system has GOOS=darwin and GOARCH=amd64.

      You now know what the GOOS and GOARCH are in Go, as well as their possible values. Next, you will put together a program to use as an example of how to use these environment variables and build tags to build binaries for other platforms.

      Write a Platform-Dependent Program with filepath.Join()

      Before you start building binaries for other platforms, let’s build an example program. A good sample for this purpose is the Join function in the path/filepath package in the Go standard library. This function takes a number of strings and returns one string that is joined together with the correct filepath separator.

      This is a good example program because the operation of the program depends on which OS it is running on. On Windows, the path separator is a backslash, , while Unix-based systems use a forward slash, /.

      Let’s start with building an application that uses filepath.Join(), and later, you’ll write your own implementation of the Join() function that customizes the code to the platform-specific binaries.

      First, create a folder in your src directory with the name of your app:

      Move into that directory:

      Next, create a new file in your text editor of choice named main.go. For this tutorial, we will use Nano:

      Once the file is open, add the following code:

      src/app/main.go

      package main
      
      import (
        "fmt"
        "path/filepath"
      )
      
      func main() {
        s := filepath.Join("a", "b", "c")
        fmt.Println(s)
      }
      

      The main() function in this file uses filepath.Join() to concatenate three strings together with the correct, platform-dependent path separator.

      Save and exit the file, then run the program:

      When running this program, you will receive different output depending on which platform you are using. On Windows, you will see the strings separated by :

      Output

      abc

      On Unix systems like macOS and Linux, you will receive the following:

      Output

      a/b/c

      This shows that, because of the different filesystem protocols used on these operating systems, the program will have to build different code for the different platforms. But since it already uses a different file separator depending on the OS, we know that filepath.Join() already accounts for the difference in platform. This is because the Go tool chain automatically detects your machine’s GOOS and GOARCH and uses this information to use the code snippet with the right build tags and file separator.

      Let’s consider where the filepath.Join() function gets its separator from. Run the following command to inspect the relevant snippet from Go’s standard library:

      • less /usr/local/go/src/os/path_unix.go

      This will display the contents of path_unix.go. Look for the following part of the file:

      /usr/local/go/os/path_unix.go

      . . .
      // +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris
      
      package os
      
      const (
        PathSeparator     = '/' // OS-specific path separator
        PathListSeparator = ':' // OS-specific path list separator
      )
      . . .
      

      This section defines the PathSeparator for all of the varieties of Unix-like systems that Go supports. Notice all of the build tags at the top, which are each one of the possible Unix GOOS platforms associated with Unix. When the GOOS matches these terms, your program will yield the Unix-styled filepath separator.

      Press q to return to the command line.

      Next, open the file that defines the behavior of filepath.Join() when used on Windows:

      • less /usr/local/go/src/os/path_windows.go

      You will see the following:

      /usr/local/go/os/path_unix.go

      . . .
      package os
      
      const (
              PathSeparator     = '\' // OS-specific path separator
              PathListSeparator = ';'  // OS-specific path list separator
      )
      . . .
      

      Although the value of PathSeparator is \ here, the code will render the single backslash () needed for Windows filepaths, since the first backslash is only needed as an escape character.

      Notice that, unlike the Unix file, there are no build tags at the top. This is because GOOS and GOARCH can also be passed to go build by adding an underscore (_) and the environment variable value as a suffix to the filename, something we will go into more in the section Using GOOS and GOARCH File Name Suffixes. Here, the _windows part of path_windows.go makes the file act as if it had the build tag // +build windows at the top of the file. Because of this, when your program is run on Windows, it will use the constants of PathSeparator and PathListSeparator from the path_windows.go code snippet.

      To return to the command line, quit less by pressing q.

      In this step, you built a program that showed how Go converts the GOOS and GOARCH automatically into build tags. With this in mind, you can now update your program and write your own implementation of filepath.Join(), using build tags to manually set the correct PathSeparator for Windows and Unix platforms.

      Implementing a Platform-Specific Function

      Now that you know how Go’s standard library implements platform-specific code, you can use build tags to do this in your own app program. To do this, you will write your own implementation of filepath.Join().

      Open up your main.go file:

      Replace the contents of main.go with the following, using your own function called Join():

      src/app/main.go

      package main
      
      import (
        "fmt"
        "strings"
      )
      
      func Join(parts ...string) string {
        return strings.Join(parts, PathSeparator)
      }
      
      func main() {
        s := Join("a", "b", "c")
        fmt.Println(s)
      }
      

      The Join function takes a number of parts and joins them together using the strings.Join() method from the strings package to concatenate the parts together using the PathSeparator.

      You haven’t defined the PathSeparator yet, so do that now in another file. Save and quit main.go, open your favorite editor, and create a new file named path.go:

      nano path.go
      

      Define the PathSeparator and set it equal to the Unix filepath separator, /:

      src/app/path.go

      package main
      
      const PathSeparator = "/"
      

      Compile and run the application:

      You’ll receive the following output:

      Output

      a/b/c

      This runs successfully to get a Unix-style filepath. But this isn’t yet what we want: the output is always a/b/c, regardless of what platform it runs on. To add in the functionality to create Windows-style filepaths, you will need to add a Windows version of the PathSeparator and tell the go build command which version to use. In the next section, you will use build tags to accomplish this.

      To account for Windows platforms, you will now create an alternate file to path.go and use build tags to make sure the code snippets only run when GOOS and GOARCH are the appropriate platform.

      But first, add a build tag to path.go to tell it to build for everything except for Windows. Open up the file:

      Add the following highlighted build tag to the file:

      src/app/path.go

      // +build !windows
      
      package main
      
      const PathSeparator = "/"
      

      Go build tags allow for inverting, meaning that you can instruct Go to build this file for any platform except for Windows. To invert a build tag, place a ! before the tag.

      Save and exit the file.

      Now, if you were to run this program on Windows, you would get the following error:

      Output

      ./main.go:9:29: undefined: PathSeparator

      In this case, Go would not be able to include path.go to define the variable PathSeparator.

      Now that you have ensured that path.go will not run when GOOS is Windows, add a new file, windows.go:

      In windows.go, define the Windows PathSeparator, as well as a build tag to let the go build command know it is the Windows implementation:

      src/app/windows.go

      // +build windows
      
      package main
      
      const PathSeparator = "\"
      

      Save the file and exit from the text editor. The application can now compile one way for Windows and another for all other platforms.

      While the binaries will now build correctly for their platforms, there are further changes you must make in order to compile for a platform that you do not have access to. To do this, you will alter your local GOOS and GOARCH environment variables in the next step.

      Using Your Local GOOS and GOARCH Environment Variables

      Earlier, you ran the go env GOOS GOARCH command to find out what OS and architecture you were working on. When you ran the go env command, it looked for the two environment variables GOOS and GOARCH; if found, their values would be used, but if not found, then Go would set them with the information for the current platform. This means that you can change GOOS or GOARCH so that they do not default to your local OS and architecture.

      The go build command behaves in a similar manner to the go env command. You can set either the GOOS or GOARCH environment variables to build for a different platform using go build.

      If you are not using a Windows system, build a windows binary of app by setting the GOOS environment variable to windows when running the go build command:

      Now list the files in your current directory:

      The output of listing the directory shows there is now an app.exe Windows executable in the project directory:

      Output

      app app.exe main.go path.go windows.go

      Using the file command, you can get more information about this file, confirming its build:

      You will receive:

      Output

      app.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows

      You can also set one, or both environment variables at build time. Run the following:

      • GOOS=linux GOARCH=ppc64 go build

      Your app executable will now be replaced by a file for a different architecture. Run the file command on this binary:

      You will receive output like the following:

      app: ELF 64-bit MSB executable, 64-bit PowerPC or cisco 7500, version 1 (SYSV), statically linked, not stripped
      

      By setting your local GOOS and GOARCH environment variables, you can now build binaries for any of Go’s compatible platforms without a complicated configuration or setup. Next, you will use filename conventions to keep your files neatly organized and build for specific platforms automatically wihout build tags.

      Using GOOS and GOARCH Filename Suffixes

      As you saw earlier, the Go standard library makes heavy use of build tags to simplify code by separating out different platform implementations into different files. When you opened the os/path_unix.go file, there was a build tag that listed all of the possible combinations that are considered Unix-like platforms. The os/path_windows.go file, however, contained no build tags, because the suffix on the filename sufficed to tell Go which platform the file was meant for.

      Let’s look at the syntax of this feature. When naming a .go file, you can add GOOS and GOARCH as suffixes to the file’s name in that order, separating the values by underscores (_). If you had a Go file named filename.go, you could specify the OS and architecture by changing the filename to filename_GOOS_GOARCH.go. For example, if you wished to compile it for Windows with 64-bit ARM architecture, you would make the name of the file filename_windows_arm64.go. This naming convention helps keep code neatly organized.

      Update your program to use the filename suffixes instead of build tags. First, rename the path.go and windows.go file to use the convention used in the os package:

      • mv path.go path_unix.go
      • mv windows.go path_windows.go

      With the two filenames changed, you can remove the build tag you added to path_windows.go:

      Remove // +build windows so that your file looks like this:

      path_windows.go

      package main
      
      const PathSeparator = "\"
      

      Save and exit from the file.

      Because unix is not a valid GOOS, the _unix.go suffix has no meaning to the Go compiler. It does, however, convey the intended purpose of the file. Like the os/path_unix.go file, your path_unix.go file still needs to use build tags, so keep that file unchanged.

      By using filename conventions, you removed uneeded build tags from your source code and made the filesystem cleaner and clearer.

      Conclusion

      The ability to generate binaries for multiple platforms that require no dependencies is a powerful feature of the Go toolchain. In this tutorial, you used this capability by adding build tags and filename suffixes to mark certain code snippets to only compile for certain architectures. You created your own platorm-dependent program, then manipulated the GOOS and GOARCH environment variables to generate binaries for platforms beyond your current platform. This is a valuable skill, because it is a common practice to have a continuous integration process that automatically runs through these environment variables to build binaries for all platforms.

      For further study on go build, check out our Customizing Go Binaries with Build Tags tutorial. If you’d like to learn more about the Go programming language in general, check out the entire How To Code in Go series.



      Source link

      How To Send Web Push Notifications from Django Applications


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      The web is constantly evolving, and it can now achieve functionalities that were formerly only available on native mobile devices. The introduction of JavaScript service workers gave the web newfound abilities to do things like background syncing, offline caching, and sending push notifications.

      Push notifications allow users to opt-in to receive updates to mobile and web applications. They also enable users to re-engage with existing applications using customized and relevant content.

      In this tutorial, you’ll set up a Django application on Ubuntu 18.04 that sends push notifications whenever there’s an activity that requires the user to visit the application. To create these notifications, you will use the Django-Webpush package and set up and register a service worker to display notifications to the client. The working application with notifications will look like this:

      Web push final

      Prerequisites

      Before you begin this guide you’ll need the following:

      Step 1 — Installing Django-Webpush and Getting Vapid Keys

      Django-Webpush is a package that enables developers to integrate and send web push notifications in Django applications. We’ll use this package to trigger and send push notifications from our application. In this step, you will install Django-Webpush and obtain the Voluntary Application Server Identification (VAPID) keys that are necessary for identifying your server and ensuring the uniqueness of each request.

      Make sure you are in the ~/djangopush project directory that you created in the prerequisites:

      Activate your virtual environment:

      • source my_env/bin/activate

      Upgrade your version of pip to ensure it's up-to-date:

      • pip install --upgrade pip

      Install Django-Webpush:

      • pip install django-webpush

      After installing the package, add it to the list of applications in your settings.py file. First open settings.py:

      • nano ~/djangopush/djangopush/settings.py

      Add webpush to the list of INSTALLED_APPS:

      ~/djangopush/djangopush/settings.py

      ...
      
      INSTALLED_APPS = [
          ...,
          'webpush',
      ]
      ...
      

      Save the file and exit your editor.

      Run migrations on the application to apply the changes you've made to your database schema:

      The output will look like this, indicating a successful migration:

      Output

      Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, webpush Running migrations: Applying webpush.0001_initial... OK

      The next step in setting up web push notifications is getting VAPID keys. These keys identify the application server and can be used to reduce the secrecy for push subscription URLs, since they restrict subscriptions to a specific server.

      To obtain VAPID keys, navigate to the wep-push-codelab web application. Here, you'll be given automatically generated keys. Copy the private and public keys.

      Next, create a new entry in settings.py for your VAPID information. First, open the file:

      • nano ~/djangopush/djangopush/settings.py

      Next, add a new directive called WEBPUSH_SETTINGS with your VAPID public and private keys and your email below AUTH_PASSWORD_VALIDATORS:

      ~/djangopush/djangopush/settings.py

      ...
      
      AUTH_PASSWORD_VALIDATORS = [
          ...
      ]
      
      WEBPUSH_SETTINGS = {
         "VAPID_PUBLIC_KEY": "your_vapid_public_key",
         "VAPID_PRIVATE_KEY": "your_vapid_private_key",
         "VAPID_ADMIN_EMAIL": "admin@example.com"
      }
      
      # Internationalization
      # https://docs.djangoproject.com/en/2.0/topics/i18n/
      
      ...
      

      Don't forget to replace the placeholder values your_vapid_public_key, your_vapid_private_key, and admin@example.com with your own information. Your email address is how you will be notified if the push server experiences any issues.

      Next, we'll set up views that will display the application's home page and trigger push notifications to subscribed users.

      Step 2 — Setting Up Views

      In this step, we'll setup a basic home view with the HttpResponse response object for our home page, along with a send_push view. Views are functions that return response objects from web requests. The send_push view will use the Django-Webpush library to send push notifications that contain the data entered by a user on the home page.

      Navigate to the ~/djangopush/djangopush folder:

      • cd ~/djangopush/djangopush

      Running ls inside the folder will show you the project's main files:

      Output

      /__init__.py /settings.py /urls.py /wsgi.py

      The files in this folder are auto-generated by the django-admin utility that you used to create your project in the prerequisites. The settings.py file contains project-wide configurations like installed applications and the static root folder. The urls.py file contains the URL configurations for the project. This is where you will set up routes to match your created views.

      Create a new file inside the ~/djangopush/djangopush directory called views.py, which will contain the views for your project:

      • nano ~/djangopush/djangopush/views.py

      The first view we'll make is the home view, which will display the home page where users can send push notifications. Add the following code to the file:

      ~/djangopush/djangopush/views.py

      from django.http.response import HttpResponse
      from django.views.decorators.http import require_GET
      
      @require_GET
      def home(request):
          return HttpResponse('<h1>Home Page<h1>')
      

      The home view is decorated by the require_GET decorator, which restricts the view to GET requests only. A view typically returns a response for every request made to it. This view returns a simple HTML tag as a response.

      The next view we'll create is send_push, which will handle sent push notifications using the django-webpush package. It will be restricted to POST requests only and will be exempted from Cross Site Request Forgery (CSRF) protection. Doing this will allow you to test the view using Postman or any other RESTful service. In production, however, you should remove this decorator to avoid leaving your views vulnerable to CSRF.

      To create the send_push view, first add the following imports to enable JSON responses and access the send_user_notification function in the webpush library:

      ~/djangopush/djangopush/views.py

      from django.http.response import JsonResponse, HttpResponse
      from django.views.decorators.http import require_GET, require_POST
      from django.shortcuts import get_object_or_404
      from django.contrib.auth.models import User
      from django.views.decorators.csrf import csrf_exempt
      from webpush import send_user_notification
      import json
      

      Next, add the require_POST decorator, which will use the request body sent by the user to create and trigger a push notification:

      ~/djangopush/djangopush/views.py

      @require_GET
      def home(request):
          ...
      
      
      @require_POST
      @csrf_exempt
      def send_push(request):
          try:
              body = request.body
              data = json.loads(body)
      
              if 'head' not in data or 'body' not in data or 'id' not in data:
                  return JsonResponse(status=400, data={"message": "Invalid data format"})
      
              user_id = data['id']
              user = get_object_or_404(User, pk=user_id)
              payload = {'head': data['head'], 'body': data['body']}
              send_user_notification(user=user, payload=payload, ttl=1000)
      
              return JsonResponse(status=200, data={"message": "Web push successful"})
          except TypeError:
              return JsonResponse(status=500, data={"message": "An error occurred"})
      

      We are using two decorators for the send_push view: the require_POST decorator, which restricts the view to POST requests only, and the csrf_exempt decorator, which exempts the view from CSRF protection.

      This view expects POST data and does the following: it gets the body of the request and, using the json package, deserializes the JSON document to a Python object using json.loads. json.loads takes a structured JSON document and converts it to a Python object.

      The view expects the request body object to have three properties:

      • head: The title of the push notification.
      • body: The body of the notification.
      • id: The id of the request user.

      If any of the required properties are missing, the view will return a JSONResponse with a 404 "Not Found" status. If the user with the given primary key exists, the view will return the user with the matching primary key using the get_object_or_404 function from the django.shortcuts library. If the user doesn't exist, the function will return a 404 error.

      The view also makes use of the send_user_notification function from the webpush library. This function takes three parameters:

      • User: The recipient of the push notification.
      • payload: The notification information, which includes the notification head and body.
      • ttl: The maximum time in seconds that the notification should be stored if the user is offline.

      If no errors occur, the view returns a JSONResponse with a 200 "Success" status and a data object. If a KeyError occurs, the view will return a 500 "Internal Server Error" status. A KeyError occurs when the requested key of an object doesn't exist.

      In the next step, we'll create corresponding URL routes to match the views we've created.

      Step 3 — Mapping URLs to Views

      Django makes it possible to create URLs that connect to views with a Python module called a URLconf. This module maps URL path expressions to Python functions (your views). Usually, a URL configuration file is auto-generated when you create a project. In this step, you will update this file to include new routes for the views you created in the previous step, along with the URLs for the django-webpush app, which will provide endpoints to subscribe users to push notifications.

      For more information about views, please see How To Create Django Views.

      Open urls.py:

      • nano ~/djangopush/djangopush/urls.py

      The file will look like this:

      ~/djangopush/djangopush/urls.py

      
      """untitled URL Configuration
      
      The `urlpatterns` list routes URLs to views. For more information please see:
          https://docs.djangoproject.com/en/2.1/topics/http/urls/
      Examples:
      Function views
          1. Add an import:  from my_app import views
          2. Add a URL to urlpatterns:  path('', views.home, name='home')
      Class-based views
          1. Add an import:  from other_app.views import Home
          2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
      Including another URLconf
          1. Import the include() function: from django.urls import include, path
          2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
      """
      from django.contrib import admin
      from django.urls import path
      
      urlpatterns = [
          path('admin/', admin.site.urls),
      ]
      

      The next step is to map the views you've created to URLs. First, add the include import to ensure that all of the routes for the Django-Webpush library will be added to your project:

      ~/djangopush/djangopush/urls.py

      
      """webpushdjango URL Configuration
      ...
      """
      from django.contrib import admin
      from django.urls import path, include
      

      Next, import the views you created in the last step and update the urlpatterns list to map your views:

      ~/djangopush/djangopush/urls.py

      
      """webpushdjango URL Configuration
      ...
      """
      from django.contrib import admin
      from django.urls import path, include
      
      from .views import home, send_push
      
      urlpatterns = [
                        path('admin/', admin.site.urls),
                        path('', home),
                        path('send_push', send_push),
                        path('webpush/', include('webpush.urls')),
                    ]
      

      Here, the urlpatterns list registers the URLs for the django-webpush package and maps your views to the URLs /send_push and /home.

      Let's test the /home view to be sure that it's working as intended. Make sure you're in the root directory of the project:

      Start your server by running the following command:

      • python manage.py runserver your_server_ip:8000

      Navigate to http://your_server_ip:8000. You should see the following home page:

      Initial Home Page view

      At this point, you can kill the server with CTRL+C, and we will move on to creating templates and rendering them in our views using the render function.

      Step 4 — Creating Templates

      Django’s template engine allows you to define the user-facing layers of your application with templates, which are similar to HTML files. In this step, you will create and render a template for the home view.

      Create a folder called templates in your project's root directory:

      • mkdir ~/djangopush/templates

      If you run ls in the root folder of your project at this point, the output will look like this:

      Output

      /djangopush /templates db.sqlite3 manage.py /my_env

      Create a file called home.html in the templates folder:

      • nano ~/djangopush/templates/home.html

      Add the following code to the file to create a form where users can enter information to create push notifications:

      {% load static %}
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <meta name="vapid-key" content="{{ vapid_key }}">
          {% if user.id %}
              <meta name="user_id" content="{{ user.id }}">
          {% endif %}
          <title>Web Push</title>
          <link href="https://fonts.googleapis.com/css?family=PT+Sans:400,700" rel="stylesheet">
      </head>
      
      <body>
      <div>
          <form id="send-push__form">
              <h3 class="header">Send a push notification</h3>
              <p class="error"></p>
              <input type="text" name="head" placeholder="Header: Your favorite airline 😍">
              <textarea name="body" id="" cols="30" rows="10" placeholder="Body: Your flight has been cancelled 😱😱😱"></textarea>
              <button>Send Me</button>
          </form>
      </div>
      </body>
      </html>
      

      The body of the file includes a form with two fields: an input element will hold the head/title of the notification and a textarea element will hold the notification body.

      In the head section of the file, there are two meta tags that will hold the VAPID public key and the user's id. These two variables are required to register a user and send them push notifications. The user's id is required here because you'll be sending AJAX requests to the server and the id will be used to identify the user. If the current user is a registered user, then the template will create a meta tag with their id as the content.

      The next step is to tell Django where to find your templates. To do this, you will edit settings.py and update the TEMPLATES list.

      Open the settings.py file:

      • nano ~/djangopush/djangopush/settings.py

      Add the following to the DIRS list to specify the path to the templates directory:

      ~/djangopush/djangopush/settings.py

      ...
      TEMPLATES = [
          {
              'BACKEND': 'django.template.backends.django.DjangoTemplates',
              'DIRS': [os.path.join(BASE_DIR, 'templates')],
              'APP_DIRS': True,
              'OPTIONS': {
                  'context_processors': [
                      ...
                  ],
              },
          },
      ]
      ...
      

      Next, in your views.py file, update the home view to render the home.html template. Open the file:

      • nano ~/djangpush/djangopush/views.py

      First, add some additional imports, including the settings configuration, which contains all of the project's settings from the settings.py file, and the render function from django.shortcuts:

      ~/djangopush/djangopush/views.py

      ...
      from django.shortcuts import render, get_object_or_404
      ...
      import json
      from django.conf import settings
      
      ...
      

      Next, remove the initial code you added to the home view and add the following, which specifies how the template you just created will be rendered:

      ~/djangopush/djangopush/views.py

      ...
      
      @require_GET
      def home(request):
         webpush_settings = getattr(settings, 'WEBPUSH_SETTINGS', {})
         vapid_key = webpush_settings.get('VAPID_PUBLIC_KEY')
         user = request.user
         return render(request, 'home.html', {user: user, 'vapid_key': vapid_key})
      

      The code assigns the following variables:

      • webpush_settings: This is assigned the value of the WEBPUSH_SETTINGS attribute from the settings configuration.
      • vapid_key: This gets the VAPID_PUBLIC_KEY value from the webpush_settings object to send to the client. This public key is checked against the private key to ensure that the client with the public key is permitted to receive push messages from the server.
      • user: This variable comes from the incoming request. Whenever a user makes a request to the server, the details for that user are stored in the user field.

      The render function will return an HTML file and a context object containing the current user and the server's vapid public key. It takes three parameters here: the request, the template to be rendered, and the object that contains the variables that will be used in the template.

      With our template created and the home view updated, we can move on to configuring Django to serve our static files.

      Step 5 — Serving Static Files

      Web applications include CSS, JavaScript, and other image files that Django refers to as “static files”. Django allows you to collect all of the static files from each application in your project into a single location from which they are served. This solution is called django.contrib.staticfiles. In this step, we'll update our settings to tell Django where our static files will be stored.

      Open settings.py:

      • nano ~/djangopush/djangopush/settings.py

      In settings.py, first ensure that the STATIC_URL has been defined:

      ~/djangopush/djangopush/settings.py

      ...
      STATIC_URL = '/static/'
      

      Next, add a list of directories called STATICFILES_DIRS where Django will look for static files:

      ~/djangopush/djangopush/settings.py

      ...
      STATIC_URL = '/static/'
      STATICFILES_DIRS = [
          os.path.join(BASE_DIR, "static"),
      ]
      

      You can now add the STATIC_URL to the list of paths defined in your urls.py file.

      Open the file:

      • nano ~/djangopush/djangopush/urls.py

      Add the following code, which will import the static url configuration and update the urlpatterns list. The helper function here uses the STATIC_URL and STATIC_ROOT properties we provided in the settings.py file to serve the project's static files:

      ~/djangopush/djangopush/urls.py

      
      ...
      from django.conf import settings
      from django.conf.urls.static import static
      
      urlpatterns = [
          ...
      ]  + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
      

      With our static files settings configured, we can move on to styling the application's home page.

      Step 6 — Styling the Home Page

      After setting up your application to serve static files, you can create an external stylesheet and link it to the home.html file to style the home page. All of your static files will be stored in a static directory in the root folder of your project.

      Create a static folder and a css folder within the static folder:

      • mkdir -p ~/djangopush/static/css

      Open a css file called styles.css inside the css folder:

      • nano ~/djangopush/static/css/styles.css

      Add the following styles for the home page:

      ~/djangopush/static/css/styles.css

      
      body {
          height: 100%;
          background: rgba(0, 0, 0, 0.87);
          font-family: 'PT Sans', sans-serif;
      }
      
      div {
          height: 100%;
          display: flex;
          align-items: center;
          justify-content: center;
      }
      
      form {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          width: 35%;
          margin: 10% auto;
      }
      
      form > h3 {
          font-size: 17px;
          font-weight: bold;
          margin: 15px 0;
          color: orangered;
          text-transform: uppercase;
      }
      
      form > .error {
          margin: 0;
          font-size: 15px;
          font-weight: normal;
          color: orange;
          opacity: 0.7;
      }
      
      form > input, form > textarea {
          border: 3px solid orangered;
          box-shadow: unset;
          padding: 13px 12px;
          margin: 12px auto;
          width: 80%;
          font-size: 13px;
          font-weight: 500;
      }
      
      form > input:focus, form > textarea:focus {
          border: 3px solid orangered;
          box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.2);
          outline: unset;
      }
      
      form > button {
          justify-self: center;
          padding: 12px 25px;
          border-radius: 0;
          text-transform: uppercase;
          font-weight: 600;
          background: orangered;
          color: white;
          border: none;
          font-size: 14px;
          letter-spacing: -0.1px;
          cursor: pointer;
      }
      
      form > button:disabled {
          background: dimgrey;
          cursor: not-allowed;
      }
      

      With the stylesheet created, you can link it to the home.html file using static template tags. Open the home.html file:

      • nano ~/djangopush/templates/home.html

      Update the head section to include a link to the external stylesheet:

      ~/djangopush/templates/home.html

      
      {% load static %}
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
          ...
          <link href="http://www.digitalocean.com/{% static"/css/styles.css' %}" rel="stylesheet">
      </head>
      <body>
          ...
      </body>
      </html>
      

      Make sure that you are in your main project directory and start your server again to inspect your work:

      • cd ~/djangopush
      • python manage.py runserver your_server_ip:8000

      When you visit http://your_server_ip:8000, it should look like this:

      Home page view
      Again, you can kill the server with CTRL+C.

      Now that you have successfully created the home.html page and styled it, you can subscribe users to push notifications whenever they visit the home page.

      Step 7 — Registering a Service Worker and Subscribing Users to Push Notifications

      Web push notifications can notify users when there are updates to applications they are subscribed to or prompt them to re-engage with applications they have used in the past. They rely on two technologies, the push API and the notifications API. Both technologies rely on the presence of a service worker.

      A push is invoked when the server provides information to the service worker and the service worker uses the notifications API to display this information.

      We'll subscribe our users to the push and then we'll send the information from the subscription to the server to register them.

      In the static directory, create a folder called js:

      • mkdir ~/djangopush/static/js

      Create a file called registerSw.js:

      • nano ~/djangopush/static/js/registerSw.js

      Add the following code, which checks if service workers are supported on the user's browser before attempting to register a service worker:

      ~/djangopush/static/js/registerSw.js

      
      const registerSw = async () => {
          if ('serviceWorker' in navigator) {
              const reg = await navigator.serviceWorker.register('sw.js');
              initialiseState(reg)
      
          } else {
              showNotAllowed("You can't send push notifications ☹️😢")
          }
      };
      

      First, the registerSw function checks if the browser supports service workers before registering them. After registration, it calls the initializeState function with the registration data. If service workers are not supported in the browser, it calls the showNotAllowed function.

      Next, add the following code below the registerSw function to check if a user is eligible to receive push notifications before attempting to subscribe them:

      ~/djangopush/static/js/registerSw.js

      
      ...
      
      const initialiseState = (reg) => {
          if (!reg.showNotification) {
              showNotAllowed('Showing notifications isn't supported ☹️😢');
              return
          }
          if (Notification.permission === 'denied') {
              showNotAllowed('You prevented us from showing notifications ☹️🤔');
              return
          }
          if (!'PushManager' in window) {
              showNotAllowed("Push isn't allowed in your browser 🤔");
              return
          }
          subscribe(reg);
      }
      
      const showNotAllowed = (message) => {
          const button = document.querySelector('form>button');
          button.innerHTML = `${message}`;
          button.setAttribute('disabled', 'true');
      };
      

      The initializeState function checks the following:

      • Whether or not the user has enabled notifications, using the value of reg.showNotification.
      • Whether or not the user has granted the application permission to display notifications.
      • Whether or not the browser supports the PushManager API.
        If any of these checks fail, the showNotAllowed function is called and the subscription is aborted.

      The showNotAllowed function displays a message on the button and disables it if a user is ineligible to receive notifications. It also displays appropriate messages if a user has restricted the application from displaying notifications or if the browser doesn't support push notifications.

      Once we ensure that a user is eligible to receive push notifications, the next step is to subscribe them using pushManager. Add the following code below the showNotAllowed function:

      ~/djangopush/static/js/registerSw.js

      
      ...
      
      function urlB64ToUint8Array(base64String) {
          const padding = '='.repeat((4 - base64String.length % 4) % 4);
          const base64 = (base64String + padding)
              .replace(/-/g, '+')
              .replace(/_/g, '/');
      
          const rawData = window.atob(base64);
          const outputArray = new Uint8Array(rawData.length);
          const outputData = outputArray.map((output, index) => rawData.charCodeAt(index));
      
          return outputData;
      }
      
      const subscribe = async (reg) => {
          const subscription = await reg.pushManager.getSubscription();
          if (subscription) {
              sendSubData(subscription);
              return;
          }
      
          const vapidMeta = document.querySelector('meta[name="vapid-key"]');
          const key = vapidMeta.content;
          const options = {
              userVisibleOnly: true,
              // if key exists, create applicationServerKey property
              ...(key && {applicationServerKey: urlB64ToUint8Array(key)})
          };
      
          const sub = await reg.pushManager.subscribe(options);
          sendSubData(sub)
      };
      

      Calling the pushManager.getSubscription function returns the data for an active subscription. When an active subscription exists, the sendSubData function is called with the subscription info passed in as a parameter.

      When no active subscription exists, the VAPID public key, which is Base64 URL-safe encoded, is converted to a Uint8Array using the urlB64ToUint8Array function. pushManager.subscribe is then called with the VAPID public key and the userVisible value as options. You can read more about the available options here.

      After successfully subscribing a user, the next step is to send the subscription data to the server. The data will be sent to the webpush/save_information endpoint provided by the django-webpush package. Add the following code below the subscribe function:

      ~/djangopush/static/js/registerSw.js

      
      ...
      
      const sendSubData = async (subscription) => {
          const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase();
          const data = {
              status_type: 'subscribe',
              subscription: subscription.toJSON(),
              browser: browser,
          };
      
          const res = await fetch('/webpush/save_information', {
              method: 'POST',
              body: JSON.stringify(data),
              headers: {
                  'content-type': 'application/json'
              },
              credentials: "include"
          });
      
          handleResponse(res);
      };
      
      const handleResponse = (res) => {
          console.log(res.status);
      };
      
      registerSw();
      

      The save_information endpoint requires information about the status of the subscription (subscribe and unsubscribe), the subscription data, and the browser. Finally, we call the registerSw() function to begin the process of subscribing the user.

      The completed file looks like this:

      ~/djangopush/static/js/registerSw.js

      
      const registerSw = async () => {
          if ('serviceWorker' in navigator) {
              const reg = await navigator.serviceWorker.register('sw.js');
              initialiseState(reg)
      
          } else {
              showNotAllowed("You can't send push notifications ☹️😢")
          }
      };
      
      const initialiseState = (reg) => {
          if (!reg.showNotification) {
              showNotAllowed('Showing notifications isn't supported ☹️😢');
              return
          }
          if (Notification.permission === 'denied') {
              showNotAllowed('You prevented us from showing notifications ☹️🤔');
              return
          }
          if (!'PushManager' in window) {
              showNotAllowed("Push isn't allowed in your browser 🤔");
              return
          }
          subscribe(reg);
      }
      
      const showNotAllowed = (message) => {
          const button = document.querySelector('form>button');
          button.innerHTML = `${message}`;
          button.setAttribute('disabled', 'true');
      };
      
      function urlB64ToUint8Array(base64String) {
          const padding = '='.repeat((4 - base64String.length % 4) % 4);
          const base64 = (base64String + padding)
              .replace(/-/g, '+')
              .replace(/_/g, '/');
      
          const rawData = window.atob(base64);
          const outputArray = new Uint8Array(rawData.length);
          const outputData = outputArray.map((output, index) => rawData.charCodeAt(index));
      
          return outputData;
      }
      
      const subscribe = async (reg) => {
          const subscription = await reg.pushManager.getSubscription();
          if (subscription) {
              sendSubData(subscription);
              return;
          }
      
          const vapidMeta = document.querySelector('meta[name="vapid-key"]');
          const key = vapidMeta.content;
          const options = {
              userVisibleOnly: true,
              // if key exists, create applicationServerKey property
              ...(key && {applicationServerKey: urlB64ToUint8Array(key)})
          };
      
          const sub = await reg.pushManager.subscribe(options);
          sendSubData(sub)
      };
      
      const sendSubData = async (subscription) => {
          const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase();
          const data = {
              status_type: 'subscribe',
              subscription: subscription.toJSON(),
              browser: browser,
          };
      
          const res = await fetch('/webpush/save_information', {
              method: 'POST',
              body: JSON.stringify(data),
              headers: {
                  'content-type': 'application/json'
              },
              credentials: "include"
          });
      
          handleResponse(res);
      };
      
      const handleResponse = (res) => {
          console.log(res.status);
      };
      
      registerSw();
      

      Next, add a script tag for the registerSw.js file in home.html. Open the file:

      • nano ~/djangopush/templates/home.html

      Add the script tag before the closing tag of the body element:

      ~/djangopush/templates/home.html

      
      {% load static %}
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
         ...
      </head>
      <body>
         ...
         <script src="https://www.digitalocean.com/{% static"/js/registerSw.js' %}"></script>
      </body>
      </html>
      

      Because a service worker doesn't yet exist, if you left your application running or tried to start it again, you would see an error message. Let's fix this by creating a service worker.

      Step 8 — Creating a Service Worker

      To display a push notification, you'll need an active service worker installed on your application's home page. We'll create a service worker that listens for push events and displays the messages when ready.

      Because we want the scope of the service worker to be the entire domain, we will need to install it in the application's root. You can read more about the process in this article outlining how to register a service worker. Our approach will be to create a sw.js file in the templates folder, which we will then register as a view.

      Create the file:

      • nano ~/djangopush/templates/sw.js

      Add the following code, which tells the service worker to listen for push events:

      ~/djangopush/templates/sw.js

      
      // Register event listener for the 'push' event.
      self.addEventListener('push', function (event) {
          // Retrieve the textual payload from event.data (a PushMessageData object).
          // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation
          // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData.
          const eventInfo = event.data.text();
          const data = JSON.parse(eventInfo);
          const head = data.head || 'New Notification 🕺🕺';
          const body = data.body || 'This is default content. Your notification didn't have one 🙄🙄';
      
          // Keep the service worker alive until the notification is created.
          event.waitUntil(
              self.registration.showNotification(head, {
                  body: body,
                  icon: 'https://i.imgur.com/MZM3K5w.png'
              })
          );
      });
      

      The service worker listens for a push event. In the callback function, the event data is converted to text. We use default title and body strings if the event data doesn't have them. The showNotification function takes the notification title, the header of the notification to be displayed, and an options object as parameters. The options object contains several properties to configure the visual options of a notification.

      For your service worker to work for the entirety of your domain, you will need to install it in the root of the application. We'll use TemplateView to allow the service worker access to the whole domain.

      Open the urls.py file:

      • nano ~/djangopush/djangopush/urls.py

      Add a new import statement and path in the urlpatterns list to create a class-based view:

      ~/djangopush/djangopush/urls.py

      ...
      from django.views.generic import TemplateView
      
      urlpatterns = [
                        ...,
                        path('sw.js', TemplateView.as_view(template_name='sw.js', content_type='application/x-javascript'))
                    ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
      

      Class-based views like TemplateView allow you to create flexible, reusable views. In this case, the TemplateView.as_view method creates a path for the service worker by passing the recently created service worker as a template and application/x-javascript as the content_type of the template.

      You have now created a service worker and registered it as a route. Next, you'll set up the form on the home page to send push notifications.

      Step 9 — Sending Push Notifications

      Using the form on the home page, users should be able to send push notifications while your server is running. You can also send push notifications using any RESTful service like Postman. When the user sends push notifications from the form on the home page, the data will include a head and body, as well as the id of the receiving user. The data should be structured in the following manner:

      {
          head: "Title of the notification",
          body: "Notification body",
          id: "User's id"
      }
      

      To listen for the submit event of the form and send the data entered by the user to the server, we will create a file called site.js in the ~/djangopush/static/js directory.

      Open the file:

      • nano ~/djangopush/static/js/site.js

      First, add a submit event listener to the form that will enable you to get the values of the form inputs and the user id stored in the meta tag of your template:

      ~/djangopush/static/js/site.js

      
      const pushForm = document.getElementById('send-push__form');
      const errorMsg = document.querySelector('.error');
      
      pushForm.addEventListener('submit', async function (e) {
          e.preventDefault();
          const input = this[0];
          const textarea = this[1];
          const button = this[2];
          errorMsg.innerText = '';
      
          const head = input.value;
          const body = textarea.value;
          const meta = document.querySelector('meta[name="user_id"]');
          const id = meta ? meta.content : null;
          ...
          // TODO: make an AJAX request to send notification
      });
      

      The pushForm function gets the input, textarea, and button inside the form. It also gets the information from the meta tag, including the name attribute user_id and the user's id stored in the content attribute of the tag. With this information, it can send a POST request to the /send_push endpoint on the server.

      To send requests to the server, we'll use the native Fetch API. We're using Fetch here because it is supported by most browsers and doesn't require external libraries to function. Below the code you've added, update the pushForm function to include the code for sending AJAX requests:

      ~/djangopush/static/js/site.js

      const pushForm = document.getElementById('send-push__form');
      const errorMsg = document.querySelector('.error');
      
      pushForm.addEventListener('submit', async function (e) {
           ...
          const id = meta ? meta.content : null;
      
           if (head && body && id) {
              button.innerText = 'Sending...';
              button.disabled = true;
      
              const res = await fetch('/send_push', {
                  method: 'POST',
                  body: JSON.stringify({head, body, id}),
                  headers: {
                      'content-type': 'application/json'
                  }
              });
              if (res.status === 200) {
                  button.innerText = 'Send another 😃!';
                  button.disabled = false;
                  input.value = '';
                  textarea.value = '';
              } else {
                  errorMsg.innerText = res.message;
                  button.innerText = 'Something broke 😢..  Try again?';
                  button.disabled = false;
              }
          }
          else {
              let error;
              if (!head || !body){
                  error = 'Please ensure you complete the form 🙏🏾'
              }
              else if (!id){
                  error = "Are you sure you're logged in? 🤔. Make sure! 👍🏼"
              }
              errorMsg.innerText = error;
          }
      });
      

      If the three required parameters head, body, and id are present, we send the request and disable the submit button temporarily.

      The completed file looks like this:

      ~/djangopush/static/js/site.js

      const pushForm = document.getElementById('send-push__form');
      const errorMsg = document.querySelector('.error');
      
      pushForm.addEventListener('submit', async function (e) {
          e.preventDefault();
          const input = this[0];
          const textarea = this[1];
          const button = this[2];
          errorMsg.innerText = '';
      
          const head = input.value;
          const body = textarea.value;
          const meta = document.querySelector('meta[name="user_id"]');
          const id = meta ? meta.content : null;
      
          if (head && body && id) {
              button.innerText = 'Sending...';
              button.disabled = true;
      
              const res = await fetch('/send_push', {
                  method: 'POST',
                  body: JSON.stringify({head, body, id}),
                  headers: {
                      'content-type': 'application/json'
                  }
              });
              if (res.status === 200) {
                  button.innerText = 'Send another 😃!';
                  button.disabled = false;
                  input.value = '';
                  textarea.value = '';
              } else {
                  errorMsg.innerText = res.message;
                  button.innerText = 'Something broke 😢..  Try again?';
                  button.disabled = false;
              }
          }
          else {
              let error;
              if (!head || !body){
                  error = 'Please ensure you complete the form 🙏🏾'
              }
              else if (!id){
                  error = "Are you sure you're logged in? 🤔. Make sure! 👍🏼"
              }
              errorMsg.innerText = error;
          }    
      });
      

      Finally, add the site.js file to home.html:

      • nano ~/djangopush/templates/home.html

      Add the script tag:

      ~/djangopush/templates/home.html

      
      {% load static %}
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
         ...
      </head>
      <body>
         ...
         <script src="https://www.digitalocean.com/{% static"/js/site.js' %}"></script>
      </body>
      </html>
      

      At this point, if you left your application running or tried to start it again, you would see an error, since service workers can only function in secure domains or on localhost. In the next step we'll use ngrok to create a secure tunnel to our web server.

      Step 10 — Creating a Secure Tunnel to Test the Application

      Service workers require secure connections to function on any site except localhost since they can allow connections to be hijacked and responses to be filtered and fabricated. For this reason, we'll create a secure tunnel for our server with ngrok.

      Open a second terminal window and ensure you're in your home directory:

      If you started with a clean 18.04 server in the prerequisites, then you will need to install unzip:

      • sudo apt update && sudo apt install unzip

      Download ngrok:

      • wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
      • unzip ngrok-stable-linux-amd64.zip

      Move ngrok to /usr/local/bin, so that you will have access to the ngrok command from the terminal:

      • sudo mv ngrok /usr/local/bin

      In your first terminal window, make sure that you are in your project directory and start your server:

      • cd ~/djangopush
      • python manage.py runserver your_server_ip:8000

      You will need to do this before creating a secure tunnel for your application.

      In your second terminal window, navigate to your project folder, and activate your virtual environment:

      • cd ~/djangopush
      • source my_env/bin/activate

      Create the secure tunnel to your application:

      • ngrok http your_server_ip:8000

      You will see the following output, which includes information about your secure ngrok URL:

      Output

      ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Session Expires 7 hours, 59 minutes Version 2.2.8 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://ngrok_secure_url -> 203.0.113.0:8000 Forwarding https://ngrok_secure_url -> 203.0.113.0:8000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

      Copy the ngrok_secure_url from the console output. You will need to add it to the list of ALLOWED_HOSTS in your settings.py file.

      Open another terminal window, navigate to your project folder, and activate your virtual environment:

      • cd ~/djangopush
      • source my_env/bin/activate

      Open the settings.py file:

      • nano ~/djangopush/djangopush/settings.py

      Update the list of ALLOWED_HOSTS with the ngrok secure tunnel:

      ~/djangopush/djangopush/settings.py

      ...
      
      ALLOWED_HOSTS = ['your_server_ip', 'ngrok_secure_url']
      ...
      
      

      Navigate to the secure admin page to log in: https://ngrok_secure_url/admin/. You will see a screen that looks like this:

      ngrok admin login

      Enter your Django admin user information on this screen. This should be the same information you entered when you logged into the admin interface in the prerequisite steps. You are now ready to send push notifications.

      Visit https://ngrok_secure_url in your browser. You will see a prompt asking for permission to display notifications. Click the Allow button to let your browser display push notifications:

      push notifications request

      Submitting a filled form will display a notification similar to this:

      screenshot of notification

      Note: Be sure that your server is running before attempting to send notifications.

      If you received notifications then your application is working as expected.

      You have created a web application that triggers push notifications on the server and, with the help of service workers, receives and displays notifications. You also went through the steps of obtaining the VAPID keys that are required to send push notifications from an application server.

      Conclusion

      In this tutorial, you've learned how to subscribe users to push notifications, install service workers, and display push notifications using the notifications API.

      You can go even further by configuring the notifications to open specific areas of your application when clicked. The source code for this tutorial can be found here.



      Source link

      How To Serve Flask Applications with uWSGI and Nginx on Ubuntu 18.04


      Introduction

      In this guide, you will build a Python application using the Flask microframework on Ubuntu 18.04. The bulk of this article will be about how to set up the uWSGI application server and how to launch the application and configure Nginx to act as a front-end reverse proxy.

      Prerequisites

      Before starting this guide, you should have:

      • A server with Ubuntu 18.04 installed and a non-root user with sudo privileges. Follow our initial server setup guide for guidance.
      • Nginx installed, following Steps 1 and 2 of How To Install Nginx on Ubuntu 18.04.
      • A domain name configured to point to your server. You can purchase one on Namecheap or get one for free on Freenom. You can learn how to point domains to DigitalOcean by following the relevant documentation on domains and DNS. Be sure to create the following DNS records:

        • An A record with your_domain pointing to your server’s public IP address.
        • An A record with www.your_domain pointing to your server’s public IP address.
      • Familiarity with uWSGI, our application server, and the WSGI specification. This discussion of definitions and concepts goes over both in detail.

      Step 1 — Installing the Components from the Ubuntu Repositories

      Our first step will be to install all of the pieces that we need from the Ubuntu repositories. We will install pip, the Python package manager, to manage our Python components. We will also get the Python development files necessary to build uWSGI.

      First, let’s update the local package index and install the packages that will allow us to build our Python environment. These will include python3-pip, along with a few more packages and development tools necessary for a robust programming environment:

      • sudo apt update
      • sudo apt install python3-pip python3-dev build-essential libssl-dev libffi-dev python3-setuptools

      With these packages in place, let’s move on to creating a virtual environment for our project.

      Step 2 — Creating a Python Virtual Environment

      Next, we’ll set up a virtual environment in order to isolate our Flask application from the other Python files on the system.

      Start by installing the python3-venv package, which will install the venv module:

      • sudo apt install python3-venv

      Next, let’s make a parent directory for our Flask project. Move into the directory after you create it:

      • mkdir ~/myproject
      • cd ~/myproject

      Create a virtual environment to store your Flask project’s Python requirements by typing:

      • python3.6 -m venv myprojectenv

      This will install a local copy of Python and pip into a directory called myprojectenv within your project directory.

      Before installing applications within the virtual environment, you need to activate it. Do so by typing:

      • source myprojectenv/bin/activate

      Your prompt will change to indicate that you are now operating within the virtual environment. It will look something like this (myprojectenv)user@host:~/myproject$.

      Step 3 — Setting Up a Flask Application

      Now that you are in your virtual environment, you can install Flask and uWSGI and get started on designing your application.

      First, let’s install wheel with the local instance of pip to ensure that our packages will install even if they are missing wheel archives:

      Note


      Regardless of which version of Python you are using, when the virtual environment is activated, you should use the pip command (not pip3).

      Next, let's install Flask and uWSGI:

      Creating a Sample App

      Now that you have Flask available, you can create a simple application. Flask is a microframework. It does not include many of the tools that more full-featured frameworks might, and exists mainly as a module that you can import into your projects to assist you in initializing a web application.

      While your application might be more complex, we'll create our Flask app in a single file, called myproject.py:

      • nano ~/myproject/myproject.py

      The application code will live in this file. It will import Flask and instantiate a Flask object. You can use this to define the functions that should be run when a specific route is requested:

      ~/myproject/myproject.py

      from flask import Flask
      app = Flask(__name__)
      
      @app.route("/")
      def hello():
          return "<h1 style='color:blue'>Hello There!</h1>"
      
      if __name__ == "__main__":
          app.run(host='0.0.0.0')
      

      This basically defines what content to present when the root domain is accessed. Save and close the file when you're finished.

      If you followed the initial server setup guide, you should have a UFW firewall enabled. To test the application, you need to allow access to port 5000:

      Now, you can test your Flask app by typing:

      You will see output like the following, including a helpful warning reminding you not to use this server setup in production:

      Output

      * Serving Flask app "myproject" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: off * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

      Visit your server's IP address followed by :5000 in your web browser:

      http://your_server_ip:5000
      

      You should see something like this:

      Flask sample app

      When you are finished, hit CTRL-C in your terminal window to stop the Flask development server.

      Creating the WSGI Entry Point

      Next, let's create a file that will serve as the entry point for our application. This will tell our uWSGI server how to interact with it.

      Let's call the file wsgi.py:

      In this file, let's import the Flask instance from our application and then run it:

      ~/myproject/wsgi.py

      from myproject import app
      
      if __name__ == "__main__":
          app.run()
      

      Save and close the file when you are finished.

      Step 4 — Configuring uWSGI

      Your application is now written with an entry point established. We can now move on to configuring uWSGI.

      Testing uWSGI Serving

      Let's test to make sure that uWSGI can serve our application.

      We can do this by simply passing it the name of our entry point. This is constructed by the name of the module (minus the .py extension) plus the name of the callable within the application. In our case, this is wsgi:app.

      Let's also specify the socket, so that it will be started on a publicly available interface, as well as the protocol, so that it will use HTTP instead of the uwsgi binary protocol. We'll use the same port number, 5000, that we opened earlier:

      • uwsgi --socket 0.0.0.0:5000 --protocol=http -w wsgi:app

      Visit your server's IP address with :5000 appended to the end in your web browser again:

      http://your_server_ip:5000
      

      You should see your application's output again:

      Flask sample app

      When you have confirmed that it's functioning properly, press CTRL-C in your terminal window.

      We're now done with our virtual environment, so we can deactivate it:

      Any Python commands will now use the system's Python environment again.

      Creating a uWSGI Configuration File

      You have tested that uWSGI is able to serve your application, but ultimately you will want something more robust for long-term usage. You can create a uWSGI configuration file with the relevant options for this.

      Let's place that file in our project directory and call it myproject.ini:

      • nano ~/myproject/myproject.ini

      Inside, we will start off with the [uwsgi] header so that uWSGI knows to apply the settings. We'll specify two things: the module itself, by referring to the wsgi.py file minus the extension, and the callable within the file, app:

      ~/myproject/myproject.ini

      [uwsgi]
      module = wsgi:app
      

      Next, we'll tell uWSGI to start up in master mode and spawn five worker processes to serve actual requests:

      ~/myproject/myproject.ini

      [uwsgi]
      module = wsgi:app
      
      master = true
      processes = 5
      

      When you were testing, you exposed uWSGI on a network port. However, you're going to be using Nginx to handle actual client connections, which will then pass requests to uWSGI. Since these components are operating on the same computer, a Unix socket is preferable because it is faster and more secure. Let's call the socket myproject.sock and place it in this directory.

      Let's also change the permissions on the socket. We'll be giving the Nginx group ownership of the uWSGI process later on, so we need to make sure the group owner of the socket can read information from it and write to it. We will also clean up the socket when the process stops by adding the vacuum option:

      ~/myproject/myproject.ini

      [uwsgi]
      module = wsgi:app
      
      master = true
      processes = 5
      
      socket = myproject.sock
      chmod-socket = 660
      vacuum = true
      

      The last thing we'll do is set the die-on-term option. This can help ensure that the init system and uWSGI have the same assumptions about what each process signal means. Setting this aligns the two system components, implementing the expected behavior:

      ~/myproject/myproject.ini

      [uwsgi]
      module = wsgi:app
      
      master = true
      processes = 5
      
      socket = myproject.sock
      chmod-socket = 660
      vacuum = true
      
      die-on-term = true
      

      You may have noticed that we did not specify a protocol like we did from the command line. That is because by default, uWSGI speaks using the uwsgi protocol, a fast binary protocol designed to communicate with other servers. Nginx can speak this protocol natively, so it's better to use this than to force communication by HTTP.

      When you are finished, save and close the file.

      Step 5 — Creating a systemd Unit File

      Next, let's create the systemd service unit file. Creating a systemd unit file will allow Ubuntu's init system to automatically start uWSGI and serve the Flask application whenever the server boots.

      Create a unit file ending in .service within the /etc/systemd/system directory to begin:

      • sudo nano /etc/systemd/system/myproject.service

      Inside, we'll start with the [Unit] section, which is used to specify metadata and dependencies. Let's put a description of our service here and tell the init system to only start this after the networking target has been reached:

      /etc/systemd/system/myproject.service

      [Unit]
      Description=uWSGI instance to serve myproject
      After=network.target
      

      Next, let's open up the [Service] section. This will specify the user and group that we want the process to run under. Let's give our regular user account ownership of the process since it owns all of the relevant files. Let's also give group ownership to the www-data group so that Nginx can communicate easily with the uWSGI processes. Remember to replace the username here with your username:

      /etc/systemd/system/myproject.service

      [Unit]
      Description=uWSGI instance to serve myproject
      After=network.target
      
      [Service]
      User=sammy
      Group=www-data
      

      Next, let's map out the working directory and set the PATH environmental variable so that the init system knows that the executables for the process are located within our virtual environment. Let's also specify the command to start the service. Systemd requires that we give the full path to the uWSGI executable, which is installed within our virtual environment. We will pass the name of the .ini configuration file we created in our project directory.

      Remember to replace the username and project paths with your own information:

      /etc/systemd/system/myproject.service

      [Unit]
      Description=uWSGI instance to serve myproject
      After=network.target
      
      [Service]
      User=sammy
      Group=www-data
      WorkingDirectory=/home/sammy/myproject
      Environment="PATH=/home/sammy/myproject/myprojectenv/bin"
      ExecStart=/home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini
      

      Finally, let's add an [Install] section. This will tell systemd what to link this service to if we enable it to start at boot. We want this service to start when the regular multi-user system is up and running:

      /etc/systemd/system/myproject.service

      [Unit]
      Description=uWSGI instance to serve myproject
      After=network.target
      
      [Service]
      User=sammy
      Group=www-data
      WorkingDirectory=/home/sammy/myproject
      Environment="PATH=/home/sammy/myproject/myprojectenv/bin"
      ExecStart=/home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini
      
      [Install]
      WantedBy=multi-user.target
      

      With that, our systemd service file is complete. Save and close it now.

      We can now start the uWSGI service we created and enable it so that it starts at boot:

      • sudo systemctl start myproject
      • sudo systemctl enable myproject

      Let's check the status:

      • sudo systemctl status myproject

      You should see output like this:

      Output

      ● myproject.service - uWSGI instance to serve myproject Loaded: loaded (/etc/systemd/system/myproject.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2018-07-13 14:28:39 UTC; 46s ago Main PID: 30360 (uwsgi) Tasks: 6 (limit: 1153) CGroup: /system.slice/myproject.service ├─30360 /home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini ├─30378 /home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini ├─30379 /home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini ├─30380 /home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini ├─30381 /home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini └─30382 /home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini

      If you see any errors, be sure to resolve them before continuing with the tutorial.

      Step 6 — Configuring Nginx to Proxy Requests

      Our uWSGI application server should now be up and running, waiting for requests on the socket file in the project directory. Let's configure Nginx to pass web requests to that socket using the uwsgi protocol.

      Begin by creating a new server block configuration file in Nginx's sites-available directory. Let's call this myproject to keep in line with the rest of the guide:

      • sudo nano /etc/nginx/sites-available/myproject

      Open up a server block and tell Nginx to listen on the default port 80. Let's also tell it to use this block for requests for our server's domain name:

      /etc/nginx/sites-available/myproject

      server {
          listen 80;
          server_name your_domain www.your_domain;
      }
      

      Next, let's add a location block that matches every request. Within this block, we'll include the uwsgi_params file that specifies some general uWSGI parameters that need to be set. We'll then pass the requests to the socket we defined using the uwsgi_pass directive:

      /etc/nginx/sites-available/myproject

      server {
          listen 80;
          server_name your_domain www.your_domain;
      
          location / {
              include uwsgi_params;
              uwsgi_pass unix:/home/sammy/myproject/myproject.sock;
          }
      }
      

      Save and close the file when you're finished.

      To enable the Nginx server block configuration you've just created, link the file to the sites-enabled directory:

      • sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled

      With the file in that directory, we can test for syntax errors by typing:

      If this returns without indicating any issues, restart the Nginx process to read the new configuration:

      • sudo systemctl restart nginx

      Finally, let's adjust the firewall again. We no longer need access through port 5000, so we can remove that rule. We can then allow access to the Nginx server:

      • sudo ufw delete allow 5000
      • sudo ufw allow 'Nginx Full'

      You should now be able to navigate to your server's domain name in your web browser:

      http://your_domain
      

      You should see your application output:

      Flask sample app

      If you encounter any errors, trying checking the following:

      • sudo less /var/log/nginx/error.log: checks the Nginx error logs.
      • sudo less /var/log/nginx/access.log: checks the Nginx access logs.
      • sudo journalctl -u nginx: checks the Nginx process logs.
      • sudo journalctl -u myproject: checks your Flask app's uWSGI logs.

      Step 7 — Securing the Application

      To ensure that traffic to your server remains secure, let's get an SSL certificate for your domain. There are multiple ways to do this, including getting a free certificate from Let's Encrypt, generating a self-signed certificate, or buying one from another provider and configuring Nginx to use it by following Steps 2 through 6 of  How to Create a Self-signed SSL Certificate for Nginx in Ubuntu 18.04. We will go with option one for the sake of expediency.

      First, add the Certbot Ubuntu repository:

      • sudo add-apt-repository ppa:certbot/certbot

      You'll need to press ENTER to accept.

      Next, install Certbot's Nginx package with apt:

      • sudo apt install python-certbot-nginx

      Certbot provides a variety of ways to obtain SSL certificates through plugins. The Nginx plugin will take care of reconfiguring Nginx and reloading the config whenever necessary. To use this plugin, type the following:

      • sudo certbot --nginx -d your_domain -d www.your_domain

      This runs certbot with the --nginx plugin, using -d to specify the names we'd like the certificate to be valid for.

      If this is your first time running certbot, you will be prompted to enter an email address and agree to the terms of service. After doing so, certbot will communicate with the Let's Encrypt server, then run a challenge to verify that you control the domain you're requesting a certificate for.

      If that's successful, certbot will ask how you'd like to configure your HTTPS settings.

      Output

      Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access. ------------------------------------------------------------------------------- 1: No redirect - Make no further changes to the webserver configuration. 2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for new sites, or if you're confident your site works on HTTPS. You can undo this change by editing your web server's configuration. ------------------------------------------------------------------------------- Select the appropriate number [1-2] then [enter] (press 'c' to cancel):

      Select your choice then hit ENTER. The configuration will be updated, and Nginx will reload to pick up the new settings. certbot will wrap up with a message telling you the process was successful and where your certificates are stored:

      Output

      IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain/privkey.pem Your cert will expire on 2018-07-23. To obtain a new or tweaked version of this certificate in the future, simply run certbot again with the "certonly" option. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le

      If you followed the Nginx installation instructions in the prerequisites, you will no longer need the redundant HTTP profile allowance:

      • sudo ufw delete allow 'Nginx HTTP'

      To verify the configuration, let's navigate once again to your domain, using https://:

      https://your_domain
      

      You should see your application output once again, along with your browser's security indicator, which should indicate that the site is secured.

      Conclusion

      In this guide, you created and secured a simple Flask application within a Python virtual environment. You created a WSGI entry point so that any WSGI-capable application server can interface with it, and then configured the uWSGI app server to provide this function. Afterwards, you created a systemd service file to automatically launch the application server on boot. You also created an Nginx server block that passes web client traffic to the application server, relaying external requests, and secured traffic to your server with Let's Encrypt.

      Flask is a very simple, but extremely flexible framework meant to provide your applications with functionality without being too restrictive about structure and design. You can use the general stack described in this guide to serve the flask applications that you design.



      Source link