Added static file handler and cleaned up API code

This commit is contained in:
Paulus Schoutsen 2013-11-04 18:18:39 -05:00
parent a60f6754aa
commit 3499814f7f
2 changed files with 74 additions and 49 deletions

View File

@ -71,10 +71,12 @@ import json
import threading import threading
import logging import logging
import re import re
import os
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs from urlparse import urlparse, parse_qs
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util
SERVER_PORT = 8123 SERVER_PORT = 8123
@ -89,14 +91,13 @@ HTTP_UNPROCESSABLE_ENTITY = 422
URL_ROOT = "/" URL_ROOT = "/"
URL_STATES_CATEGORY = "/states/{}"
URL_API_STATES = "/api/states" URL_API_STATES = "/api/states"
URL_API_STATES_CATEGORY = "/api/states/{}" URL_API_STATES_CATEGORY = "/api/states/{}"
URL_EVENTS_EVENT = "/events/{}"
URL_API_EVENTS = "/api/events" URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}" URL_API_EVENTS_EVENT = "/api/events/{}"
URL_STATIC = "/static/{}"
class HTTPInterface(threading.Thread): class HTTPInterface(threading.Thread):
""" Provides an HTTP interface for Home Assistant. """ """ Provides an HTTP interface for Home Assistant. """
@ -134,21 +135,30 @@ class HTTPInterface(threading.Thread):
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """ """ Handles incoming HTTP requests """
PATHS = [ ('GET', '/', '_handle_get_root'), PATHS = [ # debug interface
('GET', '/', '_handle_get_root'),
# /states # /states
('GET', '/states', '_handle_get_states'), ('GET', '/api/states', '_handle_get_api_states'),
('GET', re.compile(r'/states/(?P<category>[a-zA-Z\.\_0-9]+)'), ('GET',
'_handle_get_states_category'), re.compile(r'/api/states/(?P<category>[a-zA-Z\._0-9]+)'),
('POST', re.compile(r'/states/(?P<category>[a-zA-Z\.\_0-9]+)'), '_handle_get_api_states_category'),
'_handle_post_states_category'), ('POST',
re.compile(r'/api/states/(?P<category>[a-zA-Z\._0-9]+)'),
'_handle_post_api_states_category'),
# /events # /events
('GET', '/events', '_handle_get_events'), ('GET', '/api/events', '_handle_get_api_events'),
('POST', re.compile(r'/events/(?P<event_type>\w+)'), ('POST', re.compile(r'/api/events/(?P<event_type>\w+)'),
'_handle_post_events_event_type') '_handle_post_api_events_event_type'),
# Statis files
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9\/]+)'),
'_handle_get_static')
] ]
use_json = False
def _handle_request(self, method): # pylint: disable=too-many-branches def _handle_request(self, method): # pylint: disable=too-many-branches
""" Does some common checks and calls appropriate method. """ """ Does some common checks and calls appropriate method. """
url = urlparse(self.path) url = urlparse(self.path)
@ -167,30 +177,25 @@ class RequestHandler(BaseHTTPRequestHandler):
except KeyError: except KeyError:
api_password = '' api_password = ''
# We respond to API requests with JSON
# For other requests we respond with html
if url.path.startswith('/api/'): if url.path.startswith('/api/'):
path = url.path[4:]
# pylint: disable=attribute-defined-outside-init
self.use_json = True self.use_json = True
else: # Var to keep track if we found a path that matched a handler but
path = url.path # the method was different
# pylint: disable=attribute-defined-outside-init
self.use_json = False
path_matched_but_not_method = False path_matched_but_not_method = False
# Var to hold the handler for this path and method if found
handle_request_method = False handle_request_method = False
# Check every url to find matching result # Check every handler to find matching result
for t_method, t_path, t_handler in RequestHandler.PATHS: for t_method, t_path, t_handler in RequestHandler.PATHS:
# we either do string-comparison or regular expression matching # we either do string-comparison or regular expression matching
if isinstance(t_path, str): if isinstance(t_path, str):
path_match = path == t_path path_match = url.path == t_path
else: else:
path_match = t_path.match(path) #pylint:disable=maybe-no-member # pylint: disable=maybe-no-member
path_match = t_path.match(url.path)
if path_match and method == t_method: if path_match and method == t_method:
@ -202,9 +207,13 @@ class RequestHandler(BaseHTTPRequestHandler):
path_matched_but_not_method = True path_matched_but_not_method = True
# Did we find a handler for the incoming request?
if handle_request_method: if handle_request_method:
if self._verify_api_password(api_password): # Do not enforce api password for static files
if handle_request_method == self._handle_get_static or \
self._verify_api_password(api_password):
handle_request_method(path_match, data) handle_request_method(path_match, data)
elif path_matched_but_not_method: elif path_matched_but_not_method:
@ -231,7 +240,6 @@ class RequestHandler(BaseHTTPRequestHandler):
elif self.use_json: elif self.use_json:
self._message("API password missing or incorrect.", self._message("API password missing or incorrect.",
HTTP_UNAUTHORIZED) HTTP_UNAUTHORIZED)
else: else:
self.send_response(HTTP_OK) self.send_response(HTTP_OK)
self.send_header('Content-type','text/html') self.send_header('Content-type','text/html')
@ -310,12 +318,12 @@ class RequestHandler(BaseHTTPRequestHandler):
write("</body></html>") write("</body></html>")
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _handle_get_states(self, path_match, data): def _handle_get_api_states(self, path_match, data):
""" Returns the categories which state is being tracked. """ """ Returns the categories which state is being tracked. """
self._write_json({'categories': self.server.statemachine.categories}) self._write_json({'categories': self.server.statemachine.categories})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _handle_get_states_category(self, path_match, data): def _handle_get_api_states_category(self, path_match, data):
""" Returns the state of a specific category. """ """ Returns the state of a specific category. """
category = path_match.group('category') category = path_match.group('category')
@ -330,7 +338,8 @@ class RequestHandler(BaseHTTPRequestHandler):
# If category does not exist # If category does not exist
self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY) self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY)
def _handle_post_states_category(self, path_match, data): # pylint: disable=invalid-name
def _handle_post_api_states_category(self, path_match, data):
""" Handles updating the state of a category. """ """ Handles updating the state of a category. """
try: try:
category = path_match.group('category') category = path_match.group('category')
@ -348,13 +357,17 @@ class RequestHandler(BaseHTTPRequestHandler):
new_state, new_state,
attributes) attributes)
# Return state # Return state if json, else redirect to main page
state = self.server.statemachine.get_state(category) if self.use_json:
state = self.server.statemachine.get_state(category)
state['category'] = category state['category'] = category
self._write_json(state, status_code=HTTP_CREATED, self._write_json(state, status_code=HTTP_CREATED,
location=URL_STATES_CATEGORY.format(category)) location=URL_API_STATES_CATEGORY.format(category))
else:
self._message("State of {} changed to {}".format(
category, new_state))
except KeyError: except KeyError:
# If new_state don't exist in post data # If new_state don't exist in post data
@ -366,11 +379,12 @@ class RequestHandler(BaseHTTPRequestHandler):
self._message("Invalid JSON for attributes", self._message("Invalid JSON for attributes",
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
def _handle_get_events(self, path_match, data): def _handle_get_api_events(self, path_match, data):
""" Handles getting overview of event listeners. """ """ Handles getting overview of event listeners. """
self._write_json({'listeners': self.server.eventbus.listeners}) self._write_json({'listeners': self.server.eventbus.listeners})
def _handle_post_events_event_type(self, path_match, data): # pylint: disable=invalid-name
def _handle_post_api_events_event_type(self, path_match, data):
""" Handles firing of an event. """ """ Handles firing of an event. """
event_type = path_match.group('event_type') event_type = path_match.group('event_type')
@ -390,6 +404,28 @@ class RequestHandler(BaseHTTPRequestHandler):
self._message("Invalid JSON for event_data", self._message("Invalid JSON for event_data",
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
def _handle_get_static(self, path_match, data):
""" Returns a static file. """
req_file = util.sanitize_filename(path_match.group('file'))
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
if os.path.isfile(path):
self.send_response(HTTP_OK)
self.end_headers()
with open(path, 'rb') as inp:
data = inp.read(1024)
while data:
self.wfile.write(data)
data = inp.read(1024)
else:
self.send_response(HTTP_NOT_FOUND)
self.end_headers()
def _message(self, message, status_code=HTTP_OK): def _message(self, message, status_code=HTTP_OK):
""" Helper method to return a message to the caller. """ """ Helper method to return a message to the caller. """
if self.use_json: if self.use_json:

View File

@ -70,17 +70,6 @@ class TestHTTPInterface(unittest.TestCase):
self.assertNotEqual(without_pw.text, with_pw.text) self.assertNotEqual(without_pw.text, with_pw.text)
def test_debug_state_change(self):
""" Test if the debug interface allows us to change a state. """
requests.post(
_url(hah.URL_STATES_CATEGORY.format("test")),
data={"new_state":"debug_state_change",
"api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test")['state'],
"debug_state_change")
def test_api_password(self): def test_api_password(self):
""" Test if we get access denied if we omit or provide """ Test if we get access denied if we omit or provide
a wrong api password. """ a wrong api password. """
@ -173,7 +162,7 @@ class TestHTTPInterface(unittest.TestCase):
self.eventbus.listen_once("test_event_no_data", listener) self.eventbus.listen_once("test_event_no_data", listener)
requests.post( requests.post(
_url(hah.URL_EVENTS_EVENT.format("test_event_no_data")), _url(hah.URL_API_EVENTS_EVENT.format("test_event_no_data")),
data={"api_password":API_PASSWORD}) data={"api_password":API_PASSWORD})
# Allow the event to take place # Allow the event to take place
@ -195,7 +184,7 @@ class TestHTTPInterface(unittest.TestCase):
self.eventbus.listen_once("test_event_with_data", listener) self.eventbus.listen_once("test_event_with_data", listener)
requests.post( requests.post(
_url(hah.URL_EVENTS_EVENT.format("test_event_with_data")), _url(hah.URL_API_EVENTS_EVENT.format("test_event_with_data")),
data={"event_data":'{"test": 1}', data={"event_data":'{"test": 1}',
"api_password":API_PASSWORD}) "api_password":API_PASSWORD})