Secure web stream of a RaspberryPI camera

In this tutorial  I will explain how to stream securely from your RaspberryPI camera to web browser.
The page is an HTTP (unsecured) but no worries, we will make it a secure one in a bit.

After connecting and enabling the PI camera that could be done by these instructions, we will create a:
1. Script that start the camera.
2. Add the script to crontab in order to start automatically when the PI boots.
3. Redirect the insecure stream to a secure one with an SSL using a tool called stunnel.

Let’s go…

First we will create a script that starts the camera, this is a python3 script that creates a webpage and embeds the stream.

create a file called “camera_stream_to_http.py”

sudo nano /home/pi/Desktop/camera/camera_stream_to_http.py

and paste the following code: 

# Web streaming example
# Source code from the official PiCamera package
# http://picamera.readthedocs.io/en/latest/recipes2.html#web-streaming

import io
import picamera
import logging
import socketserver
from threading import Condition
from http import server

PAGE="""\
<html>
<head>
<title>Raspberry Pi - Surveillance Camera</title>
</head>
<body>
<center><h1>Raspberry Pi - Surveillance Camera</h1></center>
<center><img src="stream.mjpg" width="640" height="480"></center>
</body>
</html>
"""

class StreamingOutput(object):
    def __init__(self):
        self.frame = None
        self.buffer = io.BytesIO()
        self.condition = Condition()

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            # New frame, copy the existing buffer's content and notify all
            # clients it's available
            self.buffer.truncate()
            with self.condition:
                self.frame = self.buffer.getvalue()
                self.condition.notify_all()
            self.buffer.seek(0)
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.html')
            self.end_headers()
        elif self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif self.path == '/stream.mjpg':
            self.send_response(200)
            self.send_header('Age', 0)
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning(
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True

with picamera.PiCamera(resolution='640x480', framerate=24) as camera:
    output = StreamingOutput()
    #Uncomment the next line to change your Pi's Camera rotation (in degrees)
    #camera.rotation = 90
    camera.start_recording(output, format='mjpeg')
    try:
        address = ('', 8000)
        server = StreamingServer(address, StreamingHandler)
        server.serve_forever()
    finally:
        camera.stop_recording()

Add the script to crontab in order to start automatically when the PI boots.

sudo crontab -e

Add the following line at the bottom of the file:

@reboot sleep 20 && /home/pi/Desktop/camera/camera_stream_to_http.py

At this point you could view the stream on your browser if you will navigate to http://your-local-ip:8000

But the mission is not yet accomplished, we want to make it a secure stream,
continue reading…

Now we will make our insecure stream a secured one.
After a little search I found a nice utility called stunnel. What it can do is turn any unsecured network service into a secured service. Essentially at the simplest level you can say any network request on port A should map to another service on port B. So, I set the camera to stream on one port (8000) and stunnel routing via another port (443) and finally the port forwarding on my router points to the port that stunnel accepts on (443).

First we’ll install stunnel on the PI:

sudo apt-get install -y stunnel4
sudo systemctl enable stunnel4
sudo systemctl status stunnel4

The configuration file to make this happen with stunnel is very simple.
it takes the unsecure stream on port 8000 and redirects it to port 443 that is protected with an SSL.

sudo nano /etc/stunnel/stunnel.conf
cert = /path-to/cert.pem
key = /path-to/privkey.pem
sslVersion = all
debug = 7

[https]
client = no
accept = 443
connect = 127.0.0.1:8000

From now you could view you camera from external network on https://your-domain:443
But don’t forget to open the port on your router and on your raspberryPI

Of course you could use whatever certificate source you like, but i’ve used LetsEncrypt, this is a free one and good enough for me.

Navigate https://certbot.eff.org/, choose your setup and create your certificate.