Serving Alexa Smart Home skills on your own device or server
For creating Alexa skills, you have to implement an AWS Lambda function that is invoked for every request a user makes to Alexa (that involves your skill) in order to carry out your intended actions and possibly return a response to the user.
Now the usual »Cloud« scenario is that your Smart home devices connect to the Cloud of the manufacturer and are always connected to their servers. The Lambda function would then talk to an internal service of the manufacturer in order to relay your Alexa requests to your devices.
I don’t want that.
If you want to control your Smart Home devices locally but still use Alexa for voice control, you need get the Alexa requests that go to your skills’ Lambda function into your local network – either process in Lambda before triggering actions or just pass them to your own »Alexa service« in your home network that does that for you.
There are multiple ways to achieve that:
- Open your Smart Home controller services up to the internet (with authentication, of course) in your home router. This would probably the easiest solution, but you would have to that for every service you want to control through Alexa. Also you would implement all the control logic in Lambda – which can be annoying to debug sometimes.
- Create a full-scale VPC with a VPN gateway that connects to your home network. Overkill. You would also pay for an EC2 instance that acts merely as a proxy. Now you can forward your requests to your in-home »Alexa service« or process them in Lambda and directly trigger the corresponding actions.
- Pipe all Alexa requests through your home router to an HTTP proxy on, e.g., a Raspberry Pi. This proxy would then authenticate and forward incoming Alexa requests to your local »Alexa service«.
I decided for the last approach.
Setting up nginx on a Raspberry Pi
For the following example I’m using Arch Linux ARM. For other distros, please adapt any paths as needed.
$ sudo pacman -S nginx certbot-nginx
Generate Let’s Encrypt certificates for your proxy
Make sure your router forwards ports 80 and 443 to your Raspberry Pi.
$ sudo certbot --nginx certonly
This will generate valid certificates in /etc/letsencrypt
.
We can create a directory in /etc/nginx/certs
to have easier access to them:
$ mkdir -p /etc/nginx/certs
$ ln -sf /etc/letsencrypt/live/<your domain> /etc/nginx/certs/
Generating a CA and client certificates
Your Lambda function will use a client certificate to authenticate itself against your »Alexa service«.
First we create the CA that signs those certificates:
$ cd /etc/nginx/certs
$ openssl genpkey -out client-ca.key 2048
$ openssl req -new -x509 -years 100 -key client-ca.key -out client-ca.pem
Then we create a client ceriticate for our Lambda function:
$ mkdir -p clients
$ cd clients
$ openssl genrsa -out aws-lambda.key 2048
$ openssl req -new -key aws-lambda.key -out aws-lambda.csr
$ openssl x509 -req -days 365 \
-in aws-lambda.csr \
-CA ../client-ca.pem -CAkey ../client-ca.key \
-set_serial 01 -out aws-lambda.pem
Configure nginx
Add the following to the http
section of your nginx.conf
:
server {
listen 443 ssl;
server_name <your domain>;
# Let's Encrypt server certificate
ssl_certificate /etc/nginx/certs/<your domain>/fullchain.pem;
ssl_trusted_certificate /etc/nginx/certs/<your domain>/chain.pem;
ssl_certificate_key /etc/nginx/certs/<your domain>/privkey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5; # please don't
ssl_prefer_server_ciphers on;
# your own client ceritificate CA
ssl_client_certificate /etc/nginx/certs/client-ca.pem;
ssl_verify_client on;
location / {
proxy_pass http://localhost:4567/;
proxy_set_header X-ClientCert-DN $ssl_client_s_dn;
}
}
Now start your service on port 4567 on your Raspberry Pi. For example, using a minimal Sinatra app:
$ gem install sinatra
main.rb
:
require 'sinatra'
accepted_client_dns = [
# insert certificate DNs here if you want to differentiate / identify different client certificates
]
def proccess_alexa_request(request)
# ...
end
post '/transport' do
# uncomment to enable DN validation:
#raise unless accepted_client_dns.include? request.env['HTTP_X_CLIENTCERT_DN']
request.body.rewind
request_body = JSON.parse(request.body.read)
response_body = process_alexa_request(request_body)
JSON.generate(response_body)
end
$ ruby main.rb
Creating a Lambda function that redirects your Alexa requests
This one is simple. Just create a Lambda function running in the NodeJS environment.
index.js
:
'use strict';
var fs = require('fs');
var https = require('https');
var options = {
hostname: '<your domain>',
port: 443,
path: '/transport',
method: 'POST',
key: fs.readFileSync('aws-lambda.key'),
cert: fs.readFileSync('aws-lambda.pem')
};
// this function will be invoked by Alexa when a user makes a request involving your skill
exports.handler = (request, context, callback) => {
var request_json = JSON.stringify(request);
console.log(`Request: ${request_json}`);
// send request to your local »Alexa service« and wait for a response
var response_json = '';
var req = https.request(options, function(res) {
res.on('data', function(data) {
response_json += data;
});
res.on('end', function() {
console.log(`Response: ${response_json}`);
var response = JSON.parse(response_json);
// pass the response back to Alexa
callback(null, response);
});
});
req.on('error', function(e) {
// oh noes
console.error(e);
callback(new Error(e));
});
req.write(request_json)
req.end();
};
Now add your previously generated aws-lambda.key
and aws-lambda.pem
client key and certificate files to the same folder as index.js
.
There you go. Your requests will start filing into your Alexa service ready for being processed by you.