├── deconz ├── deconz.override.conf ├── hue-pass.service ├── install.sh ├── hue_certificate.sh ├── hue_openssl.conf ├── README.md └── HuePass.py /deconz: -------------------------------------------------------------------------------- 1 | #PLATFORM=-platform minimal 2 | DISPLAY=:0 3 | -------------------------------------------------------------------------------- /deconz.override.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | EnvironmentFile=/etc/default/deconz 3 | ExecStart= 4 | ExecStart=/usr/bin/deCONZ $PLATFORM --http-port=80 --ws-port=8088 --allow-local=0 --lan-bridgeid=1 5 | -------------------------------------------------------------------------------- /hue-pass.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=HuePass 3 | After=multi-user.target deconz.service deconz-gui.service 4 | 5 | [Service] 6 | Type=idle 7 | Restart=always 8 | RestartSec=30 9 | StartLimitInterval=200 10 | StartLimitBurst=5 11 | 12 | WorkingDirectory=/opt/hue-pass 13 | ExecStart=/opt/hue-pass/HuePass.py 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | apt install -y python3 python3-requests 3 | if [ -f '/lib/systemd/system/deconz.service' ]; then 4 | sed -i 's/ --http-port=80$/ --http-port=80 --ws-port=8088 --allow-local=0 --lan-bridgeid=1/' /lib/systemd/system/deconz.service 5 | fi 6 | if [ -f '/lib/systemd/system/deconz-gui.service' ]; then 7 | sed -i 's/ --http-port=80$/ --http-port=80 --ws-port=8088 --allow-local=0 --lan-bridgeid=1/' /lib/systemd/system/deconz-gui.service 8 | fi 9 | cp hue-pass.service /lib/systemd/system/ 10 | systemctl daemon-reload 11 | systemctl enable hue-pass.service 12 | systemctl start hue-pass.service 13 | -------------------------------------------------------------------------------- /hue_certificate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ ! -f 'hue_certificate.pem' ]; then 3 | bridgeid=$1 4 | dec_bridgeid=`python3 -c "print(int(\"$bridgeid\", 16))"` 5 | openssl req -new -config hue_openssl.conf -nodes -x509 -days 4017 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -subj "/C=NL/O=Philips Hue/CN=$bridgeid" -keyout hue_private.key -out hue_public.crt -set_serial $dec_bridgeid 6 | if [ $? -ne 0 ] ; then 7 | echo -e "\033[31m ERROR!! Local certificate generation failed!\033[0m" 8 | else 9 | cat hue_private.key hue_public.crt > hue_certificate.pem 10 | rm hue_private.key hue_public.crt 11 | fi 12 | else 13 | echo "Certificate already exists" 14 | fi 15 | -------------------------------------------------------------------------------- /hue_openssl.conf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 1024 3 | default_md = sha256 4 | default_keyfile = privkey.pem 5 | distinguished_name = req_distinguished_name 6 | attributes = req_attributes 7 | req_extensions = v3_req 8 | x509_extensions = usr_cert 9 | 10 | 11 | [ usr_cert ] 12 | basicConstraints=critical,CA:FALSE 13 | subjectKeyIdentifier=hash 14 | authorityKeyIdentifier=keyid,issuer 15 | keyUsage = critical, digitalSignature, keyEncipherment 16 | extendedKeyUsage = serverAuth 17 | 18 | [ v3_req ] 19 | extendedKeyUsage = serverAuth, clientAuth, codeSigning, emailProtection 20 | basicConstraints = CA:FALSE 21 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 22 | 23 | [ req_distinguished_name ] 24 | 25 | [ req_attributes ] 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HuePass 2 | This is a Hue REST API wrapper that sets up a Hue app compatible HTTPS server and passes through requests to the HTTP instance. The intent is to enable deCONZ (which doesn't support SSL at the moment) to work with the official Hue app (which requires an SSL connection), but theoratically it should work with any Hue compatible rest API. 3 | 4 | ## Install 5 | ``` 6 | cd /opt 7 | sudo git clone https://github.com/KodeCR/hue-pass 8 | sudo ./install.sh 9 | ``` 10 | 11 | ## Configure 12 | Hue-pass will pass through to the localhost (127.0.0.1) at port 80, and enables a HTTPS service at port 443. This default web-socket port for deCONZ is also at port 443 and needs to be changed. Also localhost access (like HuePass uses) is by default whitelisted for authorisation, this needs to be disabled. I also recommend to change deCONZ to base the bridge-id on the LAN mac-address instead of the Zigbee MAC-address so that the Hue app will work properly with HomeKit. To configure deCONZ for all this please add `--ws-port=8088 --allow-local=0 --lan-bridgeid=1` to the startup parameters for deCONZ using a systemd override. 13 | 14 | The `install.sh` script does the above for you, and installs hue-pass as a (systemd) service. 15 | -------------------------------------------------------------------------------- /HuePass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import logging 3 | import os 4 | import sys 5 | import struct 6 | import random 7 | import threading 8 | import socket 9 | import json 10 | import requests 11 | import ssl 12 | import re 13 | from functools import partial 14 | from threading import Thread 15 | from time import sleep 16 | from subprocess import check_output 17 | from collections import defaultdict 18 | from http.server import BaseHTTPRequestHandler, HTTPServer 19 | from socketserver import ThreadingMixIn 20 | from requests.models import Response 21 | 22 | # setup 23 | def getBridgeIDs(): 24 | r = requests.get('http://127.0.0.1:80/api/nouser/config') 25 | bridgeid = json.loads(r.text).get('bridgeid') 26 | logging.debug("bridge-id: " + str(bridgeid)) 27 | r = requests.get('http://127.0.0.1:80/description.xml') 28 | uuid = re.search(r'uuid:([0-9a-fA-F\-]*)', r.text).group(1) 29 | logging.debug("uuid: " + str(uuid)) 30 | if bridgeid == None or uuid == None: 31 | print("Error: could not retrieve bridgeid/uuid from 127.0.0.1:80") 32 | sys.exit(1) 33 | return (bridgeid, uuid) 34 | 35 | def generateCertificate(bridgeid): 36 | try: 37 | os.system('./hue_certificate.sh ' + bridgeid) 38 | except: 39 | print("Error: couldn't generate certificate") 40 | sys.exit(1) 41 | 42 | # http 43 | class Handler(BaseHTTPRequestHandler): 44 | protocol_version = 'HTTP/1.1' 45 | server_version = 'nginx' 46 | sys_version = '' 47 | 48 | def __init__(self, *args, **kwargs): 49 | super().__init__(*args, **kwargs) 50 | 51 | def _send(self, status_code, headers, content): 52 | self.send_response(status_code) 53 | for key, value in headers.items(): 54 | self.send_header(key, value) 55 | self.end_headers() 56 | if int(headers['Content-Length']) != 0: 57 | self.wfile.write(content) 58 | 59 | def _respond(self, r): 60 | self._send(r.status_code, r.headers, r.content) 61 | 62 | def _update(self, r, content): 63 | r.headers['Content-Length'] = len(content) 64 | self._send(r.status_code, r.headers, content) 65 | 66 | def do_OPTIONS(self): 67 | logging.debug("OPTIONS: " + self.path) 68 | try: 69 | r = requests.options('http://127.0.0.1:80' + self.path) 70 | self._respond(r) 71 | except: 72 | logging.error("OPTIONS: " + self.path) 73 | self._send(500, {"Content-Length": 0}, {}) 74 | 75 | def do_HEAD(self): 76 | logging.debug("HEAD: " + self.path) 77 | try: 78 | r = requests.head('http://127.0.0.1:80' + self.path) 79 | self._respond(r) 80 | except: 81 | logging.error("HEAD: " + self.path) 82 | self._send(500, {"Content-Length": 0}, {}) 83 | 84 | def do_GET(self): 85 | logging.debug("GET: " + self.path) 86 | try: 87 | r = requests.get('http://127.0.0.1:80' + self.path) 88 | self._respond(r) 89 | except: 90 | logging.error("GET: " + self.path) 91 | self._send(500, {"Content-Length": 0}, {}) 92 | 93 | def do_PUT(self): 94 | length = int(self.headers['Content-Length']) 95 | content = self.rfile.read(length) 96 | logging.debug("PUT: " + self.path + ' - ' + str(content)) 97 | try: 98 | r = requests.put('http://127.0.0.1:80' + self.path, data = content) 99 | self._respond(r) 100 | except: 101 | logging.error("PUT: " + self.path + ' - ' + str(content)) 102 | self._send(500, {"Content-Length": 0}, {}) 103 | 104 | def do_POST(self): 105 | length = int(self.headers['Content-Length']) 106 | content = self.rfile.read(length) 107 | logging.debug("POST: " + self.path + ' - ' + str(content)) 108 | try: 109 | r = requests.post('http://127.0.0.1:80' + self.path, data = content) 110 | self._respond(r) 111 | except: 112 | logging.error("POST: " + self.path + ' - ' + str(content)) 113 | self._send(500, {"Content-Length": 0}, {}) 114 | 115 | def do_DELETE(self): 116 | logging.debug("DELETE: " + self.path) 117 | try: 118 | r = requests.delete('http://127.0.0.1:80' + self.path) 119 | self._respond(r) 120 | except: 121 | logging.error("DELETE: " + self.path) 122 | self._send(500, {"Content-Length": 0}, {}) 123 | 124 | class HueHTTPSServer(ThreadingMixIn, HTTPServer): 125 | def run(self): 126 | print ('Starting HTTPS server...') 127 | ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 128 | ctx.load_cert_chain(certfile='./hue_certificate.pem') 129 | ctx.options |= ssl.OP_NO_TLSv1 130 | ctx.options |= ssl.OP_NO_TLSv1_1 131 | ctx.options |= ssl.OP_CIPHER_SERVER_PREFERENCE 132 | ctx.set_ciphers('ECDHE-ECDSA-AES128-GCM-SHA256') 133 | ctx.set_ecdh_curve('prime256v1') 134 | self.socket = ctx.wrap_socket(self.socket, server_side=True) 135 | self.serve_forever() 136 | self.server_close() 137 | 138 | # main 139 | def main(): 140 | print("HuePass") 141 | logging.basicConfig(level=logging.INFO) 142 | startedHTTPS = False 143 | (bridgeid, uuid) = getBridgeIDs() 144 | 145 | if not os.path.isfile('./hue_certificate.pem'): 146 | generateCertificate(bridgeid) 147 | try: 148 | serverHTTPS = HueHTTPSServer(('', 443), Handler) 149 | threadHTTPS = Thread(target=serverHTTPS.run) 150 | threadHTTPS.start() 151 | startedHTTPS = True 152 | while True: 153 | sleep(10) 154 | except KeyboardInterrupt: 155 | pass 156 | except Exception as e: 157 | print("Error: " + str(e)) 158 | finally: 159 | print("Shutting down...") 160 | if startedHTTPS: 161 | print("Stopping HTTPS server...") 162 | serverHTTPS.socket.shutdown(socket.SHUT_RDWR) 163 | serverHTTPS.shutdown() 164 | 165 | if __name__ == '__main__': 166 | main() 167 | --------------------------------------------------------------------------------