mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Added static file handler and cleaned up API code
This commit is contained in:
parent
a60f6754aa
commit
3499814f7f
@ -71,10 +71,12 @@ import json
|
||||
import threading
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from urlparse import urlparse, parse_qs
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
|
||||
SERVER_PORT = 8123
|
||||
|
||||
@ -89,14 +91,13 @@ HTTP_UNPROCESSABLE_ENTITY = 422
|
||||
|
||||
URL_ROOT = "/"
|
||||
|
||||
URL_STATES_CATEGORY = "/states/{}"
|
||||
URL_API_STATES = "/api/states"
|
||||
URL_API_STATES_CATEGORY = "/api/states/{}"
|
||||
|
||||
URL_EVENTS_EVENT = "/events/{}"
|
||||
URL_API_EVENTS = "/api/events"
|
||||
URL_API_EVENTS_EVENT = "/api/events/{}"
|
||||
|
||||
URL_STATIC = "/static/{}"
|
||||
|
||||
class HTTPInterface(threading.Thread):
|
||||
""" Provides an HTTP interface for Home Assistant. """
|
||||
@ -134,21 +135,30 @@ class HTTPInterface(threading.Thread):
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
""" Handles incoming HTTP requests """
|
||||
|
||||
PATHS = [ ('GET', '/', '_handle_get_root'),
|
||||
PATHS = [ # debug interface
|
||||
('GET', '/', '_handle_get_root'),
|
||||
|
||||
# /states
|
||||
('GET', '/states', '_handle_get_states'),
|
||||
('GET', re.compile(r'/states/(?P<category>[a-zA-Z\.\_0-9]+)'),
|
||||
'_handle_get_states_category'),
|
||||
('POST', re.compile(r'/states/(?P<category>[a-zA-Z\.\_0-9]+)'),
|
||||
'_handle_post_states_category'),
|
||||
('GET', '/api/states', '_handle_get_api_states'),
|
||||
('GET',
|
||||
re.compile(r'/api/states/(?P<category>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_get_api_states_category'),
|
||||
('POST',
|
||||
re.compile(r'/api/states/(?P<category>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_post_api_states_category'),
|
||||
|
||||
# /events
|
||||
('GET', '/events', '_handle_get_events'),
|
||||
('POST', re.compile(r'/events/(?P<event_type>\w+)'),
|
||||
'_handle_post_events_event_type')
|
||||
('GET', '/api/events', '_handle_get_api_events'),
|
||||
('POST', re.compile(r'/api/events/(?P<event_type>\w+)'),
|
||||
'_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
|
||||
""" Does some common checks and calls appropriate method. """
|
||||
url = urlparse(self.path)
|
||||
@ -167,30 +177,25 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
except KeyError:
|
||||
api_password = ''
|
||||
|
||||
# We respond to API requests with JSON
|
||||
# For other requests we respond with html
|
||||
if url.path.startswith('/api/'):
|
||||
path = url.path[4:]
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.use_json = True
|
||||
|
||||
else:
|
||||
path = url.path
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.use_json = False
|
||||
|
||||
|
||||
# Var to keep track if we found a path that matched a handler but
|
||||
# the method was different
|
||||
path_matched_but_not_method = False
|
||||
|
||||
# Var to hold the handler for this path and method if found
|
||||
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:
|
||||
|
||||
# we either do string-comparison or regular expression matching
|
||||
if isinstance(t_path, str):
|
||||
path_match = path == t_path
|
||||
path_match = url.path == t_path
|
||||
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:
|
||||
@ -202,9 +207,13 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
path_matched_but_not_method = True
|
||||
|
||||
|
||||
# Did we find a handler for the incoming request?
|
||||
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)
|
||||
|
||||
elif path_matched_but_not_method:
|
||||
@ -231,7 +240,6 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
elif self.use_json:
|
||||
self._message("API password missing or incorrect.",
|
||||
HTTP_UNAUTHORIZED)
|
||||
|
||||
else:
|
||||
self.send_response(HTTP_OK)
|
||||
self.send_header('Content-type','text/html')
|
||||
@ -310,12 +318,12 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
write("</body></html>")
|
||||
|
||||
# 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. """
|
||||
self._write_json({'categories': self.server.statemachine.categories})
|
||||
|
||||
# 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. """
|
||||
category = path_match.group('category')
|
||||
|
||||
@ -330,7 +338,8 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
# If category does not exist
|
||||
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. """
|
||||
try:
|
||||
category = path_match.group('category')
|
||||
@ -348,13 +357,17 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
new_state,
|
||||
attributes)
|
||||
|
||||
# Return state
|
||||
state = self.server.statemachine.get_state(category)
|
||||
# Return state if json, else redirect to main page
|
||||
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,
|
||||
location=URL_STATES_CATEGORY.format(category))
|
||||
self._write_json(state, status_code=HTTP_CREATED,
|
||||
location=URL_API_STATES_CATEGORY.format(category))
|
||||
else:
|
||||
self._message("State of {} changed to {}".format(
|
||||
category, new_state))
|
||||
|
||||
except KeyError:
|
||||
# If new_state don't exist in post data
|
||||
@ -366,11 +379,12 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
self._message("Invalid JSON for attributes",
|
||||
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. """
|
||||
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. """
|
||||
event_type = path_match.group('event_type')
|
||||
|
||||
@ -390,6 +404,28 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
self._message("Invalid JSON for event_data",
|
||||
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):
|
||||
""" Helper method to return a message to the caller. """
|
||||
if self.use_json:
|
||||
|
@ -70,17 +70,6 @@ class TestHTTPInterface(unittest.TestCase):
|
||||
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):
|
||||
""" Test if we get access denied if we omit or provide
|
||||
a wrong api password. """
|
||||
@ -173,7 +162,7 @@ class TestHTTPInterface(unittest.TestCase):
|
||||
self.eventbus.listen_once("test_event_no_data", listener)
|
||||
|
||||
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})
|
||||
|
||||
# Allow the event to take place
|
||||
@ -195,7 +184,7 @@ class TestHTTPInterface(unittest.TestCase):
|
||||
self.eventbus.listen_once("test_event_with_data", listener)
|
||||
|
||||
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}',
|
||||
"api_password":API_PASSWORD})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user