2020-03-01 22:53:41 -08:00
|
|
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
2021-02-17 13:28:48 -08:00
|
|
|
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
use crate::body::Body;
|
2018-12-28 21:53:29 -06:00
|
|
|
use crate::json;
|
|
|
|
use crate::mp4;
|
2021-02-16 22:15:54 -08:00
|
|
|
use base::clock::Clocks;
|
|
|
|
use base::{bail_t, ErrorKind};
|
2018-11-25 21:31:50 -08:00
|
|
|
use bytes::{BufMut, BytesMut};
|
2016-11-25 14:34:00 -08:00
|
|
|
use core::borrow::Borrow;
|
|
|
|
use core::str::FromStr;
|
2018-12-28 21:53:29 -06:00
|
|
|
use db::dir::SampleFileDir;
|
2021-02-16 22:15:54 -08:00
|
|
|
use db::{auth, recording};
|
|
|
|
use failure::{bail, format_err, Error};
|
2018-02-11 22:45:51 -08:00
|
|
|
use fnv::FnvHashMap;
|
2020-02-28 21:41:31 -08:00
|
|
|
use futures::sink::SinkExt;
|
|
|
|
use futures::stream::StreamExt;
|
2018-08-29 22:26:19 -07:00
|
|
|
use http::header::{self, HeaderValue};
|
2021-02-16 22:15:54 -08:00
|
|
|
use http::{status::StatusCode, Request, Response};
|
2020-05-29 21:20:14 -07:00
|
|
|
use http_serve::dir::FsDir;
|
2021-02-16 22:15:54 -08:00
|
|
|
use hyper::body::Bytes;
|
2018-12-28 21:53:29 -06:00
|
|
|
use log::{debug, info, warn};
|
2020-05-29 21:20:14 -07:00
|
|
|
use memchr::memchr;
|
2021-02-16 22:15:54 -08:00
|
|
|
use nom::bytes::complete::{tag, take_while1};
|
2020-04-17 23:02:02 -07:00
|
|
|
use nom::combinator::{all_consuming, map, map_res, opt};
|
|
|
|
use nom::sequence::{preceded, tuple};
|
2021-02-16 22:15:54 -08:00
|
|
|
use nom::IResult;
|
2016-12-20 22:08:18 -08:00
|
|
|
use std::cmp;
|
2021-02-16 22:15:54 -08:00
|
|
|
use std::convert::TryFrom;
|
2018-11-28 14:22:30 -08:00
|
|
|
use std::net::IpAddr;
|
2016-12-08 21:28:50 -08:00
|
|
|
use std::ops::Range;
|
2017-02-24 21:33:26 -08:00
|
|
|
use std::sync::Arc;
|
2020-02-28 21:41:31 -08:00
|
|
|
use tokio_tungstenite::tungstenite;
|
2016-11-25 14:34:00 -08:00
|
|
|
use url::form_urlencoded;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
2016-11-25 14:34:00 -08:00
|
|
|
enum Path {
|
2018-12-29 13:06:44 -06:00
|
|
|
TopLevel, // "/api/"
|
|
|
|
Request, // "/api/request"
|
2020-03-20 20:52:30 -07:00
|
|
|
InitSegment(i32, bool), // "/api/init/<id>.mp4{.txt}"
|
2018-12-29 13:06:44 -06:00
|
|
|
Camera(Uuid), // "/api/cameras/<uuid>/"
|
2019-06-06 16:18:13 -07:00
|
|
|
Signals, // "/api/signals"
|
2018-12-29 13:06:44 -06:00
|
|
|
StreamRecordings(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/recordings"
|
|
|
|
StreamViewMp4(Uuid, db::StreamType, bool), // "/api/cameras/<uuid>/<type>/view.mp4{.txt}"
|
|
|
|
StreamViewMp4Segment(Uuid, db::StreamType, bool), // "/api/cameras/<uuid>/<type>/view.m4s{.txt}"
|
2019-01-21 15:58:52 -08:00
|
|
|
StreamLiveMp4Segments(Uuid, db::StreamType), // "/api/cameras/<uuid>/<type>/live.m4s"
|
2018-12-29 13:06:44 -06:00
|
|
|
Login, // "/api/login"
|
|
|
|
Logout, // "/api/logout"
|
|
|
|
Static, // (anything that doesn't start with "/api/")
|
2016-11-25 14:34:00 -08:00
|
|
|
NotFound,
|
|
|
|
}
|
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
impl Path {
|
|
|
|
fn decode(path: &str) -> Self {
|
|
|
|
if !path.starts_with("/api/") {
|
|
|
|
return Path::Static;
|
|
|
|
}
|
|
|
|
let path = &path["/api".len()..];
|
|
|
|
if path == "/" {
|
|
|
|
return Path::TopLevel;
|
|
|
|
}
|
|
|
|
match path {
|
|
|
|
"/login" => return Path::Login,
|
|
|
|
"/logout" => return Path::Logout,
|
2019-06-06 16:18:13 -07:00
|
|
|
"/request" => return Path::Request,
|
|
|
|
"/signals" => return Path::Signals,
|
2021-02-16 22:15:54 -08:00
|
|
|
_ => {}
|
2018-12-29 13:06:44 -06:00
|
|
|
};
|
|
|
|
if path.starts_with("/init/") {
|
|
|
|
let (debug, path) = if path.ends_with(".txt") {
|
2021-02-16 22:15:54 -08:00
|
|
|
(true, &path[0..path.len() - 4])
|
2018-12-29 13:06:44 -06:00
|
|
|
} else {
|
|
|
|
(false, path)
|
|
|
|
};
|
2020-03-20 20:52:30 -07:00
|
|
|
if !path.ends_with(".mp4") {
|
2018-12-29 13:06:44 -06:00
|
|
|
return Path::NotFound;
|
|
|
|
}
|
2020-03-20 20:52:30 -07:00
|
|
|
let id_start = "/init/".len();
|
|
|
|
let id_end = path.len() - ".mp4".len();
|
2021-02-16 22:15:54 -08:00
|
|
|
if let Ok(id) = i32::from_str(&path[id_start..id_end]) {
|
2020-03-20 20:52:30 -07:00
|
|
|
return Path::InitSegment(id, debug);
|
2018-12-29 13:06:44 -06:00
|
|
|
}
|
2017-10-01 15:29:22 -07:00
|
|
|
return Path::NotFound;
|
|
|
|
}
|
2018-12-29 13:06:44 -06:00
|
|
|
if !path.starts_with("/cameras/") {
|
|
|
|
return Path::NotFound;
|
2017-10-01 15:29:22 -07:00
|
|
|
}
|
2018-12-29 13:06:44 -06:00
|
|
|
let path = &path["/cameras/".len()..];
|
|
|
|
let slash = match path.find('/') {
|
2021-02-16 22:15:54 -08:00
|
|
|
None => {
|
|
|
|
return Path::NotFound;
|
|
|
|
}
|
2018-12-29 13:06:44 -06:00
|
|
|
Some(s) => s,
|
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
let uuid = &path[0..slash];
|
|
|
|
let path = &path[slash + 1..];
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
// TODO(slamb): require uuid to be in canonical format.
|
|
|
|
let uuid = match Uuid::parse_str(uuid) {
|
|
|
|
Ok(u) => u,
|
2021-02-16 22:15:54 -08:00
|
|
|
Err(_) => return Path::NotFound,
|
2018-12-29 13:06:44 -06:00
|
|
|
};
|
2018-01-23 11:05:07 -08:00
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
if path.is_empty() {
|
|
|
|
return Path::Camera(uuid);
|
|
|
|
}
|
2018-01-23 11:05:07 -08:00
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
let slash = match path.find('/') {
|
2021-02-16 22:15:54 -08:00
|
|
|
None => {
|
|
|
|
return Path::NotFound;
|
|
|
|
}
|
2018-12-29 13:06:44 -06:00
|
|
|
Some(s) => s,
|
|
|
|
};
|
|
|
|
let (type_, path) = path.split_at(slash);
|
2018-01-23 11:05:07 -08:00
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
let type_ = match db::StreamType::parse(type_) {
|
2021-02-16 22:15:54 -08:00
|
|
|
None => {
|
|
|
|
return Path::NotFound;
|
|
|
|
}
|
2018-12-29 13:06:44 -06:00
|
|
|
Some(t) => t,
|
|
|
|
};
|
|
|
|
match path {
|
|
|
|
"/recordings" => Path::StreamRecordings(uuid, type_),
|
|
|
|
"/view.mp4" => Path::StreamViewMp4(uuid, type_, false),
|
|
|
|
"/view.mp4.txt" => Path::StreamViewMp4(uuid, type_, true),
|
|
|
|
"/view.m4s" => Path::StreamViewMp4Segment(uuid, type_, false),
|
|
|
|
"/view.m4s.txt" => Path::StreamViewMp4Segment(uuid, type_, true),
|
2019-01-21 15:58:52 -08:00
|
|
|
"/live.m4s" => Path::StreamLiveMp4Segments(uuid, type_),
|
2018-12-29 13:06:44 -06:00
|
|
|
_ => Path::NotFound,
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-25 21:31:50 -08:00
|
|
|
fn plain_response<B: Into<Body>>(status: http::StatusCode, body: B) -> Response<Body> {
|
|
|
|
Response::builder()
|
|
|
|
.status(status)
|
|
|
|
.header(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"))
|
2021-02-16 22:15:54 -08:00
|
|
|
.body(body.into())
|
|
|
|
.expect("hardcoded head should be valid")
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn not_found<B: Into<Body>>(body: B) -> Response<Body> {
|
|
|
|
plain_response(StatusCode::NOT_FOUND, body)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn bad_req<B: Into<Body>>(body: B) -> Response<Body> {
|
|
|
|
plain_response(StatusCode::BAD_REQUEST, body)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn internal_server_err<E: Into<Error>>(err: E) -> Response<Body> {
|
|
|
|
plain_response(StatusCode::INTERNAL_SERVER_ERROR, err.into().to_string())
|
|
|
|
}
|
|
|
|
|
2018-12-28 17:30:33 -06:00
|
|
|
fn from_base_error(err: base::Error) -> Response<Body> {
|
|
|
|
let status_code = match err.kind() {
|
2019-06-19 15:17:50 -07:00
|
|
|
ErrorKind::PermissionDenied | ErrorKind::Unauthenticated => StatusCode::UNAUTHORIZED,
|
2018-12-28 17:30:33 -06:00
|
|
|
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
|
|
|
|
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
|
|
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
};
|
|
|
|
plain_response(status_code, err.to_string())
|
|
|
|
}
|
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
|
|
struct Segments {
|
|
|
|
ids: Range<i32>,
|
2018-03-02 11:38:11 -08:00
|
|
|
open_id: Option<u32>,
|
2016-12-20 22:08:18 -08:00
|
|
|
start_time: i64,
|
|
|
|
end_time: Option<i64>,
|
|
|
|
}
|
|
|
|
|
2020-11-23 00:23:03 -08:00
|
|
|
fn num<'a, T: FromStr>() -> impl FnMut(&'a str) -> IResult<&'a str, T> {
|
2020-04-17 23:02:02 -07:00
|
|
|
map_res(take_while1(|c: char| c.is_ascii_digit()), FromStr::from_str)
|
|
|
|
}
|
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
impl Segments {
|
2020-04-17 23:02:02 -07:00
|
|
|
/// Parses the `s` query parameter to `view.mp4` as described in `design/api.md`.
|
|
|
|
/// Doesn't do any validation.
|
|
|
|
fn parse(i: &str) -> IResult<&str, Segments> {
|
|
|
|
// Parse START_ID[-END_ID] into Range<i32>.
|
|
|
|
// Note that END_ID is inclusive, but Ranges are half-open.
|
2021-02-16 22:15:54 -08:00
|
|
|
let (i, ids) = map(
|
|
|
|
tuple((num::<i32>(), opt(preceded(tag("-"), num::<i32>())))),
|
|
|
|
|(start, end)| start..end.unwrap_or(start) + 1,
|
|
|
|
)(i)?;
|
2020-04-17 23:02:02 -07:00
|
|
|
|
|
|
|
// Parse [@OPEN_ID] into Option<u32>.
|
|
|
|
let (i, open_id) = opt(preceded(tag("@"), num::<u32>()))(i)?;
|
|
|
|
|
|
|
|
// Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option<i64>).
|
|
|
|
let (i, (start_time, end_time)) = map(
|
2021-02-16 22:15:54 -08:00
|
|
|
opt(preceded(
|
|
|
|
tag("."),
|
|
|
|
tuple((opt(num::<i64>()), tag("-"), opt(num::<i64>()))),
|
|
|
|
)),
|
|
|
|
|t| t.map(|(s, _, e)| (s.unwrap_or(0), e)).unwrap_or((0, None)),
|
|
|
|
)(i)?;
|
|
|
|
|
|
|
|
Ok((
|
|
|
|
i,
|
|
|
|
Segments {
|
|
|
|
ids,
|
|
|
|
open_id,
|
|
|
|
start_time,
|
|
|
|
end_time,
|
|
|
|
},
|
|
|
|
))
|
2020-04-17 23:02:02 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Segments {
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
let (_, s) = all_consuming(Segments::parse)(s).map_err(|_| ())?;
|
|
|
|
if s.ids.end <= s.ids.start {
|
2016-12-20 22:08:18 -08:00
|
|
|
return Err(());
|
|
|
|
}
|
2020-04-17 23:02:02 -07:00
|
|
|
if let Some(e) = s.end_time {
|
|
|
|
if e < s.start_time {
|
|
|
|
return Err(());
|
|
|
|
}
|
2016-12-20 22:08:18 -08:00
|
|
|
}
|
2020-04-17 23:02:02 -07:00
|
|
|
Ok(s)
|
2016-12-20 22:08:18 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-19 15:17:50 -07:00
|
|
|
struct Caller {
|
|
|
|
permissions: db::Permissions,
|
|
|
|
session: Option<json::Session>,
|
|
|
|
}
|
|
|
|
|
2018-11-25 21:31:50 -08:00
|
|
|
type ResponseResult = Result<Response<Body>, Response<Body>>;
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2019-06-06 16:18:13 -07:00
|
|
|
fn serve_json<T: serde::ser::Serialize>(req: &Request<hyper::Body>, out: &T) -> ResponseResult {
|
|
|
|
let (mut resp, writer) = http_serve::streaming_body(&req).build();
|
2021-02-16 22:15:54 -08:00
|
|
|
resp.headers_mut().insert(
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
HeaderValue::from_static("application/json"),
|
|
|
|
);
|
2019-06-06 16:18:13 -07:00
|
|
|
if let Some(mut w) = writer {
|
|
|
|
serde_json::to_writer(&mut w, out).map_err(internal_server_err)?;
|
|
|
|
}
|
|
|
|
Ok(resp)
|
|
|
|
}
|
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
fn csrf_matches(csrf: &str, session: auth::SessionHash) -> bool {
|
|
|
|
let mut b64 = [0u8; 32];
|
|
|
|
session.encode_base64(&mut b64);
|
|
|
|
::ring::constant_time::verify_slices_are_equal(&b64[..], csrf.as_bytes()).is_ok()
|
|
|
|
}
|
2017-10-21 21:54:27 -07:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
/// Extracts `s` cookie from the HTTP request. Does not authenticate.
|
|
|
|
fn extract_sid(req: &Request<hyper::Body>) -> Option<auth::RawSessionId> {
|
|
|
|
let hdr = match req.headers().get(header::COOKIE) {
|
|
|
|
None => return None,
|
|
|
|
Some(c) => c,
|
|
|
|
};
|
|
|
|
for mut cookie in hdr.as_bytes().split(|&b| b == b';') {
|
|
|
|
if cookie.starts_with(b" ") {
|
|
|
|
cookie = &cookie[1..];
|
|
|
|
}
|
|
|
|
if cookie.starts_with(b"s=") {
|
|
|
|
let s = &cookie[2..];
|
|
|
|
if let Ok(s) = auth::RawSessionId::decode_base64(s) {
|
|
|
|
return Some(s);
|
2019-06-19 15:17:50 -07:00
|
|
|
}
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
None
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
/// Extracts an `application/json` POST body from a request.
|
|
|
|
///
|
|
|
|
/// This returns the request body as bytes rather than performing
|
|
|
|
/// deserialization. Keeping the bytes allows the caller to use a `Deserialize`
|
|
|
|
/// that borrows from the bytes.
|
|
|
|
async fn extract_json_body(req: &mut Request<hyper::Body>) -> Result<Bytes, Response<Body>> {
|
|
|
|
if *req.method() != http::method::Method::POST {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(plain_response(
|
|
|
|
StatusCode::METHOD_NOT_ALLOWED,
|
|
|
|
"POST expected",
|
|
|
|
));
|
2020-05-30 19:08:54 -07:00
|
|
|
}
|
|
|
|
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
|
|
|
Some(t) if t == "application/json" => true,
|
|
|
|
Some(t) if t == "application/json; charset=UTF-8" => true,
|
2021-02-16 22:15:54 -08:00
|
|
|
_ => false,
|
2020-05-30 19:08:54 -07:00
|
|
|
};
|
|
|
|
if !correct_mime_type {
|
|
|
|
return Err(bad_req("expected application/json request body"));
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
2021-02-16 22:15:54 -08:00
|
|
|
hyper::body::to_bytes(b)
|
|
|
|
.await
|
2020-05-30 19:08:54 -07:00
|
|
|
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}", e)))
|
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
pub struct Config<'a> {
|
|
|
|
pub db: Arc<db::Database>,
|
|
|
|
pub ui_dir: Option<&'a std::path::Path>,
|
|
|
|
pub trust_forward_hdrs: bool,
|
|
|
|
pub time_zone_name: String,
|
|
|
|
pub allow_unauthenticated_permissions: Option<db::Permissions>,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Service {
|
|
|
|
db: Arc<db::Database>,
|
|
|
|
ui_dir: Option<Arc<FsDir>>,
|
|
|
|
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
|
|
|
time_zone_name: String,
|
|
|
|
allow_unauthenticated_permissions: Option<db::Permissions>,
|
|
|
|
trust_forward_hdrs: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Useful HTTP `Cache-Control` values to set on successful (HTTP 200) API responses.
|
|
|
|
enum CacheControl {
|
|
|
|
/// For endpoints which have private data that may change from request to request.
|
|
|
|
PrivateDynamic,
|
|
|
|
|
|
|
|
/// For endpoints which rarely change for a given URL.
|
|
|
|
/// E.g., a fixed segment of video. The underlying video logically never changes; there may
|
|
|
|
/// rarely be some software change to the actual bytes (which would result in a new etag) so
|
|
|
|
/// (unlike the content-hashed static content) it's not entirely immutable.
|
|
|
|
PrivateStatic,
|
|
|
|
|
|
|
|
None,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Service {
|
|
|
|
pub fn new(config: Config) -> Result<Self, Error> {
|
|
|
|
let mut ui_dir = None;
|
|
|
|
if let Some(d) = config.ui_dir {
|
|
|
|
match FsDir::builder().for_path(&d) {
|
|
|
|
Err(e) => {
|
2021-02-16 22:15:54 -08:00
|
|
|
warn!(
|
|
|
|
"Unable to load --ui-dir={}; will serve no static files: {}",
|
|
|
|
d.display(),
|
|
|
|
e
|
|
|
|
);
|
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
Ok(d) => ui_dir = Some(d),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
let dirs_by_stream_id = {
|
|
|
|
let l = config.db.lock();
|
|
|
|
let mut d =
|
|
|
|
FnvHashMap::with_capacity_and_hasher(l.streams_by_id().len(), Default::default());
|
|
|
|
for (&id, s) in l.streams_by_id().iter() {
|
|
|
|
let dir_id = match s.sample_file_dir_id {
|
|
|
|
Some(d) => d,
|
|
|
|
None => continue,
|
2017-10-17 09:00:05 -07:00
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
d.insert(id, l.sample_file_dirs_by_id().get(&dir_id).unwrap().get()?);
|
2017-10-17 09:00:05 -07:00
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
Arc::new(d)
|
2020-03-13 21:20:51 -07:00
|
|
|
};
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
Ok(Service {
|
|
|
|
db: config.db,
|
|
|
|
dirs_by_stream_id,
|
|
|
|
ui_dir,
|
|
|
|
allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
|
|
|
|
trust_forward_hdrs: config.trust_forward_hdrs,
|
|
|
|
time_zone_name: config.time_zone_name,
|
|
|
|
})
|
2017-10-01 15:29:22 -07:00
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
fn stream_live_m4s(
|
|
|
|
self: Arc<Self>,
|
|
|
|
req: Request<::hyper::Body>,
|
|
|
|
caller: Caller,
|
|
|
|
uuid: Uuid,
|
|
|
|
stream_type: db::StreamType,
|
|
|
|
) -> ResponseResult {
|
2019-06-19 15:17:50 -07:00
|
|
|
if !caller.permissions.view_video {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(plain_response(
|
|
|
|
StatusCode::UNAUTHORIZED,
|
|
|
|
"view_video required",
|
|
|
|
));
|
2019-06-19 15:17:50 -07:00
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
|
|
|
|
let stream_id;
|
|
|
|
let open_id;
|
|
|
|
let (sub_tx, sub_rx) = futures::channel::mpsc::unbounded();
|
2020-02-16 23:16:19 -08:00
|
|
|
{
|
2020-05-30 19:08:54 -07:00
|
|
|
let mut db = self.db.lock();
|
|
|
|
open_id = match db.open {
|
2021-02-16 22:15:54 -08:00
|
|
|
None => {
|
|
|
|
return Err(plain_response(
|
2020-05-30 19:08:54 -07:00
|
|
|
StatusCode::PRECONDITION_FAILED,
|
2021-02-16 22:15:54 -08:00
|
|
|
"database is read-only; there are no live streams",
|
|
|
|
))
|
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
Some(o) => o.id,
|
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
let camera = db.get_camera(uuid).ok_or_else(|| {
|
|
|
|
plain_response(StatusCode::NOT_FOUND, format!("no such camera {}", uuid))
|
|
|
|
})?;
|
|
|
|
stream_id = camera.streams[stream_type.index()].ok_or_else(|| {
|
|
|
|
plain_response(
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
format!("no such stream {}/{}", uuid, stream_type),
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
db.watch_live(
|
|
|
|
stream_id,
|
|
|
|
Box::new(move |l| sub_tx.unbounded_send(l).is_ok()),
|
|
|
|
)
|
|
|
|
.expect("stream_id refed by camera");
|
2020-05-30 19:08:54 -07:00
|
|
|
}
|
|
|
|
|
2021-01-27 11:47:52 -08:00
|
|
|
let response =
|
2021-02-16 22:15:54 -08:00
|
|
|
tungstenite::handshake::server::create_response_with_body(&req, hyper::Body::empty)
|
|
|
|
.map_err(|e| bad_req(e.to_string()))?;
|
2021-01-27 11:47:52 -08:00
|
|
|
let (parts, _) = response.into_parts();
|
2020-05-30 19:08:54 -07:00
|
|
|
|
2021-01-27 11:47:52 -08:00
|
|
|
tokio::spawn(self.stream_live_m4s_ws(stream_id, open_id, req, sub_rx));
|
2020-05-30 19:08:54 -07:00
|
|
|
|
|
|
|
Ok(Response::from_parts(parts, Body::from("")))
|
|
|
|
}
|
2020-02-16 23:16:19 -08:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
async fn stream_live_m4s_ws(
|
2021-02-16 22:15:54 -08:00
|
|
|
self: Arc<Self>,
|
|
|
|
stream_id: i32,
|
|
|
|
open_id: u32,
|
|
|
|
req: hyper::Request<hyper::Body>,
|
|
|
|
mut sub_rx: futures::channel::mpsc::UnboundedReceiver<db::LiveSegment>,
|
|
|
|
) {
|
2021-01-27 11:47:52 -08:00
|
|
|
let upgraded = match hyper::upgrade::on(req).await {
|
2020-05-30 19:08:54 -07:00
|
|
|
Ok(u) => u,
|
|
|
|
Err(e) => {
|
|
|
|
warn!("Unable to upgrade stream to websocket: {}", e);
|
|
|
|
return;
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
2020-05-30 19:08:54 -07:00
|
|
|
let mut ws = tokio_tungstenite::WebSocketStream::from_raw_socket(
|
|
|
|
upgraded,
|
|
|
|
tungstenite::protocol::Role::Server,
|
|
|
|
None,
|
2021-02-16 22:15:54 -08:00
|
|
|
)
|
|
|
|
.await;
|
2020-08-07 15:30:22 -07:00
|
|
|
|
|
|
|
// Start the first segment at a key frame to reduce startup latency.
|
|
|
|
let mut start_at_key = true;
|
2020-05-30 19:08:54 -07:00
|
|
|
loop {
|
|
|
|
let live = match sub_rx.next().await {
|
|
|
|
Some(l) => l,
|
|
|
|
None => return,
|
|
|
|
};
|
2020-08-07 15:30:22 -07:00
|
|
|
|
|
|
|
info!("chunk: is_key={:?}", live.is_key);
|
2021-02-16 22:15:54 -08:00
|
|
|
if let Err(e) = self
|
|
|
|
.stream_live_m4s_chunk(open_id, stream_id, &mut ws, live, start_at_key)
|
|
|
|
.await
|
|
|
|
{
|
2020-05-30 19:08:54 -07:00
|
|
|
info!("Dropping WebSocket after error: {}", e);
|
|
|
|
return;
|
|
|
|
}
|
2020-08-07 15:30:22 -07:00
|
|
|
start_at_key = false;
|
2020-05-30 19:08:54 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn stream_live_m4s_chunk(
|
2021-02-16 22:15:54 -08:00
|
|
|
&self,
|
|
|
|
open_id: u32,
|
|
|
|
stream_id: i32,
|
2020-05-30 19:08:54 -07:00
|
|
|
ws: &mut tokio_tungstenite::WebSocketStream<hyper::upgrade::Upgraded>,
|
2021-02-16 22:15:54 -08:00
|
|
|
live: db::LiveSegment,
|
|
|
|
start_at_key: bool,
|
|
|
|
) -> Result<(), Error> {
|
2020-05-30 19:08:54 -07:00
|
|
|
let mut builder = mp4::FileBuilder::new(mp4::Type::MediaSegment);
|
2020-06-09 22:06:03 -07:00
|
|
|
let mut row = None;
|
2020-05-30 19:08:54 -07:00
|
|
|
{
|
|
|
|
let db = self.db.lock();
|
|
|
|
let mut rows = 0;
|
2021-02-16 22:15:54 -08:00
|
|
|
db.list_recordings_by_id(stream_id, live.recording..live.recording + 1, &mut |r| {
|
2020-05-30 19:08:54 -07:00
|
|
|
rows += 1;
|
2020-06-09 22:06:03 -07:00
|
|
|
row = Some(r);
|
2020-08-07 15:30:22 -07:00
|
|
|
builder.append(&db, r, live.media_off_90k.clone(), start_at_key)?;
|
2020-05-30 19:08:54 -07:00
|
|
|
Ok(())
|
|
|
|
})?;
|
|
|
|
if rows != 1 {
|
|
|
|
bail_t!(Internal, "unable to find {:?}", live);
|
|
|
|
}
|
|
|
|
}
|
2020-06-09 22:06:03 -07:00
|
|
|
let row = row.unwrap();
|
2020-05-30 19:08:54 -07:00
|
|
|
use http_serve::Entity;
|
|
|
|
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())?;
|
|
|
|
let mut hdrs = header::HeaderMap::new();
|
|
|
|
mp4.add_headers(&mut hdrs);
|
|
|
|
let mime_type = hdrs.get(header::CONTENT_TYPE).unwrap();
|
2020-08-04 21:44:01 -07:00
|
|
|
let (prev_media_duration, prev_runs) = row.prev_media_duration_and_runs.unwrap();
|
2020-05-30 19:08:54 -07:00
|
|
|
let hdr = format!(
|
|
|
|
"Content-Type: {}\r\n\
|
|
|
|
X-Recording-Start: {}\r\n\
|
|
|
|
X-Recording-Id: {}.{}\r\n\
|
2020-08-07 10:16:06 -07:00
|
|
|
X-Media-Time-Range: {}-{}\r\n\
|
2020-08-04 21:44:01 -07:00
|
|
|
X-Prev-Media-Duration: {}\r\n\
|
2020-06-09 22:06:03 -07:00
|
|
|
X-Runs: {}\r\n\
|
2020-06-02 22:58:11 -07:00
|
|
|
X-Video-Sample-Entry-Id: {}\r\n\r\n",
|
2020-05-30 19:08:54 -07:00
|
|
|
mime_type.to_str().unwrap(),
|
2020-06-09 22:06:03 -07:00
|
|
|
row.start.0,
|
2020-05-30 19:08:54 -07:00
|
|
|
open_id,
|
|
|
|
live.recording,
|
2020-08-07 10:16:06 -07:00
|
|
|
live.media_off_90k.start,
|
|
|
|
live.media_off_90k.end,
|
2020-08-04 21:44:01 -07:00
|
|
|
prev_media_duration.0,
|
2020-06-09 22:06:03 -07:00
|
|
|
prev_runs + if row.run_offset == 0 { 1 } else { 0 },
|
2021-02-16 22:15:54 -08:00
|
|
|
&row.video_sample_entry_id
|
|
|
|
);
|
2020-06-09 22:06:03 -07:00
|
|
|
let mut v = hdr.into_bytes();
|
2020-05-30 19:08:54 -07:00
|
|
|
mp4.append_into_vec(&mut v).await?;
|
|
|
|
ws.send(tungstenite::Message::Binary(v)).await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn signals(&self, req: Request<hyper::Body>, caller: Caller) -> ResponseResult {
|
|
|
|
use http::method::Method;
|
|
|
|
match *req.method() {
|
|
|
|
Method::POST => self.post_signals(req, caller).await,
|
|
|
|
Method::GET | Method::HEAD => self.get_signals(&req),
|
2021-02-16 22:15:54 -08:00
|
|
|
_ => Err(plain_response(
|
|
|
|
StatusCode::METHOD_NOT_ALLOWED,
|
|
|
|
"POST, GET, or HEAD expected",
|
|
|
|
)),
|
2020-05-30 19:08:54 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
async fn serve_inner(
|
|
|
|
self: Arc<Self>,
|
|
|
|
req: Request<::hyper::Body>,
|
|
|
|
p: Path,
|
|
|
|
caller: Caller,
|
|
|
|
) -> ResponseResult {
|
2020-05-30 19:08:54 -07:00
|
|
|
let (cache, mut response) = match p {
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::InitSegment(sha1, debug) => (
|
|
|
|
CacheControl::PrivateStatic,
|
|
|
|
self.init_segment(sha1, debug, &req)?,
|
|
|
|
),
|
2020-05-30 19:08:54 -07:00
|
|
|
Path::TopLevel => (CacheControl::PrivateDynamic, self.top_level(&req, caller)?),
|
|
|
|
Path::Request => (CacheControl::PrivateDynamic, self.request(&req)?),
|
|
|
|
Path::Camera(uuid) => (CacheControl::PrivateDynamic, self.camera(&req, uuid)?),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamRecordings(uuid, type_) => (
|
|
|
|
CacheControl::PrivateDynamic,
|
|
|
|
self.stream_recordings(&req, uuid, type_)?,
|
|
|
|
),
|
|
|
|
Path::StreamViewMp4(uuid, type_, debug) => (
|
|
|
|
CacheControl::PrivateStatic,
|
|
|
|
self.stream_view_mp4(&req, caller, uuid, type_, mp4::Type::Normal, debug)?,
|
|
|
|
),
|
|
|
|
Path::StreamViewMp4Segment(uuid, type_, debug) => (
|
|
|
|
CacheControl::PrivateStatic,
|
|
|
|
self.stream_view_mp4(&req, caller, uuid, type_, mp4::Type::MediaSegment, debug)?,
|
|
|
|
),
|
|
|
|
Path::StreamLiveMp4Segments(uuid, type_) => (
|
|
|
|
CacheControl::PrivateDynamic,
|
|
|
|
self.stream_live_m4s(req, caller, uuid, type_)?,
|
|
|
|
),
|
2020-05-30 19:08:54 -07:00
|
|
|
Path::NotFound => return Err(not_found("path not understood")),
|
|
|
|
Path::Login => (CacheControl::PrivateDynamic, self.login(req).await?),
|
|
|
|
Path::Logout => (CacheControl::PrivateDynamic, self.logout(req).await?),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::Signals => (
|
|
|
|
CacheControl::PrivateDynamic,
|
|
|
|
self.signals(req, caller).await?,
|
|
|
|
),
|
|
|
|
Path::Static => (CacheControl::None, self.static_file(req).await?),
|
2020-05-30 19:08:54 -07:00
|
|
|
};
|
|
|
|
match cache {
|
|
|
|
CacheControl::PrivateStatic => {
|
2021-02-16 22:15:54 -08:00
|
|
|
response.headers_mut().insert(
|
|
|
|
header::CACHE_CONTROL,
|
|
|
|
HeaderValue::from_static("private, max-age=3600"),
|
|
|
|
);
|
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
CacheControl::PrivateDynamic => {
|
2021-02-16 22:15:54 -08:00
|
|
|
response.headers_mut().insert(
|
|
|
|
header::CACHE_CONTROL,
|
|
|
|
HeaderValue::from_static("private, no-cache"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
CacheControl::None => {}
|
2020-05-30 19:08:54 -07:00
|
|
|
}
|
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
pub async fn serve(
|
|
|
|
self: Arc<Self>,
|
|
|
|
req: Request<::hyper::Body>,
|
|
|
|
) -> Result<Response<Body>, std::convert::Infallible> {
|
2020-05-30 19:08:54 -07:00
|
|
|
let p = Path::decode(req.uri().path());
|
|
|
|
let always_allow_unauthenticated = match p {
|
|
|
|
Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static => true,
|
|
|
|
_ => false,
|
|
|
|
};
|
|
|
|
debug!("request on: {}: {:?}", req.uri(), p);
|
|
|
|
let caller = match self.authenticate(&req, always_allow_unauthenticated) {
|
|
|
|
Ok(c) => c,
|
|
|
|
Err(e) => return Ok(from_base_error(e)),
|
|
|
|
};
|
|
|
|
Ok(self.serve_inner(req, p, caller).await.unwrap_or_else(|e| e))
|
|
|
|
}
|
|
|
|
|
2019-06-19 15:17:50 -07:00
|
|
|
fn top_level(&self, req: &Request<::hyper::Body>, caller: Caller) -> ResponseResult {
|
2017-10-21 21:54:27 -07:00
|
|
|
let mut days = false;
|
2019-06-19 15:17:50 -07:00
|
|
|
let mut camera_configs = false;
|
2018-01-23 11:22:23 -08:00
|
|
|
if let Some(q) = req.uri().query() {
|
2017-10-21 21:54:27 -07:00
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
2018-11-25 21:31:50 -08:00
|
|
|
let (key, value): (_, &str) = (key.borrow(), value.borrow());
|
2017-10-21 21:54:27 -07:00
|
|
|
match key {
|
|
|
|
"days" => days = value == "true",
|
2019-06-19 15:17:50 -07:00
|
|
|
"cameraConfigs" => camera_configs = value == "true",
|
2021-02-16 22:15:54 -08:00
|
|
|
_ => {}
|
2017-10-21 21:54:27 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-19 15:17:50 -07:00
|
|
|
if camera_configs {
|
|
|
|
if !caller.permissions.read_camera_configs {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(plain_response(
|
|
|
|
StatusCode::UNAUTHORIZED,
|
|
|
|
"read_camera_configs required",
|
|
|
|
));
|
2019-06-19 15:17:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-06 16:18:13 -07:00
|
|
|
let db = self.db.lock();
|
2021-02-16 22:15:54 -08:00
|
|
|
serve_json(
|
|
|
|
req,
|
|
|
|
&json::TopLevel {
|
|
|
|
time_zone_name: &self.time_zone_name,
|
|
|
|
cameras: (&db, days, camera_configs),
|
|
|
|
session: caller.session,
|
|
|
|
signals: (&db, days),
|
|
|
|
signal_types: &db,
|
|
|
|
},
|
|
|
|
)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2018-11-25 21:31:50 -08:00
|
|
|
fn camera(&self, req: &Request<::hyper::Body>, uuid: Uuid) -> ResponseResult {
|
2019-06-06 16:18:13 -07:00
|
|
|
let db = self.db.lock();
|
2021-02-16 22:15:54 -08:00
|
|
|
let camera = db
|
|
|
|
.get_camera(uuid)
|
|
|
|
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
|
|
|
|
serve_json(
|
|
|
|
req,
|
|
|
|
&json::Camera::wrap(camera, &db, true, false).map_err(internal_server_err)?,
|
|
|
|
)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
fn stream_recordings(
|
|
|
|
&self,
|
|
|
|
req: &Request<::hyper::Body>,
|
|
|
|
uuid: Uuid,
|
|
|
|
type_: db::StreamType,
|
|
|
|
) -> ResponseResult {
|
2017-10-17 09:00:05 -07:00
|
|
|
let (r, split) = {
|
2021-02-16 22:15:54 -08:00
|
|
|
let mut time = recording::Time::min_value()..recording::Time::max_value();
|
2017-10-17 09:00:05 -07:00
|
|
|
let mut split = recording::Duration(i64::max_value());
|
2018-01-23 11:22:23 -08:00
|
|
|
if let Some(q) = req.uri().query() {
|
2017-10-17 09:00:05 -07:00
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
|
|
|
let (key, value) = (key.borrow(), value.borrow());
|
|
|
|
match key {
|
2018-11-25 21:31:50 -08:00
|
|
|
"startTime90k" => {
|
|
|
|
time.start = recording::Time::parse(value)
|
|
|
|
.map_err(|_| bad_req("unparseable startTime90k"))?
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2018-11-25 21:31:50 -08:00
|
|
|
"endTime90k" => {
|
|
|
|
time.end = recording::Time::parse(value)
|
|
|
|
.map_err(|_| bad_req("unparseable endTime90k"))?
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2018-11-25 21:31:50 -08:00
|
|
|
"split90k" => {
|
2021-02-16 22:15:54 -08:00
|
|
|
split = recording::Duration(
|
|
|
|
i64::from_str(value)
|
|
|
|
.map_err(|_| bad_req("unparseable split90k"))?,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
_ => {}
|
2017-10-17 09:00:05 -07:00
|
|
|
}
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2017-10-17 09:00:05 -07:00
|
|
|
}
|
|
|
|
(time, split)
|
|
|
|
};
|
2020-03-13 21:20:51 -07:00
|
|
|
let db = self.db.lock();
|
|
|
|
let mut out = json::ListRecordings {
|
|
|
|
recordings: Vec::new(),
|
|
|
|
video_sample_entries: (&db, Vec::new()),
|
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
let camera = db.get_camera(uuid).ok_or_else(|| {
|
|
|
|
plain_response(StatusCode::NOT_FOUND, format!("no such camera {}", uuid))
|
|
|
|
})?;
|
|
|
|
let stream_id = camera.streams[type_.index()].ok_or_else(|| {
|
|
|
|
plain_response(
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
format!("no such stream {}/{}", uuid, type_),
|
|
|
|
)
|
|
|
|
})?;
|
2020-03-13 21:20:51 -07:00
|
|
|
db.list_aggregated_recordings(stream_id, r, split, &mut |row| {
|
2021-02-16 22:15:54 -08:00
|
|
|
let end = row.ids.end - 1; // in api, ids are inclusive.
|
2020-03-13 21:20:51 -07:00
|
|
|
out.recordings.push(json::Recording {
|
|
|
|
start_id: row.ids.start,
|
2021-02-16 22:15:54 -08:00
|
|
|
end_id: if end == row.ids.start {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(end)
|
|
|
|
},
|
2020-03-13 21:20:51 -07:00
|
|
|
start_time_90k: row.time.start.0,
|
|
|
|
end_time_90k: row.time.end.0,
|
|
|
|
sample_file_bytes: row.sample_file_bytes,
|
|
|
|
open_id: row.open_id,
|
|
|
|
first_uncommitted: row.first_uncommitted,
|
|
|
|
video_samples: row.video_samples,
|
2021-02-22 13:46:51 -08:00
|
|
|
video_sample_entry_id: row.video_sample_entry_id,
|
2020-03-13 21:20:51 -07:00
|
|
|
growing: row.growing,
|
|
|
|
});
|
2021-02-16 22:15:54 -08:00
|
|
|
if !out
|
|
|
|
.video_sample_entries
|
|
|
|
.1
|
|
|
|
.contains(&row.video_sample_entry_id)
|
|
|
|
{
|
2020-03-13 21:20:51 -07:00
|
|
|
out.video_sample_entries.1.push(row.video_sample_entry_id);
|
|
|
|
}
|
|
|
|
Ok(())
|
2021-02-16 22:15:54 -08:00
|
|
|
})
|
|
|
|
.map_err(internal_server_err)?;
|
2019-06-06 16:18:13 -07:00
|
|
|
serve_json(req, &out)
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
fn init_segment(&self, id: i32, debug: bool, req: &Request<::hyper::Body>) -> ResponseResult {
|
2017-10-01 15:29:22 -07:00
|
|
|
let mut builder = mp4::FileBuilder::new(mp4::Type::InitSegment);
|
|
|
|
let db = self.db.lock();
|
2021-02-16 22:15:54 -08:00
|
|
|
let ent = db
|
|
|
|
.video_sample_entries_by_id()
|
|
|
|
.get(&id)
|
2020-03-20 20:52:30 -07:00
|
|
|
.ok_or_else(|| not_found("not such init segment"))?;
|
|
|
|
builder.append_video_sample_entry(ent.clone());
|
2021-02-16 22:15:54 -08:00
|
|
|
let mp4 = builder
|
|
|
|
.build(self.db.clone(), self.dirs_by_stream_id.clone())
|
2020-03-20 20:52:30 -07:00
|
|
|
.map_err(from_base_error)?;
|
|
|
|
if debug {
|
|
|
|
Ok(plain_response(StatusCode::OK, format!("{:#?}", mp4)))
|
|
|
|
} else {
|
|
|
|
Ok(http_serve::serve(mp4, req))
|
2017-10-01 15:29:22 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
fn stream_view_mp4(
|
|
|
|
&self,
|
|
|
|
req: &Request<::hyper::Body>,
|
|
|
|
caller: Caller,
|
|
|
|
uuid: Uuid,
|
|
|
|
stream_type: db::StreamType,
|
|
|
|
mp4_type: mp4::Type,
|
|
|
|
debug: bool,
|
|
|
|
) -> ResponseResult {
|
2019-06-19 15:17:50 -07:00
|
|
|
if !caller.permissions.view_video {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(plain_response(
|
|
|
|
StatusCode::UNAUTHORIZED,
|
|
|
|
"view_video required",
|
|
|
|
));
|
2019-06-19 15:17:50 -07:00
|
|
|
}
|
2020-02-16 23:16:19 -08:00
|
|
|
let (stream_id, camera_name);
|
|
|
|
{
|
2016-11-25 14:34:00 -08:00
|
|
|
let db = self.db.lock();
|
2021-02-16 22:15:54 -08:00
|
|
|
let camera = db.get_camera(uuid).ok_or_else(|| {
|
|
|
|
plain_response(StatusCode::NOT_FOUND, format!("no such camera {}", uuid))
|
|
|
|
})?;
|
2020-02-16 23:16:19 -08:00
|
|
|
camera_name = camera.short_name.clone();
|
2021-02-16 22:15:54 -08:00
|
|
|
stream_id = camera.streams[stream_type.index()].ok_or_else(|| {
|
|
|
|
plain_response(
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
format!("no such stream {}/{}", uuid, stream_type),
|
|
|
|
)
|
|
|
|
})?;
|
2016-11-25 14:34:00 -08:00
|
|
|
};
|
2020-02-16 23:16:19 -08:00
|
|
|
let mut start_time_for_filename = None;
|
2019-01-21 15:58:52 -08:00
|
|
|
let mut builder = mp4::FileBuilder::new(mp4_type);
|
2018-01-23 11:05:07 -08:00
|
|
|
if let Some(q) = req.uri().query() {
|
2017-03-02 19:29:28 -08:00
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
|
|
|
let (key, value) = (key.borrow(), value.borrow());
|
|
|
|
match key {
|
|
|
|
"s" => {
|
2021-02-16 22:15:54 -08:00
|
|
|
let s = Segments::from_str(value).map_err(|()| {
|
|
|
|
plain_response(
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
format!("invalid s parameter: {}", value),
|
|
|
|
)
|
|
|
|
})?;
|
2018-01-23 11:05:07 -08:00
|
|
|
debug!("stream_view_mp4: appending s={:?}", s);
|
2020-08-04 21:44:01 -07:00
|
|
|
let mut est_segments = usize::try_from(s.ids.end - s.ids.start).unwrap();
|
2017-03-02 19:29:28 -08:00
|
|
|
if let Some(end) = s.end_time {
|
|
|
|
// There should be roughly ceil((end - start) /
|
|
|
|
// desired_recording_duration) recordings in the desired timespan if
|
|
|
|
// there are no gaps or overlap, possibly another for misalignment of
|
|
|
|
// the requested timespan with the rotate offset and another because
|
|
|
|
// rotation only happens at key frames.
|
2021-02-16 22:15:54 -08:00
|
|
|
let ceil_durations = (end - s.start_time
|
|
|
|
+ recording::DESIRED_RECORDING_WALL_DURATION
|
|
|
|
- 1)
|
|
|
|
/ recording::DESIRED_RECORDING_WALL_DURATION;
|
2017-03-02 19:29:28 -08:00
|
|
|
est_segments = cmp::min(est_segments, (ceil_durations + 2) as usize);
|
|
|
|
}
|
|
|
|
builder.reserve(est_segments);
|
|
|
|
let db = self.db.lock();
|
2020-08-04 21:44:01 -07:00
|
|
|
let mut prev = None; // previous recording id
|
2017-03-02 19:29:28 -08:00
|
|
|
let mut cur_off = 0;
|
2018-02-23 09:19:42 -08:00
|
|
|
db.list_recordings_by_id(stream_id, s.ids.clone(), &mut |r| {
|
2018-02-20 10:11:10 -08:00
|
|
|
let recording_id = r.id.recording();
|
|
|
|
|
2018-03-02 11:38:11 -08:00
|
|
|
if let Some(o) = s.open_id {
|
|
|
|
if r.open_id != o {
|
2021-02-16 22:15:54 -08:00
|
|
|
bail!(
|
|
|
|
"recording {} has open id {}, requested {}",
|
|
|
|
r.id,
|
|
|
|
r.open_id,
|
|
|
|
o
|
|
|
|
);
|
2018-03-02 11:38:11 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-02 19:29:28 -08:00
|
|
|
// Check for missing recordings.
|
|
|
|
match prev {
|
2021-02-16 22:15:54 -08:00
|
|
|
None if recording_id == s.ids.start => {}
|
2018-02-20 22:46:14 -08:00
|
|
|
None => bail!("no such recording {}/{}", stream_id, s.ids.start),
|
2018-02-20 10:11:10 -08:00
|
|
|
Some(id) if r.id.recording() != id + 1 => {
|
2018-02-20 22:46:14 -08:00
|
|
|
bail!("no such recording {}/{}", stream_id, id + 1);
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
|
|
|
_ => {}
|
2017-03-02 19:29:28 -08:00
|
|
|
};
|
2018-02-20 10:11:10 -08:00
|
|
|
prev = Some(recording_id);
|
2017-03-02 19:29:28 -08:00
|
|
|
|
|
|
|
// Add a segment for the relevant part of the recording, if any.
|
2020-08-04 21:44:01 -07:00
|
|
|
// Note all calculations here are in wall times / wall durations.
|
2017-03-02 19:29:28 -08:00
|
|
|
let end_time = s.end_time.unwrap_or(i64::max_value());
|
2020-08-07 10:16:06 -07:00
|
|
|
let wd = i64::from(r.wall_duration_90k);
|
|
|
|
if s.start_time <= cur_off + wd && cur_off < end_time {
|
2017-03-02 19:29:28 -08:00
|
|
|
let start = cmp::max(0, s.start_time - cur_off);
|
2020-08-07 10:16:06 -07:00
|
|
|
let end = cmp::min(wd, end_time - cur_off);
|
2021-02-16 22:15:54 -08:00
|
|
|
let wr = i32::try_from(start).unwrap()..i32::try_from(end).unwrap();
|
|
|
|
debug!(
|
|
|
|
"...appending recording {} with wall duration {:?} \
|
|
|
|
(out of total {})",
|
|
|
|
r.id, wr, wd
|
|
|
|
);
|
2020-02-16 23:16:19 -08:00
|
|
|
if start_time_for_filename.is_none() {
|
|
|
|
start_time_for_filename =
|
|
|
|
Some(r.start + recording::Duration(start));
|
|
|
|
}
|
2020-08-07 10:16:06 -07:00
|
|
|
use recording::rescale;
|
|
|
|
let mr =
|
2021-02-16 22:15:54 -08:00
|
|
|
rescale(wr.start, r.wall_duration_90k, r.media_duration_90k)
|
|
|
|
..rescale(
|
|
|
|
wr.end,
|
|
|
|
r.wall_duration_90k,
|
|
|
|
r.media_duration_90k,
|
|
|
|
);
|
2020-08-07 15:30:22 -07:00
|
|
|
builder.append(&db, r, mr, true)?;
|
2017-03-02 19:29:28 -08:00
|
|
|
} else {
|
2020-08-07 10:16:06 -07:00
|
|
|
debug!("...skipping recording {} wall dur {}", r.id, wd);
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
2020-08-07 10:16:06 -07:00
|
|
|
cur_off += wd;
|
2017-03-02 19:29:28 -08:00
|
|
|
Ok(())
|
2021-02-16 22:15:54 -08:00
|
|
|
})
|
|
|
|
.map_err(internal_server_err)?;
|
2017-03-02 19:29:28 -08:00
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
// Check for missing recordings.
|
|
|
|
match prev {
|
2017-03-02 19:29:28 -08:00
|
|
|
Some(id) if s.ids.end != id + 1 => {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(not_found(format!(
|
|
|
|
"no such recording {}/{}",
|
|
|
|
stream_id,
|
|
|
|
s.ids.end - 1
|
|
|
|
)));
|
|
|
|
}
|
2017-03-02 19:29:28 -08:00
|
|
|
None => {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(not_found(format!(
|
|
|
|
"no such recording {}/{}",
|
|
|
|
stream_id, s.ids.start
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
_ => {}
|
2016-12-20 22:08:18 -08:00
|
|
|
};
|
2017-03-02 19:29:28 -08:00
|
|
|
if let Some(end) = s.end_time {
|
|
|
|
if end > cur_off {
|
2018-11-25 21:31:50 -08:00
|
|
|
return Err(plain_response(
|
2021-02-16 22:15:54 -08:00
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
format!("end time {} is beyond specified recordings", end),
|
|
|
|
));
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
2016-12-20 22:08:18 -08:00
|
|
|
}
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
|
|
|
"ts" => builder
|
|
|
|
.include_timestamp_subtitle_track(value == "true")
|
|
|
|
.map_err(from_base_error)?,
|
2018-11-25 21:31:50 -08:00
|
|
|
_ => return Err(bad_req(format!("parameter {} not understood", key))),
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2017-03-02 19:29:28 -08:00
|
|
|
}
|
2020-02-16 23:16:19 -08:00
|
|
|
if let Some(start) = start_time_for_filename {
|
2021-02-16 22:15:54 -08:00
|
|
|
let tm = time::at(time::Timespec {
|
|
|
|
sec: start.unix_seconds(),
|
|
|
|
nsec: 0,
|
|
|
|
});
|
|
|
|
let stream_abbrev = if stream_type == db::StreamType::MAIN {
|
|
|
|
"main"
|
|
|
|
} else {
|
|
|
|
"sub"
|
|
|
|
};
|
|
|
|
let suffix = if mp4_type == mp4::Type::Normal {
|
|
|
|
"mp4"
|
|
|
|
} else {
|
|
|
|
"m4s"
|
|
|
|
};
|
|
|
|
builder
|
|
|
|
.set_filename(&format!(
|
|
|
|
"{}-{}-{}.{}",
|
|
|
|
tm.strftime("%Y%m%d%H%M%S").unwrap(),
|
|
|
|
camera_name,
|
|
|
|
stream_abbrev,
|
|
|
|
suffix
|
|
|
|
))
|
2020-02-16 23:16:19 -08:00
|
|
|
.map_err(from_base_error)?;
|
|
|
|
}
|
2021-02-16 22:15:54 -08:00
|
|
|
let mp4 = builder
|
|
|
|
.build(self.db.clone(), self.dirs_by_stream_id.clone())
|
|
|
|
.map_err(from_base_error)?;
|
2018-12-29 13:06:44 -06:00
|
|
|
if debug {
|
|
|
|
return Ok(plain_response(StatusCode::OK, format!("{:#?}", mp4)));
|
|
|
|
}
|
2018-01-23 11:08:21 -08:00
|
|
|
Ok(http_serve::serve(mp4, req))
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2017-10-21 21:54:27 -07:00
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
async fn static_file(&self, req: Request<hyper::Body>) -> ResponseResult {
|
2021-02-16 22:15:54 -08:00
|
|
|
let dir = self
|
|
|
|
.ui_dir
|
|
|
|
.clone()
|
2020-05-30 19:08:54 -07:00
|
|
|
.ok_or_else(|| not_found("--ui-dir not configured; no static files available."))?;
|
|
|
|
let static_req = match StaticFileRequest::parse(req.uri().path()) {
|
|
|
|
None => return Err(not_found("static file not found")),
|
|
|
|
Some(r) => r,
|
2020-05-29 21:20:14 -07:00
|
|
|
};
|
2020-05-30 19:08:54 -07:00
|
|
|
let f = dir.get(static_req.path, req.headers());
|
2021-02-16 22:15:54 -08:00
|
|
|
let node = f.await.map_err(|e| {
|
|
|
|
if e.kind() == std::io::ErrorKind::NotFound {
|
2020-05-30 19:08:54 -07:00
|
|
|
not_found("no such static file")
|
2020-05-29 21:20:14 -07:00
|
|
|
} else {
|
2020-05-30 19:08:54 -07:00
|
|
|
internal_server_err(e)
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
|
|
|
})?;
|
2018-04-06 15:54:52 -07:00
|
|
|
let mut hdrs = http::HeaderMap::new();
|
2020-05-30 19:08:54 -07:00
|
|
|
node.add_encoding_headers(&mut hdrs);
|
2021-02-16 22:15:54 -08:00
|
|
|
hdrs.insert(
|
|
|
|
header::CACHE_CONTROL,
|
|
|
|
HeaderValue::from_static(if static_req.immutable {
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Caching_static_assets
|
|
|
|
"public, max-age=604800, immutable"
|
|
|
|
} else {
|
|
|
|
"public"
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
hdrs.insert(
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
HeaderValue::from_static(static_req.mime),
|
|
|
|
);
|
2020-05-30 19:08:54 -07:00
|
|
|
let e = node.into_file_entity(hdrs).map_err(internal_server_err)?;
|
2018-01-23 11:08:21 -08:00
|
|
|
Ok(http_serve::serve(e, &req))
|
2017-10-21 21:54:27 -07:00
|
|
|
}
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
fn authreq(&self, req: &Request<::hyper::Body>) -> auth::Request {
|
|
|
|
auth::Request {
|
|
|
|
when_sec: Some(self.db.clocks().realtime().sec),
|
2018-11-28 14:22:30 -08:00
|
|
|
addr: if self.trust_forward_hdrs {
|
2021-02-16 22:15:54 -08:00
|
|
|
req.headers()
|
|
|
|
.get("X-Real-IP")
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
.and_then(|v| IpAddr::from_str(v).ok())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
user_agent: req
|
|
|
|
.headers()
|
|
|
|
.get(header::USER_AGENT)
|
|
|
|
.map(|ua| ua.as_bytes().to_vec()),
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-01 00:44:19 -08:00
|
|
|
fn request(&self, req: &Request<::hyper::Body>) -> ResponseResult {
|
|
|
|
let authreq = self.authreq(req);
|
2021-02-16 22:15:54 -08:00
|
|
|
let host = req
|
|
|
|
.headers()
|
|
|
|
.get(header::HOST)
|
|
|
|
.map(|h| String::from_utf8_lossy(h.as_bytes()));
|
|
|
|
let agent = authreq
|
|
|
|
.user_agent
|
|
|
|
.as_ref()
|
|
|
|
.map(|u| String::from_utf8_lossy(&u[..]));
|
|
|
|
Ok(plain_response(
|
|
|
|
StatusCode::OK,
|
|
|
|
format!(
|
|
|
|
"when: {}\n\
|
2018-12-27 16:00:15 -06:00
|
|
|
host: {:?}\n\
|
2018-12-01 00:44:19 -08:00
|
|
|
addr: {:?}\n\
|
|
|
|
user_agent: {:?}\n\
|
|
|
|
secure: {:?}",
|
2021-02-16 22:15:54 -08:00
|
|
|
time::at(time::Timespec {
|
|
|
|
sec: authreq.when_sec.unwrap(),
|
|
|
|
nsec: 0
|
|
|
|
})
|
|
|
|
.strftime("%FT%T")
|
|
|
|
.map(|f| f.to_string())
|
|
|
|
.unwrap_or_else(|e| e.to_string()),
|
|
|
|
host.as_ref().map(|h| &*h),
|
|
|
|
&authreq.addr,
|
|
|
|
agent.as_ref().map(|a| &*a),
|
|
|
|
self.is_secure(req)
|
|
|
|
),
|
|
|
|
))
|
2018-12-01 00:44:19 -08:00
|
|
|
}
|
|
|
|
|
2018-11-28 14:22:30 -08:00
|
|
|
fn is_secure(&self, req: &Request<::hyper::Body>) -> bool {
|
2021-02-16 22:15:54 -08:00
|
|
|
self.trust_forward_hdrs
|
|
|
|
&& req
|
|
|
|
.headers()
|
|
|
|
.get("X-Forwarded-Proto")
|
|
|
|
.map(|v| v.as_bytes() == b"https")
|
|
|
|
.unwrap_or(false)
|
2018-11-28 14:22:30 -08:00
|
|
|
}
|
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
async fn login(&self, mut req: Request<::hyper::Body>) -> ResponseResult {
|
|
|
|
let r = extract_json_body(&mut req).await?;
|
2021-02-16 22:15:54 -08:00
|
|
|
let r: json::LoginRequest =
|
|
|
|
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
|
2020-05-30 19:08:54 -07:00
|
|
|
let authreq = self.authreq(&req);
|
2021-02-16 22:15:54 -08:00
|
|
|
let host = req
|
|
|
|
.headers()
|
|
|
|
.get(header::HOST)
|
|
|
|
.ok_or_else(|| bad_req("missing Host header!"))?;
|
2018-11-25 21:31:50 -08:00
|
|
|
let host = host.as_bytes();
|
2020-05-29 21:20:14 -07:00
|
|
|
let domain = match memchr(b':', host) {
|
2018-11-25 21:31:50 -08:00
|
|
|
Some(colon) => &host[0..colon],
|
|
|
|
None => host,
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
|
|
|
.to_owned();
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut l = self.db.lock();
|
2020-05-30 19:08:54 -07:00
|
|
|
let is_secure = self.is_secure(&req);
|
2021-02-16 22:15:54 -08:00
|
|
|
let flags = (auth::SessionFlag::HttpOnly as i32)
|
|
|
|
| (auth::SessionFlag::SameSite as i32)
|
|
|
|
| (auth::SessionFlag::SameSiteStrict as i32)
|
|
|
|
| if is_secure {
|
|
|
|
auth::SessionFlag::Secure as i32
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
|
|
|
let (sid, _) = l
|
|
|
|
.login_by_password(authreq, &r.username, r.password, Some(domain), flags)
|
2018-11-25 21:31:50 -08:00
|
|
|
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
2018-11-28 14:22:30 -08:00
|
|
|
let s_suffix = if is_secure {
|
2020-01-08 23:04:36 -08:00
|
|
|
&b"; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
2018-11-28 14:22:30 -08:00
|
|
|
} else {
|
2020-01-08 23:04:36 -08:00
|
|
|
&b"; HttpOnly; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
2018-11-28 14:22:30 -08:00
|
|
|
};
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut encoded = [0u8; 64];
|
|
|
|
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
|
|
|
let mut cookie = BytesMut::with_capacity("s=".len() + encoded.len() + s_suffix.len());
|
2020-01-08 23:04:36 -08:00
|
|
|
cookie.put(&b"s="[..]);
|
2018-11-25 21:31:50 -08:00
|
|
|
cookie.put(&encoded[..]);
|
|
|
|
cookie.put(s_suffix);
|
|
|
|
Ok(Response::builder()
|
2021-02-16 22:15:54 -08:00
|
|
|
.header(
|
|
|
|
header::SET_COOKIE,
|
|
|
|
HeaderValue::from_maybe_shared(cookie.freeze())
|
|
|
|
.expect("cookie can't have invalid bytes"),
|
|
|
|
)
|
2018-11-25 21:31:50 -08:00
|
|
|
.status(StatusCode::NO_CONTENT)
|
2021-02-16 22:15:54 -08:00
|
|
|
.body(b""[..].into())
|
|
|
|
.unwrap())
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
2020-05-30 19:08:54 -07:00
|
|
|
async fn logout(&self, mut req: Request<hyper::Body>) -> ResponseResult {
|
|
|
|
let r = extract_json_body(&mut req).await?;
|
2021-02-16 22:15:54 -08:00
|
|
|
let r: json::LogoutRequest =
|
|
|
|
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
let mut res = Response::new(b""[..].into());
|
2020-05-30 19:08:54 -07:00
|
|
|
if let Some(sid) = extract_sid(&req) {
|
|
|
|
let authreq = self.authreq(&req);
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut l = self.db.lock();
|
|
|
|
let hash = sid.hash();
|
|
|
|
let need_revoke = match l.authenticate_session(authreq.clone(), &hash) {
|
|
|
|
Ok((s, _)) => {
|
2020-01-08 23:23:58 -08:00
|
|
|
if !csrf_matches(r.csrf, s.csrf()) {
|
2018-11-25 21:31:50 -08:00
|
|
|
warn!("logout request with missing/incorrect csrf");
|
|
|
|
return Err(bad_req("logout with incorrect csrf token"));
|
|
|
|
}
|
|
|
|
info!("revoking session");
|
|
|
|
true
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2018-11-25 21:31:50 -08:00
|
|
|
Err(e) => {
|
|
|
|
// TODO: distinguish "no such session", "session is no longer valid", and
|
|
|
|
// "user ... is disabled" (which are all client error / bad state) from database
|
|
|
|
// errors.
|
|
|
|
warn!("logout failed: {}", e);
|
|
|
|
false
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2018-11-25 21:31:50 -08:00
|
|
|
};
|
|
|
|
if need_revoke {
|
|
|
|
// TODO: inline this above with non-lexical lifetimes.
|
|
|
|
l.revoke_session(auth::RevocationReason::LoggedOut, None, authreq, &hash)
|
2021-02-16 22:15:54 -08:00
|
|
|
.map_err(internal_server_err)?;
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// By now the session is invalid (whether it was valid to start with or not).
|
|
|
|
// Clear useless cookie.
|
2021-02-16 22:15:54 -08:00
|
|
|
res.headers_mut().append(
|
|
|
|
header::SET_COOKIE,
|
|
|
|
HeaderValue::from_str("s=; Max-Age=0; Path=/").unwrap(),
|
|
|
|
);
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
*res.status_mut() = StatusCode::NO_CONTENT;
|
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
async fn post_signals(&self, mut req: Request<hyper::Body>, caller: Caller) -> ResponseResult {
|
2019-06-19 15:17:50 -07:00
|
|
|
if !caller.permissions.update_signals {
|
2021-02-16 22:15:54 -08:00
|
|
|
return Err(plain_response(
|
|
|
|
StatusCode::UNAUTHORIZED,
|
|
|
|
"update_signals required",
|
|
|
|
));
|
2019-06-19 15:17:50 -07:00
|
|
|
}
|
2020-05-30 19:08:54 -07:00
|
|
|
let r = extract_json_body(&mut req).await?;
|
2021-02-16 22:15:54 -08:00
|
|
|
let r: json::PostSignalsRequest =
|
|
|
|
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
|
2019-06-14 16:11:12 -07:00
|
|
|
let mut l = self.db.lock();
|
|
|
|
let now = recording::Time::new(self.db.clocks().realtime());
|
|
|
|
let start = r.start_time_90k.map(recording::Time).unwrap_or(now);
|
|
|
|
let end = match r.end_base {
|
2021-02-16 22:15:54 -08:00
|
|
|
json::PostSignalsEndBase::Epoch => {
|
|
|
|
recording::Time(r.rel_end_time_90k.ok_or_else(|| {
|
|
|
|
bad_req("must specify rel_end_time_90k when end_base is epoch")
|
|
|
|
})?)
|
|
|
|
}
|
2019-06-14 16:11:12 -07:00
|
|
|
json::PostSignalsEndBase::Now => {
|
|
|
|
now + recording::Duration(r.rel_end_time_90k.unwrap_or(0))
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2019-06-14 16:11:12 -07:00
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
l.update_signals(start..end, &r.signal_ids, &r.states)
|
|
|
|
.map_err(from_base_error)?;
|
|
|
|
serve_json(&req, &json::PostSignalsResponse { time_90k: now.0 })
|
2019-06-14 16:11:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn get_signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
|
2021-02-16 22:15:54 -08:00
|
|
|
let mut time = recording::Time::min_value()..recording::Time::max_value();
|
2019-06-06 16:18:13 -07:00
|
|
|
if let Some(q) = req.uri().query() {
|
|
|
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
|
|
|
let (key, value) = (key.borrow(), value.borrow());
|
|
|
|
match key {
|
|
|
|
"startTime90k" => {
|
|
|
|
time.start = recording::Time::parse(value)
|
|
|
|
.map_err(|_| bad_req("unparseable startTime90k"))?
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
2019-06-06 16:18:13 -07:00
|
|
|
"endTime90k" => {
|
|
|
|
time.end = recording::Time::parse(value)
|
|
|
|
.map_err(|_| bad_req("unparseable endTime90k"))?
|
2021-02-16 22:15:54 -08:00
|
|
|
}
|
|
|
|
_ => {}
|
2019-06-06 16:18:13 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut signals = json::Signals::default();
|
2021-02-16 22:15:54 -08:00
|
|
|
self.db
|
|
|
|
.lock()
|
|
|
|
.list_changes_by_time(time, &mut |c: &db::signal::ListStateChangesRow| {
|
|
|
|
signals.times_90k.push(c.when.0);
|
|
|
|
signals.signal_ids.push(c.signal);
|
|
|
|
signals.states.push(c.state);
|
|
|
|
});
|
2019-06-06 16:18:13 -07:00
|
|
|
serve_json(req, &signals)
|
|
|
|
}
|
|
|
|
|
2021-03-06 05:16:09 -08:00
|
|
|
/// Authenticates the session (if any) and returns a Caller.
|
|
|
|
///
|
|
|
|
/// If there's no session,
|
|
|
|
/// 1. if `allow_unauthenticated_permissions` is configured, returns okay
|
|
|
|
/// with those permissions.
|
|
|
|
/// 2. if the caller specifies `unauth_path`, returns okay with no
|
|
|
|
/// permissions.
|
|
|
|
/// 3. returns `Unauthenticated` error otherwise.
|
|
|
|
///
|
|
|
|
/// Does no authorization. That is, this doesn't check that the returned
|
|
|
|
/// permissions are sufficient for whatever operation the caller is
|
|
|
|
/// performing.
|
2021-02-16 22:15:54 -08:00
|
|
|
fn authenticate(
|
|
|
|
&self,
|
|
|
|
req: &Request<hyper::Body>,
|
|
|
|
unauth_path: bool,
|
|
|
|
) -> Result<Caller, base::Error> {
|
2018-11-25 21:31:50 -08:00
|
|
|
if let Some(sid) = extract_sid(req) {
|
|
|
|
let authreq = self.authreq(req);
|
2019-06-19 15:17:50 -07:00
|
|
|
|
2021-03-06 05:16:09 -08:00
|
|
|
match self
|
2021-02-16 22:15:54 -08:00
|
|
|
.db
|
|
|
|
.lock()
|
|
|
|
.authenticate_session(authreq.clone(), &sid.hash())
|
|
|
|
{
|
2021-03-06 05:16:09 -08:00
|
|
|
Ok((s, u)) => {
|
|
|
|
return Ok(Caller {
|
|
|
|
permissions: s.permissions.clone(),
|
|
|
|
session: Some(json::Session {
|
|
|
|
username: u.username.clone(),
|
|
|
|
csrf: s.csrf(),
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
Err(e) if e.kind() == base::ErrorKind::Unauthenticated => {
|
|
|
|
// Log the specific reason this session is unauthenticated.
|
|
|
|
// Don't let the API client see it, as it may have a
|
|
|
|
// revocation reason that isn't for their eyes.
|
|
|
|
warn!("Session authentication failed: {:?}", &e);
|
|
|
|
}
|
|
|
|
Err(e) => return Err(e),
|
|
|
|
};
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
2019-06-19 15:17:50 -07:00
|
|
|
|
|
|
|
if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
|
|
|
|
return Ok(Caller {
|
|
|
|
permissions: s.clone(),
|
|
|
|
session: None,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if unauth_path {
|
|
|
|
return Ok(Caller {
|
|
|
|
permissions: db::Permissions::default(),
|
|
|
|
session: None,
|
2021-02-16 22:15:54 -08:00
|
|
|
});
|
2019-06-19 15:17:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bail_t!(Unauthenticated, "unauthenticated");
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-29 21:20:14 -07:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
|
|
struct StaticFileRequest<'a> {
|
|
|
|
path: &'a str,
|
|
|
|
immutable: bool,
|
|
|
|
mime: &'static str,
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
2020-05-29 21:20:14 -07:00
|
|
|
impl<'a> StaticFileRequest<'a> {
|
|
|
|
fn parse(path: &'a str) -> Option<Self> {
|
|
|
|
if !path.starts_with("/") {
|
|
|
|
return None;
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
2017-10-21 21:54:27 -07:00
|
|
|
|
2020-05-29 21:20:14 -07:00
|
|
|
let (path, immutable) = match &path[1..] {
|
|
|
|
"" => ("index.html", false),
|
|
|
|
p => (p, true),
|
2018-02-11 22:45:51 -08:00
|
|
|
};
|
2018-11-25 21:31:50 -08:00
|
|
|
|
2020-05-29 21:20:14 -07:00
|
|
|
let last_dot = match path.rfind('.') {
|
|
|
|
None => return None,
|
|
|
|
Some(d) => d,
|
2017-10-21 21:54:27 -07:00
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
let ext = &path[last_dot + 1..];
|
2020-05-29 21:20:14 -07:00
|
|
|
let mime = match ext {
|
|
|
|
"html" => "text/html",
|
|
|
|
"ico" => "image/x-icon",
|
|
|
|
"js" | "map" => "text/javascript",
|
|
|
|
"json" => "application/json",
|
|
|
|
"png" => "image/png",
|
|
|
|
"webapp" => "application/x-web-app-manifest+json",
|
2021-02-16 22:15:54 -08:00
|
|
|
_ => return None,
|
2020-02-28 21:41:31 -08:00
|
|
|
};
|
2019-06-14 16:11:12 -07:00
|
|
|
|
2020-05-29 21:20:14 -07:00
|
|
|
Some(StaticFileRequest {
|
|
|
|
path,
|
|
|
|
immutable,
|
|
|
|
mime,
|
|
|
|
})
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-02-16 22:15:54 -08:00
|
|
|
use super::{Segments, StaticFileRequest};
|
2018-12-28 21:53:29 -06:00
|
|
|
use db::testutil::{self, TestDb};
|
2020-01-08 23:04:36 -08:00
|
|
|
use futures::future::FutureExt;
|
2018-12-28 21:53:29 -06:00
|
|
|
use log::info;
|
2018-11-25 21:31:50 -08:00
|
|
|
use std::collections::HashMap;
|
2020-04-17 23:02:02 -07:00
|
|
|
use std::str::FromStr;
|
2020-05-30 19:08:54 -07:00
|
|
|
use std::sync::Arc;
|
2016-11-25 14:34:00 -08:00
|
|
|
|
2018-11-25 21:31:50 -08:00
|
|
|
struct Server {
|
2018-12-28 21:53:29 -06:00
|
|
|
db: TestDb<base::clock::RealClocks>,
|
2018-11-25 21:31:50 -08:00
|
|
|
base_url: String,
|
|
|
|
//test_camera_uuid: Uuid,
|
|
|
|
handle: Option<::std::thread::JoinHandle<()>>,
|
2020-01-08 23:04:36 -08:00
|
|
|
shutdown_tx: Option<futures::channel::oneshot::Sender<()>>,
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Server {
|
2019-06-19 15:17:50 -07:00
|
|
|
fn new(allow_unauthenticated_permissions: Option<db::Permissions>) -> Server {
|
2018-12-28 21:53:29 -06:00
|
|
|
let db = TestDb::new(base::clock::RealClocks {});
|
2020-01-08 23:04:36 -08:00
|
|
|
let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel::<()>();
|
2021-02-16 22:15:54 -08:00
|
|
|
let service = Arc::new(
|
|
|
|
super::Service::new(super::Config {
|
|
|
|
db: db.db.clone(),
|
|
|
|
ui_dir: None,
|
|
|
|
allow_unauthenticated_permissions,
|
|
|
|
trust_forward_hdrs: true,
|
|
|
|
time_zone_name: "".to_owned(),
|
|
|
|
})
|
|
|
|
.unwrap(),
|
|
|
|
);
|
2020-01-08 23:04:36 -08:00
|
|
|
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
|
|
|
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
2020-05-30 19:08:54 -07:00
|
|
|
let s = Arc::clone(&service);
|
|
|
|
move |req| Arc::clone(&s).serve(req)
|
2020-01-08 23:04:36 -08:00
|
|
|
}))
|
|
|
|
});
|
2020-01-09 20:06:30 -08:00
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
let handle = ::std::thread::spawn(move || {
|
2020-01-08 23:04:36 -08:00
|
|
|
let addr = ([127, 0, 0, 1], 0).into();
|
2021-01-27 11:47:52 -08:00
|
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let srv = {
|
|
|
|
let _guard = rt.enter();
|
2021-02-16 22:15:54 -08:00
|
|
|
hyper::server::Server::bind(&addr)
|
|
|
|
.tcp_nodelay(true)
|
|
|
|
.serve(make_svc)
|
2021-01-27 11:47:52 -08:00
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
2020-01-09 20:06:30 -08:00
|
|
|
tx.send(addr).unwrap();
|
2021-02-16 22:15:54 -08:00
|
|
|
rt.block_on(srv.with_graceful_shutdown(shutdown_rx.map(|_| ())))
|
|
|
|
.unwrap();
|
2018-11-25 21:31:50 -08:00
|
|
|
});
|
2020-01-09 20:06:30 -08:00
|
|
|
let addr = rx.recv().unwrap();
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
// Create a user.
|
|
|
|
let mut c = db::UserChange::add_user("slamb".to_owned());
|
|
|
|
c.set_password("hunter2".to_owned());
|
|
|
|
db.db.lock().apply_user_change(c).unwrap();
|
|
|
|
|
|
|
|
Server {
|
|
|
|
db,
|
|
|
|
base_url: format!("http://{}:{}", addr.ip(), addr.port()),
|
|
|
|
handle: Some(handle),
|
|
|
|
shutdown_tx: Some(shutdown_tx),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for Server {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
self.shutdown_tx.take().unwrap().send(()).unwrap();
|
|
|
|
self.handle.take().unwrap().join().unwrap()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
struct SessionCookie(Option<String>);
|
|
|
|
|
|
|
|
impl SessionCookie {
|
2020-01-08 23:04:36 -08:00
|
|
|
pub fn new(headers: &reqwest::header::HeaderMap) -> Self {
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut c = SessionCookie::default();
|
|
|
|
c.update(headers);
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
2020-01-08 23:04:36 -08:00
|
|
|
pub fn update(&mut self, headers: &reqwest::header::HeaderMap) {
|
|
|
|
for set_cookie in headers.get_all(reqwest::header::SET_COOKIE) {
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut set_cookie = set_cookie.to_str().unwrap().split("; ");
|
|
|
|
let c = set_cookie.next().unwrap();
|
|
|
|
let mut clear = false;
|
|
|
|
for attr in set_cookie {
|
|
|
|
if attr == "Max-Age=0" {
|
|
|
|
clear = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !c.starts_with("s=") {
|
|
|
|
panic!("unrecognized cookie");
|
|
|
|
}
|
|
|
|
self.0 = if clear { None } else { Some(c.to_owned()) };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Produces a `Cookie` header value.
|
|
|
|
pub fn header(&self) -> String {
|
|
|
|
self.0.as_ref().map(|s| s.as_str()).unwrap_or("").to_owned()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-29 13:06:44 -06:00
|
|
|
#[test]
|
|
|
|
fn paths() {
|
|
|
|
use super::Path;
|
|
|
|
use uuid::Uuid;
|
|
|
|
let cam_uuid = Uuid::parse_str("35144640-ff1e-4619-b0d5-4c74c185741c").unwrap();
|
|
|
|
assert_eq!(Path::decode("/foo"), Path::Static);
|
|
|
|
assert_eq!(Path::decode("/api/"), Path::TopLevel);
|
2021-02-16 22:15:54 -08:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/init/42.mp4"),
|
|
|
|
Path::InitSegment(42, false)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/init/42.mp4.txt"),
|
|
|
|
Path::InitSegment(42, true)
|
|
|
|
);
|
|
|
|
assert_eq!(Path::decode("/api/init/x.mp4"), Path::NotFound); // non-digit
|
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/"),
|
|
|
|
Path::Camera(cam_uuid)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(Path::decode("/api/cameras/asdf/"), Path::NotFound);
|
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/recordings"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamRecordings(cam_uuid, db::StreamType::MAIN)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/sub/recordings"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamRecordings(cam_uuid, db::StreamType::SUB)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/junk/recordings"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::NotFound
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/view.mp4"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamViewMp4(cam_uuid, db::StreamType::MAIN, false)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/view.mp4.txt"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamViewMp4(cam_uuid, db::StreamType::MAIN, true)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/view.m4s"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamViewMp4Segment(cam_uuid, db::StreamType::MAIN, false)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/view.m4s.txt"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamViewMp4Segment(cam_uuid, db::StreamType::MAIN, true)
|
|
|
|
);
|
2019-01-21 15:58:52 -08:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/live.m4s"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::StreamLiveMp4Segments(cam_uuid, db::StreamType::MAIN)
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(
|
|
|
|
Path::decode("/api/cameras/35144640-ff1e-4619-b0d5-4c74c185741c/main/junk"),
|
2021-02-16 22:15:54 -08:00
|
|
|
Path::NotFound
|
|
|
|
);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(Path::decode("/api/login"), Path::Login);
|
|
|
|
assert_eq!(Path::decode("/api/logout"), Path::Logout);
|
2019-06-06 16:18:13 -07:00
|
|
|
assert_eq!(Path::decode("/api/signals"), Path::Signals);
|
2018-12-29 13:06:44 -06:00
|
|
|
assert_eq!(Path::decode("/api/junk"), Path::NotFound);
|
|
|
|
}
|
|
|
|
|
2020-05-29 21:20:14 -07:00
|
|
|
#[test]
|
|
|
|
fn static_file() {
|
|
|
|
testutil::init();
|
|
|
|
let r = StaticFileRequest::parse("/jquery-ui.b6d3d46c828800e78499.js").unwrap();
|
2021-02-16 22:15:54 -08:00
|
|
|
assert_eq!(
|
|
|
|
r,
|
|
|
|
StaticFileRequest {
|
|
|
|
path: "jquery-ui.b6d3d46c828800e78499.js",
|
|
|
|
mime: "text/javascript",
|
|
|
|
immutable: true,
|
|
|
|
}
|
|
|
|
);
|
2020-05-29 21:20:14 -07:00
|
|
|
|
|
|
|
let r = StaticFileRequest::parse("/").unwrap();
|
2021-02-16 22:15:54 -08:00
|
|
|
assert_eq!(
|
|
|
|
r,
|
|
|
|
StaticFileRequest {
|
|
|
|
path: "index.html",
|
|
|
|
mime: "text/html",
|
|
|
|
immutable: false,
|
|
|
|
}
|
|
|
|
);
|
2020-05-29 21:20:14 -07:00
|
|
|
}
|
|
|
|
|
2016-12-20 22:08:18 -08:00
|
|
|
#[test]
|
2021-02-16 22:15:54 -08:00
|
|
|
#[rustfmt::skip]
|
2016-12-20 22:08:18 -08:00
|
|
|
fn test_segments() {
|
|
|
|
testutil::init();
|
2021-02-16 22:15:54 -08:00
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..2, open_id: None, start_time: 0, end_time: None },
|
|
|
|
Segments::from_str("1").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..2, open_id: Some(42), start_time: 0, end_time: None },
|
|
|
|
Segments::from_str("1@42").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..2, open_id: None, start_time: 26, end_time: None },
|
|
|
|
Segments::from_str("1.26-").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..2, open_id: Some(42), start_time: 26, end_time: None },
|
|
|
|
Segments::from_str("1@42.26-").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..2, open_id: None, start_time: 0, end_time: Some(42) },
|
|
|
|
Segments::from_str("1.-42").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..2, open_id: None, start_time: 26, end_time: Some(42) },
|
|
|
|
Segments::from_str("1.26-42").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..6, open_id: None, start_time: 0, end_time: None },
|
|
|
|
Segments::from_str("1-5").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..6, open_id: None, start_time: 26, end_time: None },
|
|
|
|
Segments::from_str("1-5.26-").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..6, open_id: None, start_time: 0, end_time: Some(42) },
|
|
|
|
Segments::from_str("1-5.-42").unwrap()
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
Segments { ids: 1..6, open_id: None, start_time: 26, end_time: Some(42) },
|
|
|
|
Segments::from_str("1-5.26-42").unwrap()
|
|
|
|
);
|
2016-12-20 22:08:18 -08:00
|
|
|
}
|
2018-11-25 21:31:50 -08:00
|
|
|
|
2020-01-09 20:06:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn unauthorized_without_cookie() {
|
2018-11-25 21:31:50 -08:00
|
|
|
testutil::init();
|
2019-06-19 15:17:50 -07:00
|
|
|
let s = Server::new(None);
|
2018-11-25 21:31:50 -08:00
|
|
|
let cli = reqwest::Client::new();
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.get(&format!("{}/api/", &s.base_url))
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
2020-01-09 20:06:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn login() {
|
2018-11-25 21:31:50 -08:00
|
|
|
testutil::init();
|
2019-06-19 15:17:50 -07:00
|
|
|
let s = Server::new(None);
|
2018-11-25 21:31:50 -08:00
|
|
|
let cli = reqwest::Client::new();
|
|
|
|
let login_url = format!("{}/api/login", &s.base_url);
|
|
|
|
|
2020-01-09 20:06:30 -08:00
|
|
|
let resp = cli.get(&login_url).send().await.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
|
2018-11-25 21:31:50 -08:00
|
|
|
|
2020-01-09 20:06:30 -08:00
|
|
|
let resp = cli.post(&login_url).send().await.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
let mut p = HashMap::new();
|
|
|
|
p.insert("username", "slamb");
|
|
|
|
p.insert("password", "asdf");
|
2020-01-09 20:06:30 -08:00
|
|
|
let resp = cli.post(&login_url).json(&p).send().await.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
p.insert("password", "hunter2");
|
2020-01-09 20:06:30 -08:00
|
|
|
let resp = cli.post(&login_url).json(&p).send().await.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
2018-11-25 21:31:50 -08:00
|
|
|
let cookie = SessionCookie::new(resp.headers());
|
|
|
|
info!("cookie: {:?}", cookie);
|
|
|
|
info!("header: {}", cookie.header());
|
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.get(&format!("{}/api/", &s.base_url))
|
|
|
|
.header(reqwest::header::COOKIE, cookie.header())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::OK);
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
|
|
|
|
2020-01-09 20:06:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn logout() {
|
2018-11-25 21:31:50 -08:00
|
|
|
testutil::init();
|
2019-06-19 15:17:50 -07:00
|
|
|
let s = Server::new(None);
|
2018-11-25 21:31:50 -08:00
|
|
|
let cli = reqwest::Client::new();
|
|
|
|
let mut p = HashMap::new();
|
|
|
|
p.insert("username", "slamb");
|
|
|
|
p.insert("password", "hunter2");
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.post(&format!("{}/api/login", &s.base_url))
|
|
|
|
.json(&p)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
2018-11-25 21:31:50 -08:00
|
|
|
let cookie = SessionCookie::new(resp.headers());
|
|
|
|
|
|
|
|
// A GET shouldn't work.
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.get(&format!("{}/api/logout", &s.base_url))
|
|
|
|
.header(reqwest::header::COOKIE, cookie.header())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
// Neither should a POST without a csrf token.
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.post(&format!("{}/api/logout", &s.base_url))
|
|
|
|
.header(reqwest::header::COOKIE, cookie.header())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
2018-11-25 21:31:50 -08:00
|
|
|
|
|
|
|
// But it should work with the csrf token.
|
|
|
|
// Retrieve that from the toplevel API request.
|
2021-02-16 22:15:54 -08:00
|
|
|
let toplevel: serde_json::Value = cli
|
|
|
|
.post(&format!("{}/api/", &s.base_url))
|
|
|
|
.header(reqwest::header::COOKIE, cookie.header())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap()
|
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
let csrf = toplevel
|
|
|
|
.get("session")
|
|
|
|
.unwrap()
|
|
|
|
.get("csrf")
|
|
|
|
.unwrap()
|
|
|
|
.as_str();
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut p = HashMap::new();
|
|
|
|
p.insert("csrf", csrf);
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.post(&format!("{}/api/logout", &s.base_url))
|
|
|
|
.header(reqwest::header::COOKIE, cookie.header())
|
|
|
|
.json(&p)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
2018-11-25 21:31:50 -08:00
|
|
|
let mut updated_cookie = cookie.clone();
|
|
|
|
updated_cookie.update(resp.headers());
|
|
|
|
|
|
|
|
// The cookie should be cleared client-side.
|
|
|
|
assert!(updated_cookie.0.is_none());
|
|
|
|
|
|
|
|
// It should also be invalidated server-side.
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.get(&format!("{}/api/", &s.base_url))
|
|
|
|
.header(reqwest::header::COOKIE, cookie.header())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
2018-11-25 21:31:50 -08:00
|
|
|
}
|
2018-12-28 17:30:33 -06:00
|
|
|
|
2020-01-09 20:06:30 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn view_without_segments() {
|
2018-12-28 17:30:33 -06:00
|
|
|
testutil::init();
|
2019-06-19 15:17:50 -07:00
|
|
|
let mut permissions = db::Permissions::new();
|
|
|
|
permissions.view_video = true;
|
|
|
|
let s = Server::new(Some(permissions));
|
2018-12-28 17:30:33 -06:00
|
|
|
let cli = reqwest::Client::new();
|
2021-02-16 22:15:54 -08:00
|
|
|
let resp = cli
|
|
|
|
.get(&format!(
|
|
|
|
"{}/api/cameras/{}/main/view.mp4",
|
|
|
|
&s.base_url, s.db.test_camera_uuid
|
|
|
|
))
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2020-01-08 23:04:36 -08:00
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
2018-12-28 17:30:33 -06:00
|
|
|
}
|
2016-11-25 14:34:00 -08:00
|
|
|
}
|
2017-02-12 20:37:03 -08:00
|
|
|
|
2021-02-16 22:15:54 -08:00
|
|
|
#[cfg(all(test, feature = "nightly"))]
|
2017-02-12 20:37:03 -08:00
|
|
|
mod bench {
|
|
|
|
extern crate test;
|
|
|
|
|
2018-02-20 23:15:39 -08:00
|
|
|
use db::testutil::{self, TestDb};
|
2017-02-12 20:37:03 -08:00
|
|
|
use hyper;
|
2018-12-28 21:53:29 -06:00
|
|
|
use lazy_static::lazy_static;
|
2020-05-30 19:08:54 -07:00
|
|
|
use std::sync::Arc;
|
2018-02-03 21:56:04 -08:00
|
|
|
use uuid::Uuid;
|
2017-02-12 20:37:03 -08:00
|
|
|
|
|
|
|
struct Server {
|
|
|
|
base_url: String,
|
2018-02-03 21:56:04 -08:00
|
|
|
test_camera_uuid: Uuid,
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Server {
|
|
|
|
fn new() -> Server {
|
2018-03-23 15:16:43 -07:00
|
|
|
let db = TestDb::new(::base::clock::RealClocks {});
|
2018-02-03 21:56:04 -08:00
|
|
|
let test_camera_uuid = db.test_camera_uuid;
|
2017-02-12 20:37:03 -08:00
|
|
|
testutil::add_dummy_recordings_to_db(&db.db, 1440);
|
2021-02-16 22:15:54 -08:00
|
|
|
let service = Arc::new(
|
|
|
|
super::Service::new(super::Config {
|
|
|
|
db: db.db.clone(),
|
|
|
|
ui_dir: None,
|
|
|
|
allow_unauthenticated_permissions: Some(db::Permissions::default()),
|
|
|
|
trust_forward_hdrs: false,
|
|
|
|
time_zone_name: "".to_owned(),
|
|
|
|
})
|
|
|
|
.unwrap(),
|
|
|
|
);
|
2020-01-08 23:04:36 -08:00
|
|
|
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
|
|
|
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
2020-05-30 19:08:54 -07:00
|
|
|
let s = Arc::clone(&service);
|
|
|
|
move |req| Arc::clone(&s).serve(req)
|
2020-01-08 23:04:36 -08:00
|
|
|
}))
|
|
|
|
});
|
2021-01-27 11:47:52 -08:00
|
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let srv = {
|
|
|
|
let _guard = rt.enter();
|
2020-01-08 23:04:36 -08:00
|
|
|
let addr = ([127, 0, 0, 1], 0).into();
|
|
|
|
hyper::server::Server::bind(&addr)
|
|
|
|
.tcp_nodelay(true)
|
|
|
|
.serve(make_svc)
|
2021-01-27 11:47:52 -08:00
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
2017-02-12 20:37:03 -08:00
|
|
|
::std::thread::spawn(move || {
|
2020-01-08 23:04:36 -08:00
|
|
|
rt.block_on(srv).unwrap();
|
2017-02-12 20:37:03 -08:00
|
|
|
});
|
2018-02-03 21:56:04 -08:00
|
|
|
Server {
|
|
|
|
base_url: format!("http://{}:{}", addr.ip(), addr.port()),
|
|
|
|
test_camera_uuid,
|
|
|
|
}
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lazy_static! {
|
2020-08-07 15:30:22 -07:00
|
|
|
static ref SERVER: Server = Server::new();
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
2018-12-28 21:53:29 -06:00
|
|
|
fn serve_stream_recordings(b: &mut test::Bencher) {
|
2017-02-12 20:37:03 -08:00
|
|
|
testutil::init();
|
|
|
|
let server = &*SERVER;
|
2021-02-16 22:15:54 -08:00
|
|
|
let url = reqwest::Url::parse(&format!(
|
|
|
|
"{}/api/cameras/{}/main/recordings",
|
|
|
|
server.base_url, server.test_camera_uuid
|
|
|
|
))
|
|
|
|
.unwrap();
|
2017-11-16 23:01:09 -08:00
|
|
|
let client = reqwest::Client::new();
|
2021-01-27 11:47:52 -08:00
|
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let f = || {
|
2020-02-16 23:58:07 -08:00
|
|
|
rt.block_on(async {
|
|
|
|
let resp = client.get(url.clone()).send().await.unwrap();
|
|
|
|
assert_eq!(resp.status(), reqwest::StatusCode::OK);
|
|
|
|
let _b = resp.bytes().await.unwrap();
|
|
|
|
});
|
2017-03-03 22:26:29 -08:00
|
|
|
};
|
2021-02-16 22:15:54 -08:00
|
|
|
f(); // warm.
|
2017-03-03 22:26:29 -08:00
|
|
|
b.iter(f);
|
2017-02-12 20:37:03 -08:00
|
|
|
}
|
|
|
|
}
|