Make sure we always sent content-length header

This commit is contained in:
Paulus Schoutsen 2016-03-27 11:57:15 -07:00
parent 4cbd49921f
commit 7cb69ae9d9
4 changed files with 38 additions and 82 deletions

View file

@ -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)

View file

@ -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):

View file

@ -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."""

View file

@ -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")