mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 06:17:07 +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 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:
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user