Make sure we always sent content-length header
This commit is contained in:
parent
4cbd49921f
commit
7cb69ae9d9
4 changed files with 38 additions and 82 deletions
|
@ -14,10 +14,7 @@ import requests
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import bloomsky
|
||||
from homeassistant.const import (
|
||||
HTTP_NOT_FOUND,
|
||||
ATTR_ENTITY_ID,
|
||||
)
|
||||
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
|
||||
|
||||
|
||||
DOMAIN = 'camera'
|
||||
|
@ -36,7 +33,7 @@ STATE_IDLE = 'idle'
|
|||
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
||||
|
||||
MULTIPART_BOUNDARY = '--jpegboundary'
|
||||
MULTIPART_BOUNDARY = '--jpgboundary'
|
||||
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
||||
|
||||
|
||||
|
@ -49,17 +46,6 @@ def setup(hass, config):
|
|||
|
||||
component.setup(config)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CAMERA COMPONENT ENDPOINTS
|
||||
# -------------------------------------------------------------------------
|
||||
# The following defines the endpoints for serving images from the camera
|
||||
# via the HA http server. This is means that you can access images from
|
||||
# your camera outside of your LAN without the need for port forwards etc.
|
||||
|
||||
# Because the authentication header can't be added in image requests these
|
||||
# endpoints are secured with session based security.
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _proxy_camera_image(handler, path_match, data):
|
||||
"""Serve the camera image via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
|
@ -77,22 +63,16 @@ def setup(hass, config):
|
|||
handler.end_headers()
|
||||
return
|
||||
|
||||
handler.wfile.write(response)
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(response)
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_proxy_camera_image)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
||||
"""
|
||||
Proxy the camera image as an mjpeg stream via the HA server.
|
||||
|
||||
This function takes still images from the IP camera and turns them
|
||||
into an MJPEG stream. This means that HA can return a live video
|
||||
stream even with only a still image URL available.
|
||||
"""
|
||||
"""Proxy the camera image as an mjpeg stream via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
|
@ -112,8 +92,7 @@ def setup(hass, config):
|
|||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(
|
||||
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_proxy_camera_mjpeg_stream)
|
||||
|
||||
return True
|
||||
|
@ -137,19 +116,16 @@ class Camera(Entity):
|
|||
return ENTITY_IMAGE_URL.format(self.entity_id)
|
||||
|
||||
@property
|
||||
# pylint: disable=no-self-use
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return False
|
||||
|
||||
@property
|
||||
# pylint: disable=no-self-use
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
return None
|
||||
|
||||
@property
|
||||
# pylint: disable=no-self-use
|
||||
def model(self):
|
||||
"""Camera model."""
|
||||
return None
|
||||
|
@ -160,29 +136,28 @@ class Camera(Entity):
|
|||
|
||||
def mjpeg_stream(self, handler):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
def write_string(text):
|
||||
"""Helper method to write a string to the stream."""
|
||||
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
|
||||
|
||||
write_string('HTTP/1.1 200 OK')
|
||||
write_string('Content-type: multipart/x-mixed-replace; '
|
||||
'boundary={}'.format(MULTIPART_BOUNDARY))
|
||||
write_string('')
|
||||
write_string(MULTIPART_BOUNDARY)
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
write_string('Content-length: {}'.format(len(img_bytes)))
|
||||
write_string('Content-type: image/jpeg')
|
||||
write_string('')
|
||||
handler.request.sendall(img_bytes)
|
||||
write_string('')
|
||||
write_string(MULTIPART_BOUNDARY)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
|
|
|
@ -66,10 +66,6 @@ def _handle_get_api_bootstrap(handler, path_match, data):
|
|||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
"""Render the frontend."""
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
handler.end_headers()
|
||||
|
||||
if handler.server.development:
|
||||
app_url = "home-assistant-polymer/src/home-assistant.html"
|
||||
else:
|
||||
|
@ -86,7 +82,9 @@ def _handle_get_root(handler, path_match, data):
|
|||
template_html = template_html.replace('{{ auth }}', auth)
|
||||
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
|
||||
|
||||
handler.wfile.write(template_html.encode("UTF-8"))
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(template_html.encode("UTF-8"),
|
||||
'text/html; charset=utf-8')
|
||||
|
||||
|
||||
def _handle_get_service_worker(handler, path_match, data):
|
||||
|
|
|
@ -7,7 +7,6 @@ https://home-assistant.io/developers/api/
|
|||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
|
@ -283,31 +282,21 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
json_data = json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode('UTF-8')
|
||||
self.send_response(status_code)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(json_data)))
|
||||
|
||||
if location:
|
||||
self.send_header('Location', location)
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.end_headers()
|
||||
|
||||
if data is not None:
|
||||
self.wfile.write(json_data)
|
||||
self.write_content(json_data, CONTENT_TYPE_JSON)
|
||||
|
||||
def write_text(self, message, status_code=HTTP_OK):
|
||||
"""Helper method to return a text message to the caller."""
|
||||
msg_data = message.encode('UTF-8')
|
||||
self.send_response(status_code)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(msg_data)))
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.end_headers()
|
||||
|
||||
self.wfile.write(msg_data)
|
||||
self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
def write_file(self, path, cache_headers=True):
|
||||
"""Return a file to the user."""
|
||||
|
@ -323,36 +312,32 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
|
||||
def write_file_pointer(self, content_type, inp, cache_headers=True):
|
||||
"""Helper function to write a file pointer to the user."""
|
||||
do_gzip = 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, '')
|
||||
|
||||
self.send_response(HTTP_OK)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
||||
|
||||
if cache_headers:
|
||||
self.set_cache_header()
|
||||
self.set_session_cookie_header()
|
||||
|
||||
if do_gzip:
|
||||
gzip_data = gzip.compress(inp.read())
|
||||
self.write_content(inp.read(), content_type)
|
||||
|
||||
def write_content(self, content, content_type=None):
|
||||
"""Helper method to write content bytes to output stream."""
|
||||
if content_type is not None:
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
||||
|
||||
if 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, ''):
|
||||
content = gzip.compress(content)
|
||||
|
||||
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip")
|
||||
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(gzip_data)))
|
||||
|
||||
else:
|
||||
fst = os.fstat(inp.fileno())
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(fst[6]))
|
||||
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
|
||||
self.end_headers()
|
||||
|
||||
if self.command == 'HEAD':
|
||||
return
|
||||
|
||||
elif do_gzip:
|
||||
self.wfile.write(gzip_data)
|
||||
|
||||
else:
|
||||
self.copyfile(inp, self.wfile)
|
||||
self.wfile.write(content)
|
||||
|
||||
def set_cache_header(self):
|
||||
"""Add cache headers if not in development."""
|
||||
|
|
|
@ -104,8 +104,6 @@ class TestAPI(unittest.TestCase):
|
|||
_url(const.URL_API_STATES_ENTITY.format("test.test")),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
self.assertEqual(req.headers['content-length'], str(len(req.content)))
|
||||
|
||||
data = ha.State.from_dict(req.json())
|
||||
|
||||
state = hass.states.get("test.test")
|
||||
|
|
Loading…
Add table
Reference in a new issue