declension / squeeze-alexa

Squeezebox integration for Amazon Alexa
GNU General Public License v3.0
59 stars 20 forks source link

IDEA: Use JSONRPC for server interactions and Apache as reverse proxy #120

Open philchillbill opened 5 years ago

philchillbill commented 5 years ago

I've written a personal skill to handle my rather extensive smart home setup, and part of that is controlling my 10 squeezeboxes. Using the Amazon smart home API interfaces Alexa.PlaybackController and Alexa.Speaker, I can currently issue Play, Pause, Next, Previous, Stop, and StartOver transport commands, plus issue SetVolume, AdjustVolume and Mute/Unmute for audio. I'm nearly at the point of having Alexa.RangeController allow me to skip to a specific numbered track. All this is achieved with surprisingly little code due to its being a non-custom skill.

Anyway, there are two aspects of what I've done that I think would benefit squeeze-alexa. First is using the jsonrpc.js interface over port 9000 (internally) rather than the CLI port 9090 stuff. This keeps it all to standard http traffic which is simple. The response from the server each time is JSON that's easy to parse.

Secondly, I've used Apache as a reverse proxy that's brutally easy to set up very securely. With a single external SSL-enabled DNS IP address and port, I actually have 5 internal proxies that map to different servers using HTTP internally. I differentiate between them using the path name rather than ports, with /lms/ getting me to my LMS server. Here's a fingered version of my 000-default.conf file that shows two proxies in parallel use:

  <VirtualHost *:443>

    ServerName myhouse.ddns.com
    SSLEngine on
    Header always set Referrer-Policy "same-origin"
    Header always append X-Frame-Options SAMEORIGIN

    ProxyPass /lms/ http://192.168.1.3:9000/
    ProxyPassReverse /lms/ http://192.168.1.3:9000/

    ProxyPass /kodi/ http://192.168.1.5:8080/
    ProxyPassReverse /kodi/ http://192.168.1.5:8080/

    SSLCertificateFile /etc/letsencrypt/live/myhouse.ddns.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/myhouse.ddns.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/myhouse.ddns.com/chain.pem

    <Location />
      AuthType Basic
      AuthName "joebloggs"
      AuthUserFile /etc/apache2/.htpasswd
      Require valid-user
    </Location>

    <Directory "/">
        Require all denied
    </Directory>

  </VirtualHost>

With this, an external address of https://myhouse.ddns.com:6666/lms/ talks to my internal LMS server on port 9000 and an external address of https://myhouse.ddns.com:6666/kodi/ talks to my Kodi server on port 8080 on a different machine. Assume for this that my router maps external port 6666 to internal port 443 on the machine running my Apache server.

In reality, I prepend the /lms/ path-name with a 128-bit UUID (e.g. 82d1fb90-1a7f-4dd5-8bc2-0f9c140f043b) and also have a UUID as password for the basic-auth part, so somebody guessing the 'myhouse' part of my DNS name is not a risk. Remember, with your stunnel setup, just knowing https://myhouse.ddns.com:19090 gets an attacker face to face with LMS. In my case, he has to know https://myhouse.ddns.com:6666/82d1fb90-1a7f-4dd5-8bc2-0f9c140f043b/lms/. The 'Require all denied' means it's impossible to get a directory listing from the server, so you need to know the UUID and can't just ask for a listing. The basic auth login applies at the Apache levels and not LMS, so it's the same username & password for all my internal servers. Having a (different) UUID as password means no dictionary attacks.

The python code that handles the POST to the jsonrpc.js endpoint from my skill looks like this

def apiLMS(self, query):
    url = self.url + '/lms/jsonrpc.js' + query
    headers = { 'Content-Type': 'application/json' }        
    if self.authorization is not None:
        headers['Authorization'] = self.authorization
    data = query.encode("utf-8") # needed for POST         
    payload = urlopen(Request(url, data, headers)).read()
    return json.loads(payload.decode('utf-8'))

A call to this method to e.g. StartOver (go to track 1 of the playlist) is simple JSON

def lmsStartOver(self, playerid):    
    self.apiLMS('{"id":1,"method":"slim.request","params":["'+str(playerid)+'",["playlist","index",0]]}')

I store the playerid (MAC address) in an endpoint cookie during discovery.

In summary, it's very, very simple yet very, very safe, it has no timeout/session-persistence balancing issues, and there's no noticeable latency. Should be really easy for you to implement these thoughts in squeeze-alexa too.

Regards, Phil

declension commented 5 years ago

Thanks for the detailed post @philchillbill. It feels like two separate issues:

  1. JSONRPC was brought up a couple of years ago actually (#3). I agree definitely a more modern approach. The lack of pipelining support is a shame though, and I suspect there is extra latency at both ends due to connection opening (and perhaps parsing, though negligible perhaps). Perhaps this should become a new issue (or this one could become it), so that if someone wants to rewrite the current interface (or fork) then it'd provide a starting place.

  2. Apache as SSL termination / proxy hasn't been covered; HAProxy (#3), nginx (#47) and stunnel are currrent options. Working / example config and README for (stream-level, for now) SSL proxying with Apache too would be great though, please submit a PR if you can!

Side note:

Remember, with your stunnel setup, just knowing https://myhouse.ddns.com:19090 gets an attacker face to face with LMS.

No - perhaps you misunderstood how the setup works. The URL is assumed to be known (as is, say, https://api.github.com) - relying on obscurity for security is not advisable of course, so mutual x509 authentication is used here. The connection will be dropped before the application layer (i.e. LMS, or HTTP in other circumstances) is even started if you fail authentication. A few nice additional benefits for

  1. latency: SSL session caching, using optimised low-level code, and
  2. security: primarily avoiding the simpler application level auth protocols, e.g. HTTP basic auth (and LMS auth), that don't protect against replay attacks etc
philchillbill commented 5 years ago

The jsonrpc latency is truly negligible, trust me. I have local scripts running in my smart home setup that send ten jsonrpc commands in sequence to play TTS announcements and it feels instantaneous.

In my case, obscurity is merely an additional benefit - it's not relied upon solely (I also use SSL with a Letsencrypt cert, plus basic auth). But I indeed don't understand you referencing x509 in the context of authentication here. The comms between Alexa and the skill Lambda are x509 authenticated (code-signing), sure. But when it leaves your lambda and heads off on the internet to your house, surely it's only x509 TLS protection and not x509 authentication that's then involved? Where in stunnel setup do you check the authentication?