mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-13 21:12:55 -04:00
upgrade to hyper 0.13 ecosystem
This doesn't take much advantage of async fns so far. For example, the with_{form,json}_body functions are still designed to be used with future combinators when it'd be more natural to call them from async fns now. But it's a start. Similarly, this still uses the old version of reqwest. Small steps. Requires Rust 1.40 now. (1.39 is a requirement of async, and 1.40 is a requirement of http-serve 0.2.0.)
This commit is contained in:
parent
fce0c5b014
commit
8af7bca6c2
@ -23,7 +23,7 @@ matrix:
|
|||||||
script:
|
script:
|
||||||
- ci/script-rust.sh
|
- ci/script-rust.sh
|
||||||
- language: rust
|
- language: rust
|
||||||
rust: 1.36.0
|
rust: 1.40.0
|
||||||
script:
|
script:
|
||||||
- ci/script-rust.sh
|
- ci/script-rust.sh
|
||||||
- language: node_js
|
- language: node_js
|
||||||
|
2724
Cargo.lock
generated
2724
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -20,7 +20,7 @@ members = ["base", "db", "ffmpeg"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
base = { package = "moonfire-base", path = "base" }
|
base = { package = "moonfire-base", path = "base" }
|
||||||
base64 = "0.10.0"
|
base64 = "0.10.0"
|
||||||
bytes = "0.4.6"
|
bytes = "0.5.3"
|
||||||
byteorder = "1.0"
|
byteorder = "1.0"
|
||||||
cstr = "0.1.7"
|
cstr = "0.1.7"
|
||||||
cursive = "0.12"
|
cursive = "0.12"
|
||||||
@ -28,12 +28,11 @@ db = { package = "moonfire-db", path = "db" }
|
|||||||
docopt = "1.0"
|
docopt = "1.0"
|
||||||
failure = "0.1.1"
|
failure = "0.1.1"
|
||||||
ffmpeg = { package = "moonfire-ffmpeg", path = "ffmpeg" }
|
ffmpeg = { package = "moonfire-ffmpeg", path = "ffmpeg" }
|
||||||
futures = "0.1"
|
futures = "0.3"
|
||||||
futures-cpupool = "0.1"
|
|
||||||
fnv = "1.0"
|
fnv = "1.0"
|
||||||
http = "0.1.5"
|
http = "0.2.0"
|
||||||
http-serve = "0.1.0"
|
http-serve = "0.2.0"
|
||||||
hyper = "0.12.9"
|
hyper = "0.13.0"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = { version = "0.4", features = ["release_max_level_info"] }
|
log = { version = "0.4", features = ["release_max_level_info"] }
|
||||||
@ -52,8 +51,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
smallvec = "0.6"
|
smallvec = "0.6"
|
||||||
time = "0.1"
|
time = "0.1"
|
||||||
tokio = "0.1.8"
|
tokio = { version = "0.2.0", features = ["blocking", "macros", "rt-threaded", "signal"] }
|
||||||
tokio-signal = "0.2"
|
|
||||||
url = "1.4"
|
url = "1.4"
|
||||||
uuid = { version = "0.7", features = ["serde", "std", "v4"] }
|
uuid = { version = "0.7", features = ["serde", "std", "v4"] }
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ $ sudo apt-get install \
|
|||||||
tzdata
|
tzdata
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, you need Rust 1.36+ and Cargo. The easiest way to install them is by
|
Next, you need Rust 1.40+ and Cargo. The easiest way to install them is by
|
||||||
following the instructions at [rustup.rs](https://www.rustup.rs/).
|
following the instructions at [rustup.rs](https://www.rustup.rs/).
|
||||||
|
|
||||||
Finally, building the UI requires [yarn](https://yarnpkg.com/en/).
|
Finally, building the UI requires [yarn](https://yarnpkg.com/en/).
|
||||||
|
@ -40,7 +40,7 @@ fi
|
|||||||
NODE_MIN_VERSION="8"
|
NODE_MIN_VERSION="8"
|
||||||
YARN_MIN_VERSION="1.0"
|
YARN_MIN_VERSION="1.0"
|
||||||
CARGO_MIN_VERSION="0.2"
|
CARGO_MIN_VERSION="0.2"
|
||||||
RUSTC_MIN_VERSION="1.36"
|
RUSTC_MIN_VERSION="1.40"
|
||||||
|
|
||||||
normalizeDirPath()
|
normalizeDirPath()
|
||||||
{
|
{
|
||||||
|
52
src/body.rs
52
src/body.rs
@ -32,77 +32,81 @@
|
|||||||
|
|
||||||
use base::Error;
|
use base::Error;
|
||||||
use futures::{Stream, stream};
|
use futures::{Stream, stream};
|
||||||
use hyper::body::Payload;
|
use reffers::ARefss;
|
||||||
use reffers::ARefs;
|
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
pub struct Chunk(ARefs<'static, [u8]>);
|
pub struct Chunk(ARefss<'static, [u8]>);
|
||||||
|
|
||||||
//pub type CompatError = ::failure::Compat<Error>;
|
//pub type CompatError = ::failure::Compat<Error>;
|
||||||
pub type BoxedError = Box<dyn StdError + Send + Sync>;
|
pub type BoxedError = Box<dyn StdError + Send + Sync>;
|
||||||
pub type BodyStream = Box<dyn Stream<Item = Chunk, Error = BoxedError> + Send + 'static>;
|
pub type BodyStream = Box<dyn Stream<Item = Result<Chunk, BoxedError>> + Send + Sync + 'static>;
|
||||||
|
|
||||||
pub fn wrap_error(e: Error) -> BoxedError {
|
pub fn wrap_error(e: Error) -> BoxedError {
|
||||||
Box::new(e.compat())
|
Box::new(e.compat())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ARefs<'static, [u8]>> for Chunk {
|
impl From<ARefss<'static, [u8]>> for Chunk {
|
||||||
fn from(r: ARefs<'static, [u8]>) -> Self { Chunk(r) }
|
fn from(r: ARefss<'static, [u8]>) -> Self { Chunk(r) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static [u8]> for Chunk {
|
impl From<&'static [u8]> for Chunk {
|
||||||
fn from(r: &'static [u8]) -> Self { Chunk(ARefs::new(r)) }
|
fn from(r: &'static [u8]) -> Self { Chunk(ARefss::new(r)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static str> for Chunk {
|
impl From<&'static str> for Chunk {
|
||||||
fn from(r: &'static str) -> Self { Chunk(ARefs::new(r.as_bytes())) }
|
fn from(r: &'static str) -> Self { Chunk(ARefss::new(r.as_bytes())) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Chunk {
|
impl From<String> for Chunk {
|
||||||
fn from(r: String) -> Self { Chunk(ARefs::new(r.into_bytes()).map(|v| &v[..])) }
|
fn from(r: String) -> Self { Chunk(ARefss::new(r.into_bytes()).map(|v| &v[..])) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<u8>> for Chunk {
|
impl From<Vec<u8>> for Chunk {
|
||||||
fn from(r: Vec<u8>) -> Self { Chunk(ARefs::new(r).map(|v| &v[..])) }
|
fn from(r: Vec<u8>) -> Self { Chunk(ARefss::new(r).map(|v| &v[..])) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::bytes::Buf for Chunk {
|
impl ::bytes::Buf for Chunk {
|
||||||
fn remaining(&self) -> usize { self.0.len() }
|
fn remaining(&self) -> usize { self.0.len() }
|
||||||
fn bytes(&self) -> &[u8] { &*self.0 }
|
fn bytes(&self) -> &[u8] { &*self.0 }
|
||||||
fn advance(&mut self, cnt: usize) {
|
fn advance(&mut self, cnt: usize) {
|
||||||
self.0 = ::std::mem::replace(&mut self.0, ARefs::new(&[][..])).map(|b| &b[cnt..]);
|
self.0 = ::std::mem::replace(&mut self.0, ARefss::new(&[][..])).map(|b| &b[cnt..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Body(BodyStream);
|
pub struct Body(Pin<BodyStream>);
|
||||||
|
|
||||||
impl Payload for Body {
|
impl hyper::body::HttpBody for Body {
|
||||||
type Data = Chunk;
|
type Data = Chunk;
|
||||||
type Error = BoxedError;
|
type Error = BoxedError;
|
||||||
|
|
||||||
fn poll_data(&mut self) -> ::futures::Poll<Option<Self::Data>, Self::Error> {
|
fn poll_data(self: Pin<&mut Self>, cx: &mut std::task::Context)
|
||||||
self.0.poll()
|
-> std::task::Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||||
|
// This is safe because the pin is not structural.
|
||||||
|
// https://doc.rust-lang.org/std/pin/#pinning-is-not-structural-for-field
|
||||||
|
// (The field _holds_ a pin, but isn't itself pinned.)
|
||||||
|
unsafe { self.get_unchecked_mut() }.0.as_mut().poll_next(cx)
|
||||||
|
//Pin::from(self.0).as_mut().poll_next(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_trailers(self: Pin<&mut Self>, _cx: &mut std::task::Context)
|
||||||
|
-> std::task::Poll<Result<Option<http::header::HeaderMap>, Self::Error>> {
|
||||||
|
std::task::Poll::Ready(Ok(None))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BodyStream> for Body {
|
impl From<BodyStream> for Body {
|
||||||
fn from(b: BodyStream) -> Self { Body(b) }
|
fn from(b: BodyStream) -> Self { Body(Pin::from(b)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Into<Chunk>> From<C> for Body {
|
impl<C: Into<Chunk>> From<C> for Body {
|
||||||
fn from(c: C) -> Self {
|
fn from(c: C) -> Self {
|
||||||
Body(Box::new(stream::once(Ok(c.into()))))
|
Body(Box::pin(stream::once(futures::future::ok(c.into()))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for Body {
|
impl From<Error> for Body {
|
||||||
fn from(e: Error) -> Self {
|
fn from(e: Error) -> Self {
|
||||||
Body(Box::new(stream::once(Err(wrap_error(e)))))
|
Body(Box::pin(stream::once(futures::future::err(wrap_error(e)))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//impl<C: Into<Chunk>> From<C> for Body {
|
|
||||||
// fn from(c: C) -> Self {
|
|
||||||
// Body(Box::new(stream::once(Ok(c.into()))))
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
@ -35,15 +35,16 @@ use crate::web;
|
|||||||
use db::{dir, writer};
|
use db::{dir, writer};
|
||||||
use failure::{Error, ResultExt, bail};
|
use failure::{Error, ResultExt, bail};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use futures::{Future, Stream};
|
use futures::future::FutureExt;
|
||||||
use log::{error, info, warn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use log::{info, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::error::Error as StdError;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tokio;
|
use tokio;
|
||||||
use tokio_signal::unix::{Signal, SIGINT, SIGTERM};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
|
||||||
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
|
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
|
||||||
// They seem to be correct for Linux and macOS at least.
|
// They seem to be correct for Linux and macOS at least.
|
||||||
@ -92,14 +93,6 @@ struct Args {
|
|||||||
flag_trust_forward_hdrs: bool,
|
flag_trust_forward_hdrs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_shutdown() -> impl Future<Item = (), Error = ()> + Send {
|
|
||||||
let int = Signal::new(SIGINT).flatten_stream().into_future();
|
|
||||||
let term = Signal::new(SIGTERM).flatten_stream().into_future();
|
|
||||||
int.select(term)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trim_zoneinfo(p: &str) -> &str {
|
fn trim_zoneinfo(p: &str) -> &str {
|
||||||
for zp in &ZONEINFO_PATHS {
|
for zp in &ZONEINFO_PATHS {
|
||||||
if p.starts_with(zp) {
|
if p.starts_with(zp) {
|
||||||
@ -173,7 +166,8 @@ struct Syncer {
|
|||||||
join: thread::JoinHandle<()>,
|
join: thread::JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> Result<(), Error> {
|
#[tokio::main]
|
||||||
|
pub async fn run() -> Result<(), Error> {
|
||||||
let args: Args = super::parse_args(USAGE)?;
|
let args: Args = super::parse_args(USAGE)?;
|
||||||
let clocks = clock::RealClocks {};
|
let clocks = clock::RealClocks {};
|
||||||
let (_db_dir, conn) = super::open_conn(
|
let (_db_dir, conn) = super::open_conn(
|
||||||
@ -274,18 +268,29 @@ pub fn run() -> Result<(), Error> {
|
|||||||
|
|
||||||
// Start the web interface.
|
// Start the web interface.
|
||||||
let addr = args.flag_http_addr.parse().unwrap();
|
let addr = args.flag_http_addr.parse().unwrap();
|
||||||
let server = ::hyper::server::Server::bind(&addr).tcp_nodelay(true).serve(
|
let make_svc = make_service_fn(move |_conn| {
|
||||||
move || Ok::<_, Box<dyn StdError + Send + Sync>>(s.clone()));
|
futures::future::ok::<_, std::convert::Infallible>(service_fn({
|
||||||
|
let mut s = s.clone();
|
||||||
|
move |req| Pin::from(s.serve(req))
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
let server = ::hyper::server::Server::bind(&addr)
|
||||||
|
.tcp_nodelay(true)
|
||||||
|
.serve(make_svc);
|
||||||
|
|
||||||
let shutdown = setup_shutdown().shared();
|
let mut int = signal(SignalKind::interrupt())?;
|
||||||
|
let mut term = signal(SignalKind::terminate())?;
|
||||||
|
let shutdown = futures::future::select(
|
||||||
|
Box::pin(int.recv()),
|
||||||
|
Box::pin(term.recv()));
|
||||||
|
|
||||||
|
let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel();
|
||||||
|
let server = server.with_graceful_shutdown(shutdown_rx.map(|_| ()));
|
||||||
|
let server_handle = tokio::spawn(server);
|
||||||
|
|
||||||
info!("Ready to serve HTTP requests");
|
info!("Ready to serve HTTP requests");
|
||||||
let reactor = ::std::thread::spawn({
|
shutdown.await;
|
||||||
let shutdown = shutdown.clone();
|
shutdown_tx.send(()).unwrap();
|
||||||
|| tokio::run(server.with_graceful_shutdown(shutdown.map(|_| ()))
|
|
||||||
.map_err(|e| error!("hyper error: {}", e)))
|
|
||||||
});
|
|
||||||
shutdown.wait().unwrap();
|
|
||||||
|
|
||||||
info!("Shutting down streamers.");
|
info!("Shutting down streamers.");
|
||||||
shutdown_streamers.store(true, Ordering::SeqCst);
|
shutdown_streamers.store(true, Ordering::SeqCst);
|
||||||
@ -306,7 +311,7 @@ pub fn run() -> Result<(), Error> {
|
|||||||
db.lock().clear_watches();
|
db.lock().clear_watches();
|
||||||
|
|
||||||
info!("Waiting for HTTP requests to finish.");
|
info!("Waiting for HTTP requests to finish.");
|
||||||
reactor.join().unwrap();
|
server_handle.await??;
|
||||||
info!("Exiting.");
|
info!("Exiting.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
352
src/mp4.rs
352
src/mp4.rs
@ -91,7 +91,7 @@ use log::{debug, error, trace, warn};
|
|||||||
use memmap;
|
use memmap;
|
||||||
use openssl::hash;
|
use openssl::hash;
|
||||||
use parking_lot::Once;
|
use parking_lot::Once;
|
||||||
use reffers::ARefs;
|
use reffers::ARefss;
|
||||||
use crate::slices::{self, Slices};
|
use crate::slices::{self, Slices};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::cell::UnsafeCell;
|
use std::cell::UnsafeCell;
|
||||||
@ -613,7 +613,7 @@ impl Slice {
|
|||||||
|
|
||||||
fn wrap_index<F>(&self, mp4: &File, r: Range<u64>, f: &F) -> Result<Chunk, Error>
|
fn wrap_index<F>(&self, mp4: &File, r: Range<u64>, f: &F) -> Result<Chunk, Error>
|
||||||
where F: Fn(&[u8], SegmentLengths) -> &[u8] {
|
where F: Fn(&[u8], SegmentLengths) -> &[u8] {
|
||||||
let mp4 = ARefs::new(mp4.0.clone());
|
let mp4 = ARefss::new(mp4.0.clone());
|
||||||
let r = r.start as usize .. r.end as usize;
|
let r = r.start as usize .. r.end as usize;
|
||||||
let p = self.p();
|
let p = self.p();
|
||||||
Ok(mp4.try_map(|mp4| Ok::<_, Error>(&mp4.segments[p].get_index(&mp4.db, f)?[r]))?.into())
|
Ok(mp4.try_map(|mp4| Ok::<_, Error>(&mp4.segments[p].get_index(&mp4.db, f)?[r]))?.into())
|
||||||
@ -630,7 +630,7 @@ impl Slice {
|
|||||||
mp4.0.db.lock()
|
mp4.0.db.lock()
|
||||||
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))
|
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))
|
||||||
.err_kind(ErrorKind::Unknown)?;
|
.err_kind(ErrorKind::Unknown)?;
|
||||||
let truns = ARefs::new(truns);
|
let truns = ARefss::new(truns);
|
||||||
Ok(truns.map(|t| &t[r.start as usize .. r.end as usize]).into())
|
Ok(truns.map(|t| &t[r.start as usize .. r.end as usize]).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -641,7 +641,7 @@ impl slices::Slice for Slice {
|
|||||||
|
|
||||||
fn end(&self) -> u64 { return self.0 & 0xFF_FF_FF_FF_FF }
|
fn end(&self) -> u64 { return self.0 & 0xFF_FF_FF_FF_FF }
|
||||||
fn get_range(&self, f: &File, range: Range<u64>, len: u64)
|
fn get_range(&self, f: &File, range: Range<u64>, len: u64)
|
||||||
-> Box<dyn Stream<Item = Self::Chunk, Error = BoxedError> + Send> {
|
-> Box<dyn Stream<Item = Result<Self::Chunk, BoxedError>> + Send + Sync> {
|
||||||
trace!("getting mp4 slice {:?}'s range {:?} / {}", self, range, len);
|
trace!("getting mp4 slice {:?}'s range {:?} / {}", self, range, len);
|
||||||
let p = self.p();
|
let p = self.p();
|
||||||
let res = match self.t() {
|
let res = match self.t() {
|
||||||
@ -651,11 +651,11 @@ impl slices::Slice for Slice {
|
|||||||
Ok(part.into())
|
Ok(part.into())
|
||||||
},
|
},
|
||||||
SliceType::Buf => {
|
SliceType::Buf => {
|
||||||
let r = ARefs::new(f.0.clone());
|
let r = ARefss::new(f.0.clone());
|
||||||
Ok(r.map(|f| &f.buf[p+range.start as usize .. p+range.end as usize]).into())
|
Ok(r.map(|f| &f.buf[p+range.start as usize .. p+range.end as usize]).into())
|
||||||
},
|
},
|
||||||
SliceType::VideoSampleEntry => {
|
SliceType::VideoSampleEntry => {
|
||||||
let r = ARefs::new(f.0.clone());
|
let r = ARefss::new(f.0.clone());
|
||||||
Ok(r.map(|f| &f.video_sample_entries[p]
|
Ok(r.map(|f| &f.video_sample_entries[p]
|
||||||
.data[range.start as usize .. range.end as usize]).into())
|
.data[range.start as usize .. range.end as usize]).into())
|
||||||
},
|
},
|
||||||
@ -667,7 +667,7 @@ impl slices::Slice for Slice {
|
|||||||
SliceType::SubtitleSampleData => f.0.get_subtitle_sample_data(p, range.clone(), len),
|
SliceType::SubtitleSampleData => f.0.get_subtitle_sample_data(p, range.clone(), len),
|
||||||
SliceType::Truns => self.wrap_truns(f, range.clone(), len as usize),
|
SliceType::Truns => self.wrap_truns(f, range.clone(), len as usize),
|
||||||
};
|
};
|
||||||
Box::new(stream::once(res
|
Box::new(stream::once(futures::future::ready(res
|
||||||
.map_err(|e| wrap_error(e))
|
.map_err(|e| wrap_error(e))
|
||||||
.and_then(move |c| {
|
.and_then(move |c| {
|
||||||
if c.remaining() != (range.end - range.start) as usize {
|
if c.remaining() != (range.end - range.start) as usize {
|
||||||
@ -677,7 +677,7 @@ impl slices::Slice for Slice {
|
|||||||
self, range, c.remaining())));
|
self, range, c.remaining())));
|
||||||
}
|
}
|
||||||
Ok(c)
|
Ok(c)
|
||||||
})))
|
}))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_slices(ctx: &File) -> &Slices<Self> { &ctx.0.slices }
|
fn get_slices(ctx: &File) -> &Slices<Self> { &ctx.0.slices }
|
||||||
@ -1444,7 +1444,7 @@ impl FileInner {
|
|||||||
let r = s.s.sample_file_range();
|
let r = s.s.sample_file_range();
|
||||||
pos += r.end - r.start;
|
pos += r.end - r.start;
|
||||||
}
|
}
|
||||||
Ok(ARefs::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
Ok(ARefss::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a `Chunk` of video sample data from disk.
|
/// Gets a `Chunk` of video sample data from disk.
|
||||||
@ -1470,7 +1470,7 @@ impl FileInner {
|
|||||||
.map(&f).err_kind(ErrorKind::Internal)?
|
.map(&f).err_kind(ErrorKind::Internal)?
|
||||||
});
|
});
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
Ok(ARefs::new(mmap).map(|m| m.deref()).into())
|
Ok(ARefss::new(mmap).map(|m| m.deref()).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_subtitle_sample_data(&self, i: usize, r: Range<u64>, l: u64) -> Result<Chunk, Error> {
|
fn get_subtitle_sample_data(&self, i: usize, r: Range<u64>, l: u64) -> Result<Chunk, Error> {
|
||||||
@ -1487,7 +1487,7 @@ impl FileInner {
|
|||||||
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE).err_kind(ErrorKind::Internal)?)
|
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE).err_kind(ErrorKind::Internal)?)
|
||||||
.expect("Vec write shouldn't fail");
|
.expect("Vec write shouldn't fail");
|
||||||
}
|
}
|
||||||
Ok(ARefs::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
Ok(ARefss::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1512,13 +1512,13 @@ impl http_serve::Entity for File {
|
|||||||
}
|
}
|
||||||
mime.extend_from_slice(b"\"");
|
mime.extend_from_slice(b"\"");
|
||||||
hdrs.insert(http::header::CONTENT_TYPE,
|
hdrs.insert(http::header::CONTENT_TYPE,
|
||||||
http::header::HeaderValue::from_shared(mime.freeze()).unwrap());
|
http::header::HeaderValue::from_maybe_shared(mime.freeze()).unwrap());
|
||||||
}
|
}
|
||||||
fn last_modified(&self) -> Option<SystemTime> { Some(self.0.last_modified) }
|
fn last_modified(&self) -> Option<SystemTime> { Some(self.0.last_modified) }
|
||||||
fn etag(&self) -> Option<HeaderValue> { Some(self.0.etag.clone()) }
|
fn etag(&self) -> Option<HeaderValue> { Some(self.0.etag.clone()) }
|
||||||
fn len(&self) -> u64 { self.0.slices.len() }
|
fn len(&self) -> u64 { self.0.slices.len() }
|
||||||
fn get_range(&self, range: Range<u64>)
|
fn get_range(&self, range: Range<u64>)
|
||||||
-> Box<dyn Stream<Item = Self::Data, Error = Self::Error> + Send> {
|
-> Box<dyn Stream<Item = Result<Self::Data, Self::Error>> + Send + Sync> {
|
||||||
self.0.slices.get_range(self, range)
|
self.0.slices.get_range(self, range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1553,41 +1553,41 @@ mod tests {
|
|||||||
use db::recording::{self, TIME_UNITS_PER_SEC};
|
use db::recording::{self, TIME_UNITS_PER_SEC};
|
||||||
use db::testutil::{self, TestDb, TEST_STREAM_ID};
|
use db::testutil::{self, TestDb, TEST_STREAM_ID};
|
||||||
use db::writer;
|
use db::writer;
|
||||||
use futures::Future;
|
use futures::stream::TryStreamExt;
|
||||||
use futures::Stream as FuturesStream;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use openssl::hash;
|
use openssl::hash;
|
||||||
use http_serve::{self, Entity};
|
use http_serve::{self, Entity};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::str;
|
use std::str;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn fill_slice<E: http_serve::Entity>(slice: &mut [u8], e: &E, start: u64)
|
async fn fill_slice<E: http_serve::Entity>(slice: &mut [u8], e: &E, start: u64)
|
||||||
where E::Error : ::std::fmt::Debug {
|
where E::Error : ::std::fmt::Debug {
|
||||||
let mut p = 0;
|
let mut p = 0;
|
||||||
e.get_range(start .. start + slice.len() as u64)
|
Pin::from(e.get_range(start .. start + slice.len() as u64))
|
||||||
.for_each(|chunk| {
|
.try_for_each(|chunk| {
|
||||||
let c: &[u8] = chunk.bytes();
|
let c: &[u8] = chunk.bytes();
|
||||||
slice[p .. p + c.len()].copy_from_slice(c);
|
slice[p .. p + c.len()].copy_from_slice(c);
|
||||||
p += c.len();
|
p += c.len();
|
||||||
Ok::<_, E::Error>(())
|
futures::future::ok::<_, E::Error>(())
|
||||||
})
|
})
|
||||||
.wait()
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the SHA-1 digest of the given `Entity`.
|
/// Returns the SHA-1 digest of the given `Entity`.
|
||||||
fn digest<E: http_serve::Entity>(e: &E) -> hash::DigestBytes
|
async fn digest<E: http_serve::Entity>(e: &E) -> hash::DigestBytes
|
||||||
where E::Error : ::std::fmt::Debug {
|
where E::Error : ::std::fmt::Debug {
|
||||||
e.get_range(0 .. e.len())
|
Pin::from(e.get_range(0 .. e.len()))
|
||||||
.fold(hash::Hasher::new(hash::MessageDigest::sha1()).unwrap(), |mut sha1, chunk| {
|
.try_fold(hash::Hasher::new(hash::MessageDigest::sha1()).unwrap(), |mut sha1, chunk| {
|
||||||
let c: &[u8] = chunk.bytes();
|
let c: &[u8] = chunk.bytes();
|
||||||
sha1.update(c).unwrap();
|
sha1.update(c).unwrap();
|
||||||
Ok::<_, E::Error>(sha1)
|
futures::future::ok::<_, E::Error>(sha1)
|
||||||
})
|
})
|
||||||
.wait()
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.finish()
|
.finish()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -1618,14 +1618,14 @@ mod tests {
|
|||||||
|
|
||||||
/// Pushes the box at the given position onto the stack (returning true), or returns
|
/// Pushes the box at the given position onto the stack (returning true), or returns
|
||||||
/// false if pos == max.
|
/// false if pos == max.
|
||||||
fn internal_push(&mut self, pos: u64, max: u64) -> bool {
|
async fn internal_push(&mut self, pos: u64, max: u64) -> bool {
|
||||||
if pos == max { return false; }
|
if pos == max { return false; }
|
||||||
let mut hdr = [0u8; 16];
|
let mut hdr = [0u8; 16];
|
||||||
fill_slice(&mut hdr[..8], &self.mp4, pos);
|
fill_slice(&mut hdr[..8], &self.mp4, pos).await;
|
||||||
let (len, hdr_len, boxtype_slice) = match BigEndian::read_u32(&hdr[..4]) {
|
let (len, hdr_len, boxtype_slice) = match BigEndian::read_u32(&hdr[..4]) {
|
||||||
0 => (self.mp4.len() - pos, 8, &hdr[4..8]),
|
0 => (self.mp4.len() - pos, 8, &hdr[4..8]),
|
||||||
1 => {
|
1 => {
|
||||||
fill_slice(&mut hdr[8..], &self.mp4, pos + 8);
|
fill_slice(&mut hdr[8..], &self.mp4, pos + 8).await;
|
||||||
(BigEndian::read_u64(&hdr[8..16]), 16, &hdr[4..8])
|
(BigEndian::read_u64(&hdr[8..16]), 16, &hdr[4..8])
|
||||||
},
|
},
|
||||||
l => (l as u64, 8, &hdr[4..8]),
|
l => (l as u64, 8, &hdr[4..8]),
|
||||||
@ -1661,53 +1661,53 @@ mod tests {
|
|||||||
|
|
||||||
/// Gets the specified byte range within the current box (excluding length and type).
|
/// Gets the specified byte range within the current box (excluding length and type).
|
||||||
/// Must not be at EOF.
|
/// Must not be at EOF.
|
||||||
pub fn get(&self, start: u64, buf: &mut [u8]) {
|
pub async fn get(&self, start: u64, buf: &mut [u8]) {
|
||||||
let interior = &self.stack.last().expect("at root").interior;
|
let interior = &self.stack.last().expect("at root").interior;
|
||||||
assert!(start + (buf.len() as u64) <= interior.end - interior.start,
|
assert!(start + (buf.len() as u64) <= interior.end - interior.start,
|
||||||
"path={} start={} buf.len={} interior={:?}",
|
"path={} start={} buf.len={} interior={:?}",
|
||||||
self.path(), start, buf.len(), interior);
|
self.path(), start, buf.len(), interior);
|
||||||
fill_slice(buf, &self.mp4, start+interior.start);
|
fill_slice(buf, &self.mp4, start+interior.start).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all(&self) -> Vec<u8> {
|
pub async fn get_all(&self) -> Vec<u8> {
|
||||||
let interior = self.stack.last().expect("at root").interior.clone();
|
let interior = self.stack.last().expect("at root").interior.clone();
|
||||||
let len = (interior.end - interior.start) as usize;
|
let len = (interior.end - interior.start) as usize;
|
||||||
trace!("get_all: start={}, len={}", interior.start, len);
|
trace!("get_all: start={}, len={}", interior.start, len);
|
||||||
let mut out = Vec::with_capacity(len);
|
let mut out = Vec::with_capacity(len);
|
||||||
unsafe { out.set_len(len) };
|
unsafe { out.set_len(len) };
|
||||||
fill_slice(&mut out[..], &self.mp4, interior.start);
|
fill_slice(&mut out[..], &self.mp4, interior.start).await;
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the specified u32 within the current box (excluding length and type).
|
/// Gets the specified u32 within the current box (excluding length and type).
|
||||||
/// Must not be at EOF.
|
/// Must not be at EOF.
|
||||||
pub fn get_u32(&self, p: u64) -> u32 {
|
pub async fn get_u32(&self, p: u64) -> u32 {
|
||||||
let mut buf = [0u8; 4];
|
let mut buf = [0u8; 4];
|
||||||
self.get(p, &mut buf);
|
self.get(p, &mut buf).await;
|
||||||
BigEndian::read_u32(&buf[..])
|
BigEndian::read_u32(&buf[..])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_u64(&self, p: u64) -> u64 {
|
pub async fn get_u64(&self, p: u64) -> u64 {
|
||||||
let mut buf = [0u8; 8];
|
let mut buf = [0u8; 8];
|
||||||
self.get(p, &mut buf);
|
self.get(p, &mut buf).await;
|
||||||
BigEndian::read_u64(&buf[..])
|
BigEndian::read_u64(&buf[..])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the next box after the current one, or up if the current one is last.
|
/// Navigates to the next box after the current one, or up if the current one is last.
|
||||||
pub fn next(&mut self) -> bool {
|
pub async fn next(&mut self) -> bool {
|
||||||
let old = self.stack.pop().expect("positioned at root; there is no next");
|
let old = self.stack.pop().expect("positioned at root; there is no next");
|
||||||
let max = self.stack.last().map(|b| b.interior.end).unwrap_or_else(|| self.mp4.len());
|
let max = self.stack.last().map(|b| b.interior.end).unwrap_or_else(|| self.mp4.len());
|
||||||
self.internal_push(old.interior.end, max)
|
self.internal_push(old.interior.end, max).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the next box of the given type after the current one, or navigates up if absent.
|
/// Finds the next box of the given type after the current one, or navigates up if absent.
|
||||||
pub fn find(&mut self, boxtype: &[u8]) -> bool {
|
pub async fn find(&mut self, boxtype: &[u8]) -> bool {
|
||||||
trace!("looking for {}", str::from_utf8(boxtype).unwrap());
|
trace!("looking for {}", str::from_utf8(boxtype).unwrap());
|
||||||
loop {
|
loop {
|
||||||
if &self.stack.last().unwrap().boxtype[..] == boxtype {
|
if &self.stack.last().unwrap().boxtype[..] == boxtype {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if !self.next() {
|
if !self.next().await {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1717,10 +1717,11 @@ mod tests {
|
|||||||
pub fn up(&mut self) { self.stack.pop(); }
|
pub fn up(&mut self) { self.stack.pop(); }
|
||||||
|
|
||||||
/// Moves down the stack. Must be positioned on a box with children.
|
/// Moves down the stack. Must be positioned on a box with children.
|
||||||
pub fn down(&mut self) {
|
pub async fn down(&mut self) {
|
||||||
let range = self.stack.last().map(|b| b.interior.clone())
|
let range = self.stack.last().map(|b| b.interior.clone())
|
||||||
.unwrap_or_else(|| 0 .. self.mp4.len());
|
.unwrap_or_else(|| 0 .. self.mp4.len());
|
||||||
assert!(self.internal_push(range.start, range.end), "no children in {}", self.path());
|
assert!(self.internal_push(range.start, range.end).await,
|
||||||
|
"no children in {}", self.path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1732,17 +1733,17 @@ mod tests {
|
|||||||
|
|
||||||
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
||||||
/// exist.
|
/// exist.
|
||||||
fn find_track(mp4: File, track_id: u32) -> Track {
|
async fn find_track(mp4: File, track_id: u32) -> Track {
|
||||||
let mut cursor = BoxCursor::new(mp4);
|
let mut cursor = BoxCursor::new(mp4);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"moov"));
|
assert!(cursor.find(b"moov").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
loop {
|
loop {
|
||||||
assert!(cursor.find(b"trak"));
|
assert!(cursor.find(b"trak").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"tkhd"));
|
assert!(cursor.find(b"tkhd").await);
|
||||||
let mut version = [0u8; 1];
|
let mut version = [0u8; 1];
|
||||||
cursor.get(0, &mut version);
|
cursor.get(0, &mut version).await;
|
||||||
|
|
||||||
// Let id_pos be the offset after the FullBox section of the track_id.
|
// Let id_pos be the offset after the FullBox section of the track_id.
|
||||||
let id_pos = match version[0] {
|
let id_pos = match version[0] {
|
||||||
@ -1750,27 +1751,27 @@ mod tests {
|
|||||||
1 => 16, // ...64-bit times...
|
1 => 16, // ...64-bit times...
|
||||||
v => panic!("unexpected tkhd version {}", v),
|
v => panic!("unexpected tkhd version {}", v),
|
||||||
};
|
};
|
||||||
let cur_track_id = cursor.get_u32(4 + id_pos);
|
let cur_track_id = cursor.get_u32(4 + id_pos).await;
|
||||||
trace!("found moov/trak/tkhd with id {}; want {}", cur_track_id, track_id);
|
trace!("found moov/trak/tkhd with id {}; want {}", cur_track_id, track_id);
|
||||||
if cur_track_id == track_id {
|
if cur_track_id == track_id {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cursor.up();
|
cursor.up();
|
||||||
assert!(cursor.next());
|
assert!(cursor.next().await);
|
||||||
}
|
}
|
||||||
let edts_cursor;
|
let edts_cursor;
|
||||||
if cursor.find(b"edts") {
|
if cursor.find(b"edts").await {
|
||||||
edts_cursor = Some(cursor.clone());
|
edts_cursor = Some(cursor.clone());
|
||||||
cursor.up();
|
cursor.up();
|
||||||
} else {
|
} else {
|
||||||
edts_cursor = None;
|
edts_cursor = None;
|
||||||
};
|
};
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"mdia"));
|
assert!(cursor.find(b"mdia").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"minf"));
|
assert!(cursor.find(b"minf").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"stbl"));
|
assert!(cursor.find(b"stbl").await);
|
||||||
Track{
|
Track{
|
||||||
edts_cursor: edts_cursor,
|
edts_cursor: edts_cursor,
|
||||||
stbl_cursor: cursor,
|
stbl_cursor: cursor,
|
||||||
@ -1833,17 +1834,16 @@ mod tests {
|
|||||||
builder.build(tdb.db.clone(), tdb.dirs_by_stream_id.clone()).unwrap()
|
builder.build(tdb.db.clone(), tdb.dirs_by_stream_id.clone()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_mp4(mp4: &File, dir: &Path) -> String {
|
async fn write_mp4(mp4: &File, dir: &Path) -> String {
|
||||||
let mut filename = dir.to_path_buf();
|
let mut filename = dir.to_path_buf();
|
||||||
filename.push("clip.new.mp4");
|
filename.push("clip.new.mp4");
|
||||||
let mut out = fs::OpenOptions::new().write(true).create_new(true).open(&filename).unwrap();
|
let mut out = fs::OpenOptions::new().write(true).create_new(true).open(&filename).unwrap();
|
||||||
use ::std::io::Write;
|
use ::std::io::Write;
|
||||||
mp4.get_range(0 .. mp4.len())
|
Pin::from(mp4.get_range(0 .. mp4.len()))
|
||||||
.for_each(|chunk| {
|
.try_for_each(|chunk| {
|
||||||
out.write_all(chunk.bytes())?;
|
futures::future::ready(out.write_all(chunk.bytes()).map_err(|e| e.into()))
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
.wait()
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
info!("wrote {:?}", filename);
|
info!("wrote {:?}", filename);
|
||||||
filename.to_str().unwrap().to_string()
|
filename.to_str().unwrap().to_string()
|
||||||
@ -1909,8 +1909,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Tests sample table for a simple video index of all sync frames.
|
/// Tests sample table for a simple video index of all sync frames.
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_all_sync_frames() {
|
async fn test_all_sync_frames() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
let mut r = db::RecordingToInsert::default();
|
let mut r = db::RecordingToInsert::default();
|
||||||
@ -1923,12 +1923,12 @@ mod tests {
|
|||||||
|
|
||||||
// Time range [2, 2+4+6+8) means the 2nd, 3rd, and 4th samples should be included.
|
// Time range [2, 2+4+6+8) means the 2nd, 3rd, and 4th samples should be included.
|
||||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2 .. 2+4+6+8).unwrap();
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2 .. 2+4+6+8).unwrap();
|
||||||
let track = find_track(mp4, 1);
|
let track = find_track(mp4, 1).await;
|
||||||
assert!(track.edts_cursor.is_none());
|
assert!(track.edts_cursor.is_none());
|
||||||
let mut cursor = track.stbl_cursor;
|
let mut cursor = track.stbl_cursor;
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
cursor.find(b"stts");
|
cursor.find(b"stts").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x03, // entry_count
|
0x00, 0x00, 0x00, 0x03, // entry_count
|
||||||
|
|
||||||
@ -1938,8 +1938,8 @@ mod tests {
|
|||||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
cursor.find(b"stsz");
|
cursor.find(b"stsz").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x00, // sample_size
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
||||||
0x00, 0x00, 0x00, 0x03, // sample_count
|
0x00, 0x00, 0x00, 0x03, // sample_count
|
||||||
@ -1950,8 +1950,8 @@ mod tests {
|
|||||||
0x00, 0x00, 0x00, 0x0c,
|
0x00, 0x00, 0x00, 0x0c,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
cursor.find(b"stss");
|
cursor.find(b"stss").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x03, // entry_count
|
0x00, 0x00, 0x00, 0x03, // entry_count
|
||||||
|
|
||||||
@ -1963,8 +1963,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Tests sample table and edit list for a video index with half sync frames.
|
/// Tests sample table and edit list for a video index with half sync frames.
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_half_sync_frames() {
|
async fn test_half_sync_frames() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
let mut r = db::RecordingToInsert::default();
|
let mut r = db::RecordingToInsert::default();
|
||||||
@ -1978,13 +1978,13 @@ mod tests {
|
|||||||
// Time range [2+4+6, 2+4+6+8) means the 4th sample should be included.
|
// Time range [2+4+6, 2+4+6+8) means the 4th sample should be included.
|
||||||
// The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
|
// The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
|
||||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2+4+6 .. 2+4+6+8).unwrap();
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2+4+6 .. 2+4+6+8).unwrap();
|
||||||
let track = find_track(mp4, 1);
|
let track = find_track(mp4, 1).await;
|
||||||
|
|
||||||
// Examine edts. It should skip the 3rd frame.
|
// Examine edts. It should skip the 3rd frame.
|
||||||
let mut cursor = track.edts_cursor.unwrap();
|
let mut cursor = track.edts_cursor.unwrap();
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
cursor.find(b"elst");
|
cursor.find(b"elst").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x01, 0x00, 0x00, 0x00, // version + flags
|
0x01, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x01, // length
|
0x00, 0x00, 0x00, 0x01, // length
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // segment_duration
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // segment_duration
|
||||||
@ -1994,9 +1994,9 @@ mod tests {
|
|||||||
|
|
||||||
// Examine stbl.
|
// Examine stbl.
|
||||||
let mut cursor = track.stbl_cursor;
|
let mut cursor = track.stbl_cursor;
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
cursor.find(b"stts");
|
cursor.find(b"stts").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x02, // entry_count
|
0x00, 0x00, 0x00, 0x02, // entry_count
|
||||||
|
|
||||||
@ -2005,8 +2005,8 @@ mod tests {
|
|||||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
cursor.find(b"stsz");
|
cursor.find(b"stsz").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x00, // sample_size
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
||||||
0x00, 0x00, 0x00, 0x02, // sample_count
|
0x00, 0x00, 0x00, 0x02, // sample_count
|
||||||
@ -2016,8 +2016,8 @@ mod tests {
|
|||||||
0x00, 0x00, 0x00, 0x0c,
|
0x00, 0x00, 0x00, 0x0c,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
cursor.find(b"stss");
|
cursor.find(b"stss").await;
|
||||||
assert_eq!(cursor.get_all(), &[
|
assert_eq!(cursor.get_all().await, &[
|
||||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||||
0x00, 0x00, 0x00, 0x01, // entry_count
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
||||||
|
|
||||||
@ -2026,16 +2026,16 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_no_segments() {
|
async fn test_no_segments() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
let e = make_mp4_from_encoders(Type::Normal, &db, vec![], 0 .. 0).err().unwrap();
|
let e = make_mp4_from_encoders(Type::Normal, &db, vec![], 0 .. 0).err().unwrap();
|
||||||
assert_eq!(e.to_string(), "Invalid argument: no video_sample_entries");
|
assert_eq!(e.to_string(), "Invalid argument: no video_sample_entries");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_multi_segment() {
|
async fn test_multi_segment() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
let mut encoders = Vec::new();
|
let mut encoders = Vec::new();
|
||||||
@ -2054,25 +2054,25 @@ mod tests {
|
|||||||
// This should include samples 3 and 4 only, both sync frames.
|
// This should include samples 3 and 4 only, both sync frames.
|
||||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1+2 .. 1+2+3+4).unwrap();
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1+2 .. 1+2+3+4).unwrap();
|
||||||
let mut cursor = BoxCursor::new(mp4);
|
let mut cursor = BoxCursor::new(mp4);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"moov"));
|
assert!(cursor.find(b"moov").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"trak"));
|
assert!(cursor.find(b"trak").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"mdia"));
|
assert!(cursor.find(b"mdia").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"minf"));
|
assert!(cursor.find(b"minf").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"stbl"));
|
assert!(cursor.find(b"stbl").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"stss"));
|
assert!(cursor.find(b"stss").await);
|
||||||
assert_eq!(cursor.get_u32(4), 2); // entry_count
|
assert_eq!(cursor.get_u32(4).await, 2); // entry_count
|
||||||
assert_eq!(cursor.get_u32(8), 1);
|
assert_eq!(cursor.get_u32(8).await, 1);
|
||||||
assert_eq!(cursor.get_u32(12), 2);
|
assert_eq!(cursor.get_u32(12).await, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_zero_duration_recording() {
|
async fn test_zero_duration_recording() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
let mut encoders = Vec::new();
|
let mut encoders = Vec::new();
|
||||||
@ -2088,17 +2088,17 @@ mod tests {
|
|||||||
|
|
||||||
// Multi-segment recording with an edit list, encoding with a zero-duration recording.
|
// Multi-segment recording with an edit list, encoding with a zero-duration recording.
|
||||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1 .. 2+3).unwrap();
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1 .. 2+3).unwrap();
|
||||||
let track = find_track(mp4, 1);
|
let track = find_track(mp4, 1).await;
|
||||||
let mut cursor = track.edts_cursor.unwrap();
|
let mut cursor = track.edts_cursor.unwrap();
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
cursor.find(b"elst");
|
cursor.find(b"elst").await;
|
||||||
assert_eq!(cursor.get_u32(4), 1); // entry_count
|
assert_eq!(cursor.get_u32(4).await, 1); // entry_count
|
||||||
assert_eq!(cursor.get_u64(8), 4); // segment_duration
|
assert_eq!(cursor.get_u64(8).await, 4); // segment_duration
|
||||||
assert_eq!(cursor.get_u64(16), 1); // media_time
|
assert_eq!(cursor.get_u64(16).await, 1); // media_time
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_media_segment() {
|
async fn test_media_segment() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
let mut r = db::RecordingToInsert::default();
|
let mut r = db::RecordingToInsert::default();
|
||||||
@ -2114,45 +2114,45 @@ mod tests {
|
|||||||
let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![r],
|
let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![r],
|
||||||
2+4+6 .. 2+4+6+8+1).unwrap();
|
2+4+6 .. 2+4+6+8+1).unwrap();
|
||||||
let mut cursor = BoxCursor::new(mp4);
|
let mut cursor = BoxCursor::new(mp4);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
|
|
||||||
let mut mdat = cursor.clone();
|
let mut mdat = cursor.clone();
|
||||||
assert!(mdat.find(b"mdat"));
|
assert!(mdat.find(b"mdat").await);
|
||||||
|
|
||||||
assert!(cursor.find(b"moof"));
|
assert!(cursor.find(b"moof").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"traf"));
|
assert!(cursor.find(b"traf").await);
|
||||||
cursor.down();
|
cursor.down().await;
|
||||||
assert!(cursor.find(b"trun"));
|
assert!(cursor.find(b"trun").await);
|
||||||
assert_eq!(cursor.get_u32(4), 2);
|
assert_eq!(cursor.get_u32(4).await, 2);
|
||||||
assert_eq!(cursor.get_u32(8) as u64, mdat.interior().start);
|
assert_eq!(cursor.get_u32(8).await as u64, mdat.interior().start);
|
||||||
assert_eq!(cursor.get_u32(12), 174063616); // first_sample_flags
|
assert_eq!(cursor.get_u32(12).await, 174063616); // first_sample_flags
|
||||||
assert_eq!(cursor.get_u32(16), 6); // sample duration
|
assert_eq!(cursor.get_u32(16).await, 6); // sample duration
|
||||||
assert_eq!(cursor.get_u32(20), 9); // sample size
|
assert_eq!(cursor.get_u32(20).await, 9); // sample size
|
||||||
assert_eq!(cursor.get_u32(24), 8); // sample duration
|
assert_eq!(cursor.get_u32(24).await, 8); // sample duration
|
||||||
assert_eq!(cursor.get_u32(28), 12); // sample size
|
assert_eq!(cursor.get_u32(28).await, 12); // sample size
|
||||||
assert!(cursor.next());
|
assert!(cursor.next().await);
|
||||||
assert_eq!(cursor.name(), "trun");
|
assert_eq!(cursor.name(), "trun");
|
||||||
assert_eq!(cursor.get_u32(4), 1);
|
assert_eq!(cursor.get_u32(4).await, 1);
|
||||||
assert_eq!(cursor.get_u32(8) as u64, mdat.interior().start + 9 + 12);
|
assert_eq!(cursor.get_u32(8).await as u64, mdat.interior().start + 9 + 12);
|
||||||
assert_eq!(cursor.get_u32(12), 174063616); // first_sample_flags
|
assert_eq!(cursor.get_u32(12).await, 174063616); // first_sample_flags
|
||||||
assert_eq!(cursor.get_u32(16), 1); // sample duration
|
assert_eq!(cursor.get_u32(16).await, 1); // sample duration
|
||||||
assert_eq!(cursor.get_u32(20), 15); // sample size
|
assert_eq!(cursor.get_u32(20).await, 15); // sample size
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_round_trip() {
|
async fn test_round_trip() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
copy_mp4_to_db(&db);
|
copy_mp4_to_db(&db);
|
||||||
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
||||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||||
compare_mp4s(&new_filename, 0, 0);
|
compare_mp4s(&new_filename, 0, 0);
|
||||||
|
|
||||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||||
// combine ranges from the new format with ranges from the old format.
|
// combine ranges from the new format with ranges from the old format.
|
||||||
let sha1 = digest(&mp4);
|
let sha1 = digest(&mp4).await;
|
||||||
assert_eq!("17376879bcf872dd4ad1197225a32d5473fb0dc6", strutil::hex(&sha1[..]));
|
assert_eq!("17376879bcf872dd4ad1197225a32d5473fb0dc6", strutil::hex(&sha1[..]));
|
||||||
const EXPECTED_ETAG: &'static str = "\"953dcf1a61debe785d5dec3ae2d3992a819b68ae\"";
|
const EXPECTED_ETAG: &'static str = "\"953dcf1a61debe785d5dec3ae2d3992a819b68ae\"";
|
||||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||||
@ -2161,19 +2161,19 @@ mod tests {
|
|||||||
db.syncer_join.join().unwrap();
|
db.syncer_join.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_round_trip_with_subtitles() {
|
async fn test_round_trip_with_subtitles() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
copy_mp4_to_db(&db);
|
copy_mp4_to_db(&db);
|
||||||
let mp4 = create_mp4_from_db(&db, 0, 0, true);
|
let mp4 = create_mp4_from_db(&db, 0, 0, true);
|
||||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||||
compare_mp4s(&new_filename, 0, 0);
|
compare_mp4s(&new_filename, 0, 0);
|
||||||
|
|
||||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||||
// combine ranges from the new format with ranges from the old format.
|
// combine ranges from the new format with ranges from the old format.
|
||||||
let sha1 = digest(&mp4);
|
let sha1 = digest(&mp4).await;
|
||||||
assert_eq!("1cd90e0b49747cc54c953153d6709f2fb5df6b14", strutil::hex(&sha1[..]));
|
assert_eq!("1cd90e0b49747cc54c953153d6709f2fb5df6b14", strutil::hex(&sha1[..]));
|
||||||
const EXPECTED_ETAG: &'static str = "\"736655313f10747528a663190517620cdffea6d0\"";
|
const EXPECTED_ETAG: &'static str = "\"736655313f10747528a663190517620cdffea6d0\"";
|
||||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||||
@ -2182,19 +2182,19 @@ mod tests {
|
|||||||
db.syncer_join.join().unwrap();
|
db.syncer_join.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_round_trip_with_edit_list() {
|
async fn test_round_trip_with_edit_list() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
copy_mp4_to_db(&db);
|
copy_mp4_to_db(&db);
|
||||||
let mp4 = create_mp4_from_db(&db, 1, 0, false);
|
let mp4 = create_mp4_from_db(&db, 1, 0, false);
|
||||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||||
compare_mp4s(&new_filename, 1, 0);
|
compare_mp4s(&new_filename, 1, 0);
|
||||||
|
|
||||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||||
// combine ranges from the new format with ranges from the old format.
|
// combine ranges from the new format with ranges from the old format.
|
||||||
let sha1 = digest(&mp4);
|
let sha1 = digest(&mp4).await;
|
||||||
assert_eq!("49893e3997da6bc625a04b09abf4b1ddbe0bc85d", strutil::hex(&sha1[..]));
|
assert_eq!("49893e3997da6bc625a04b09abf4b1ddbe0bc85d", strutil::hex(&sha1[..]));
|
||||||
const EXPECTED_ETAG: &'static str = "\"e87ed99dea31b7c4d1e9186045abaf5ac3c2d2f8\"";
|
const EXPECTED_ETAG: &'static str = "\"e87ed99dea31b7c4d1e9186045abaf5ac3c2d2f8\"";
|
||||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||||
@ -2203,19 +2203,19 @@ mod tests {
|
|||||||
db.syncer_join.join().unwrap();
|
db.syncer_join.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_round_trip_with_shorten() {
|
async fn test_round_trip_with_shorten() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let db = TestDb::new(RealClocks {});
|
let db = TestDb::new(RealClocks {});
|
||||||
copy_mp4_to_db(&db);
|
copy_mp4_to_db(&db);
|
||||||
let mp4 = create_mp4_from_db(&db, 0, 1, false);
|
let mp4 = create_mp4_from_db(&db, 0, 1, false);
|
||||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||||
compare_mp4s(&new_filename, 0, 1);
|
compare_mp4s(&new_filename, 0, 1);
|
||||||
|
|
||||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||||
// combine ranges from the new format with ranges from the old format.
|
// combine ranges from the new format with ranges from the old format.
|
||||||
let sha1 = digest(&mp4);
|
let sha1 = digest(&mp4).await;
|
||||||
assert_eq!("0615feaa3c50a7889fb0e6842de3bd3d3143bc78", strutil::hex(&sha1[..]));
|
assert_eq!("0615feaa3c50a7889fb0e6842de3bd3d3143bc78", strutil::hex(&sha1[..]));
|
||||||
const EXPECTED_ETAG: &'static str = "\"6f0d21a6027b0e444f404a68527dbf5c9a5c1a26\"";
|
const EXPECTED_ETAG: &'static str = "\"6f0d21a6027b0e444f404a68527dbf5c9a5c1a26\"";
|
||||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||||
@ -2232,12 +2232,10 @@ mod bench {
|
|||||||
use base::clock::RealClocks;
|
use base::clock::RealClocks;
|
||||||
use db::recording;
|
use db::recording;
|
||||||
use db::testutil::{self, TestDb};
|
use db::testutil::{self, TestDb};
|
||||||
use futures::{Future, future};
|
use futures::future;
|
||||||
use hyper;
|
use hyper;
|
||||||
use http::header;
|
|
||||||
use http_serve;
|
use http_serve;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::error::Error as StdError;
|
|
||||||
use super::tests::create_mp4_from_db;
|
use super::tests::create_mp4_from_db;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@ -2259,17 +2257,24 @@ mod bench {
|
|||||||
testutil::add_dummy_recordings_to_db(&db.db, 60);
|
testutil::add_dummy_recordings_to_db(&db.db, 60);
|
||||||
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
||||||
let p = mp4.0.initial_sample_byte_pos;
|
let p = mp4.0.initial_sample_byte_pos;
|
||||||
let (tx, rx) = ::std::sync::mpsc::channel();
|
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
||||||
::std::thread::spawn(move || {
|
future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
||||||
let addr = "127.0.0.1:0".parse().unwrap();
|
let mp4 = mp4.clone();
|
||||||
let server = hyper::server::Server::bind(&addr)
|
move |req| future::ok::<hyper::Response<crate::body::Body>, hyper::Error>(
|
||||||
.tcp_nodelay(true)
|
http_serve::serve(mp4.clone(), &req))
|
||||||
.serve(move || Ok::<_, Box<dyn StdError + Send + Sync>>(
|
}))
|
||||||
MyService(mp4.clone())));
|
});
|
||||||
tx.send(server.local_addr()).unwrap();
|
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
::tokio::run(server.map_err(|e| panic!(e)));
|
let srv = rt.enter(|| {
|
||||||
|
let addr = ([127, 0, 0, 1], 0).into();
|
||||||
|
hyper::server::Server::bind(&addr)
|
||||||
|
.tcp_nodelay(true)
|
||||||
|
.serve(make_svc)
|
||||||
|
});
|
||||||
|
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||||
|
::std::thread::spawn(move || {
|
||||||
|
rt.block_on(srv).unwrap();
|
||||||
});
|
});
|
||||||
let addr = rx.recv().unwrap();
|
|
||||||
BenchServer {
|
BenchServer {
|
||||||
url: Url::parse(&format!("http://{}:{}/", addr.ip(), addr.port())).unwrap(),
|
url: Url::parse(&format!("http://{}:{}/", addr.ip(), addr.port())).unwrap(),
|
||||||
generated_len: p,
|
generated_len: p,
|
||||||
@ -2277,19 +2282,6 @@ mod bench {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MyService(super::File);
|
|
||||||
|
|
||||||
impl hyper::service::Service for MyService {
|
|
||||||
type ReqBody = hyper::Body;
|
|
||||||
type ResBody = crate::body::Body;
|
|
||||||
type Error = crate::body::BoxedError;
|
|
||||||
type Future = future::FutureResult<::http::Response<Self::ResBody>, Self::Error>;
|
|
||||||
|
|
||||||
fn call(&mut self, req: ::http::Request<Self::ReqBody>) -> Self::Future {
|
|
||||||
future::ok(http_serve::serve(self.0.clone(), &req))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SERVER: BenchServer = { BenchServer::new() };
|
static ref SERVER: BenchServer = { BenchServer::new() };
|
||||||
}
|
}
|
||||||
@ -2332,7 +2324,7 @@ mod bench {
|
|||||||
let mut run = || {
|
let mut run = || {
|
||||||
let mut resp =
|
let mut resp =
|
||||||
client.get(server.url.clone())
|
client.get(server.url.clone())
|
||||||
.header(header::RANGE, format!("bytes=0-{}", p - 1))
|
.header(reqwest::header::RANGE, format!("bytes=0-{}", p - 1))
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
buf.clear();
|
buf.clear();
|
||||||
|
@ -33,15 +33,16 @@
|
|||||||
use base::format_err_t;
|
use base::format_err_t;
|
||||||
use crate::body::{BoxedError, wrap_error};
|
use crate::body::{BoxedError, wrap_error};
|
||||||
use failure::{Error, bail};
|
use failure::{Error, bail};
|
||||||
use futures::{Stream, stream};
|
use futures::{Stream, stream, stream::StreamExt};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
/// Gets a byte range given a context argument.
|
/// Gets a byte range given a context argument.
|
||||||
/// Each `Slice` instance belongs to a single `Slices`.
|
/// Each `Slice` instance belongs to a single `Slices`.
|
||||||
pub trait Slice : fmt::Debug + Sized + Sync + 'static {
|
pub trait Slice : fmt::Debug + Sized + Sync + 'static {
|
||||||
type Ctx: Send + Clone;
|
type Ctx: Send + Sync + Clone;
|
||||||
type Chunk: Send;
|
type Chunk: Send + Sync;
|
||||||
|
|
||||||
/// The byte position (relative to the start of the `Slices`) of the end of this slice,
|
/// The byte position (relative to the start of the `Slices`) of the end of this slice,
|
||||||
/// exclusive. Note the starting position (and thus length) are inferred from the previous
|
/// exclusive. Note the starting position (and thus length) are inferred from the previous
|
||||||
@ -52,7 +53,7 @@ pub trait Slice : fmt::Debug + Sized + Sync + 'static {
|
|||||||
/// The additional argument `ctx` is as supplied to the `Slices`.
|
/// The additional argument `ctx` is as supplied to the `Slices`.
|
||||||
/// The additional argument `l` is the length of this slice, as determined by the `Slices`.
|
/// The additional argument `l` is the length of this slice, as determined by the `Slices`.
|
||||||
fn get_range(&self, ctx: &Self::Ctx, r: Range<u64>, len: u64)
|
fn get_range(&self, ctx: &Self::Ctx, r: Range<u64>, len: u64)
|
||||||
-> Box<dyn Stream<Item = Self::Chunk, Error = BoxedError> + Send>;
|
-> Box<dyn Stream<Item = Result<Self::Chunk, BoxedError>> + Sync + Send>;
|
||||||
|
|
||||||
fn get_slices(ctx: &Self::Ctx) -> &Slices<Self>;
|
fn get_slices(ctx: &Self::Ctx) -> &Slices<Self>;
|
||||||
}
|
}
|
||||||
@ -111,9 +112,9 @@ impl<S> Slices<S> where S: Slice {
|
|||||||
/// Writes `range` to `out`.
|
/// Writes `range` to `out`.
|
||||||
/// This interface mirrors `http_serve::Entity::write_to`, with the additional `ctx` argument.
|
/// This interface mirrors `http_serve::Entity::write_to`, with the additional `ctx` argument.
|
||||||
pub fn get_range(&self, ctx: &S::Ctx, range: Range<u64>)
|
pub fn get_range(&self, ctx: &S::Ctx, range: Range<u64>)
|
||||||
-> Box<dyn Stream<Item = S::Chunk, Error = BoxedError> + Send> {
|
-> Box<dyn Stream<Item = Result<S::Chunk, BoxedError>> + Sync + Send> {
|
||||||
if range.start > range.end || range.end > self.len {
|
if range.start > range.end || range.end > self.len {
|
||||||
return Box::new(stream::once(Err(wrap_error(format_err_t!(
|
return Box::new(stream::once(futures::future::err(wrap_error(format_err_t!(
|
||||||
Internal, "Bad range {:?} for slice of length {}", range, self.len)))));
|
Internal, "Bad range {:?} for slice of length {}", range, self.len)))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,15 +134,15 @@ impl<S> Slices<S> where S: Slice {
|
|||||||
let (body, min_end);
|
let (body, min_end);
|
||||||
{
|
{
|
||||||
let self_ = S::get_slices(&c);
|
let self_ = S::get_slices(&c);
|
||||||
if i == self_.slices.len() { return None }
|
if i == self_.slices.len() { return futures::future::ready(None) }
|
||||||
let s = &self_.slices[i];
|
let s = &self_.slices[i];
|
||||||
if range.end == slice_start + start_pos { return None }
|
if range.end == slice_start + start_pos { return futures::future::ready(None) }
|
||||||
let s_end = s.end();
|
let s_end = s.end();
|
||||||
min_end = ::std::cmp::min(range.end, s_end);
|
min_end = ::std::cmp::min(range.end, s_end);
|
||||||
let l = s_end - slice_start;
|
let l = s_end - slice_start;
|
||||||
body = s.get_range(&c, start_pos .. min_end - slice_start, l);
|
body = s.get_range(&c, start_pos .. min_end - slice_start, l);
|
||||||
};
|
};
|
||||||
Some(Ok::<_, BoxedError>((body, (c, i+1, 0, min_end))))
|
futures::future::ready(Some((Pin::from(body), (c, i+1, 0, min_end))))
|
||||||
});
|
});
|
||||||
Box::new(bodies.flatten())
|
Box::new(bodies.flatten())
|
||||||
}
|
}
|
||||||
@ -151,10 +152,10 @@ impl<S> Slices<S> where S: Slice {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::body::BoxedError;
|
use crate::body::BoxedError;
|
||||||
use db::testutil;
|
use db::testutil;
|
||||||
use futures::{Future, Stream};
|
use futures::stream::{self, Stream, TryStreamExt};
|
||||||
use futures::stream;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
use std::pin::Pin;
|
||||||
use super::{Slice, Slices};
|
use super::{Slice, Slices};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
@ -176,8 +177,8 @@ mod tests {
|
|||||||
fn end(&self) -> u64 { self.end }
|
fn end(&self) -> u64 { self.end }
|
||||||
|
|
||||||
fn get_range(&self, _ctx: &&'static Slices<FakeSlice>, r: Range<u64>, _l: u64)
|
fn get_range(&self, _ctx: &&'static Slices<FakeSlice>, r: Range<u64>, _l: u64)
|
||||||
-> Box<dyn Stream<Item = FakeChunk, Error = BoxedError> + Send> {
|
-> Box<dyn Stream<Item = Result<FakeChunk, BoxedError>> + Send + Sync> {
|
||||||
Box::new(stream::once(Ok(FakeChunk{slice: self.name, range: r})))
|
Box::new(stream::once(futures::future::ok(FakeChunk{slice: self.name, range: r})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_slices(ctx: &&'static Slices<FakeSlice>) -> &'static Slices<Self> { *ctx }
|
fn get_slices(ctx: &&'static Slices<FakeSlice>) -> &'static Slices<Self> { *ctx }
|
||||||
@ -195,33 +196,37 @@ mod tests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_range(r: Range<u64>) -> Vec<FakeChunk> {
|
||||||
|
Pin::from(SLICES.get_range(&&*SLICES, r)).try_collect().await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn size() {
|
pub fn size() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
assert_eq!(5 + 13 + 7 + 17 + 19, SLICES.len());
|
assert_eq!(5 + 13 + 7 + 17 + 19, SLICES.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
pub fn exact_slice() {
|
pub async fn exact_slice() {
|
||||||
// Test writing exactly slice b.
|
// Test writing exactly slice b.
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let out = SLICES.get_range(&&*SLICES, 5 .. 18).collect().wait().unwrap();
|
let out = get_range(5 .. 18).await;
|
||||||
assert_eq!(&[FakeChunk{slice: "b", range: 0 .. 13}], &out[..]);
|
assert_eq!(&[FakeChunk{slice: "b", range: 0 .. 13}], &out[..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
pub fn offset_first() {
|
pub async fn offset_first() {
|
||||||
// Test writing part of slice a.
|
// Test writing part of slice a.
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let out = SLICES.get_range(&&*SLICES, 1 .. 3).collect().wait().unwrap();
|
let out = get_range(1 .. 3).await;
|
||||||
assert_eq!(&[FakeChunk{slice: "a", range: 1 .. 3}], &out[..]);
|
assert_eq!(&[FakeChunk{slice: "a", range: 1 .. 3}], &out[..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
pub fn offset_mid() {
|
pub async fn offset_mid() {
|
||||||
// Test writing part of slice b, all of slice c, and part of slice d.
|
// Test writing part of slice b, all of slice c, and part of slice d.
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let out = SLICES.get_range(&&*SLICES, 17 .. 26).collect().wait().unwrap();
|
let out = get_range(17 .. 26).await;
|
||||||
assert_eq!(&[
|
assert_eq!(&[
|
||||||
FakeChunk{slice: "b", range: 12 .. 13},
|
FakeChunk{slice: "b", range: 12 .. 13},
|
||||||
FakeChunk{slice: "c", range: 0 .. 7},
|
FakeChunk{slice: "c", range: 0 .. 7},
|
||||||
@ -229,11 +234,11 @@ mod tests {
|
|||||||
], &out[..]);
|
], &out[..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
pub fn everything() {
|
pub async fn everything() {
|
||||||
// Test writing the whole Slices.
|
// Test writing the whole Slices.
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let out = SLICES.get_range(&&*SLICES, 0 .. 61).collect().wait().unwrap();
|
let out = get_range(0 .. 61).await;
|
||||||
assert_eq!(&[
|
assert_eq!(&[
|
||||||
FakeChunk{slice: "a", range: 0 .. 5},
|
FakeChunk{slice: "a", range: 0 .. 5},
|
||||||
FakeChunk{slice: "b", range: 0 .. 13},
|
FakeChunk{slice: "b", range: 0 .. 13},
|
||||||
@ -243,10 +248,10 @@ mod tests {
|
|||||||
], &out[..]);
|
], &out[..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
pub fn at_end() {
|
pub async fn at_end() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let out = SLICES.get_range(&&*SLICES, 61 .. 61).collect().wait().unwrap();
|
let out = get_range(61 .. 61).await;
|
||||||
let empty: &[FakeChunk] = &[];
|
let empty: &[FakeChunk] = &[];
|
||||||
assert_eq!(empty, &out[..]);
|
assert_eq!(empty, &out[..]);
|
||||||
}
|
}
|
||||||
|
226
src/web.rs
226
src/web.rs
@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
use base::clock::Clocks;
|
use base::clock::Clocks;
|
||||||
use base::{ErrorKind, ResultExt, bail_t, strutil};
|
use base::{ErrorKind, ResultExt, bail_t, strutil};
|
||||||
|
use bytes::Bytes;
|
||||||
use crate::body::{Body, BoxedError};
|
use crate::body::{Body, BoxedError};
|
||||||
use crate::json;
|
use crate::json;
|
||||||
use crate::mp4;
|
use crate::mp4;
|
||||||
@ -41,8 +42,8 @@ use db::{auth, recording};
|
|||||||
use db::dir::SampleFileDir;
|
use db::dir::SampleFileDir;
|
||||||
use failure::{Error, bail, format_err};
|
use failure::{Error, bail, format_err};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use futures::{Future, Stream, future};
|
use futures::future::{self, Future, TryFutureExt};
|
||||||
use futures_cpupool;
|
use futures::stream::{Stream, StreamExt, TryStreamExt};
|
||||||
use http::{Request, Response, status::StatusCode};
|
use http::{Request, Response, status::StatusCode};
|
||||||
use http_serve;
|
use http_serve;
|
||||||
use http::header::{self, HeaderValue};
|
use http::header::{self, HeaderValue};
|
||||||
@ -56,6 +57,7 @@ use std::fs;
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -68,6 +70,9 @@ lazy_static! {
|
|||||||
Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
|
Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BoxedFuture = Box<dyn Future<Output = Result<Response<Body>, BoxedError>> +
|
||||||
|
Sync + Send + 'static>;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
enum Path {
|
enum Path {
|
||||||
TopLevel, // "/api/"
|
TopLevel, // "/api/"
|
||||||
@ -254,7 +259,6 @@ struct ServiceInner {
|
|||||||
db: Arc<db::Database>,
|
db: Arc<db::Database>,
|
||||||
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
||||||
ui_files: HashMap<String, UiFile>,
|
ui_files: HashMap<String, UiFile>,
|
||||||
pool: futures_cpupool::CpuPool,
|
|
||||||
time_zone_name: String,
|
time_zone_name: String,
|
||||||
allow_unauthenticated_permissions: Option<db::Permissions>,
|
allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||||
trust_forward_hdrs: bool,
|
trust_forward_hdrs: bool,
|
||||||
@ -505,11 +509,12 @@ impl ServiceInner {
|
|||||||
|
|
||||||
fn static_file(&self, req: &Request<::hyper::Body>, path: &str) -> ResponseResult {
|
fn static_file(&self, req: &Request<::hyper::Body>, path: &str) -> ResponseResult {
|
||||||
let s = self.ui_files.get(path).ok_or_else(|| not_found("no such static file"))?;
|
let s = self.ui_files.get(path).ok_or_else(|| not_found("no such static file"))?;
|
||||||
let f = fs::File::open(&s.path).map_err(internal_server_err)?;
|
let f = tokio::task::block_in_place(move || {
|
||||||
|
fs::File::open(&s.path).map_err(internal_server_err)
|
||||||
|
})?;
|
||||||
let mut hdrs = http::HeaderMap::new();
|
let mut hdrs = http::HeaderMap::new();
|
||||||
hdrs.insert(header::CONTENT_TYPE, s.mime.clone());
|
hdrs.insert(header::CONTENT_TYPE, s.mime.clone());
|
||||||
let e = http_serve::ChunkedReadFile::new(f, Some(self.pool.clone()), hdrs)
|
let e = http_serve::ChunkedReadFile::new(f, hdrs).map_err(internal_server_err)?;
|
||||||
.map_err(internal_server_err)?;
|
|
||||||
Ok(http_serve::serve(e, &req))
|
Ok(http_serve::serve(e, &req))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,7 +557,7 @@ impl ServiceInner {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn login(&self, req: &Request<::hyper::Body>, body: hyper::Chunk) -> ResponseResult {
|
fn login(&self, req: &Request<::hyper::Body>, body: Bytes) -> ResponseResult {
|
||||||
let mut username = None;
|
let mut username = None;
|
||||||
let mut password = None;
|
let mut password = None;
|
||||||
for (key, value) in form_urlencoded::parse(&body) {
|
for (key, value) in form_urlencoded::parse(&body) {
|
||||||
@ -583,23 +588,24 @@ impl ServiceInner {
|
|||||||
flags)
|
flags)
|
||||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||||
let s_suffix = if is_secure {
|
let s_suffix = if is_secure {
|
||||||
"; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/"
|
&b"; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
||||||
} else {
|
} else {
|
||||||
"; HttpOnly; SameSite=Strict; Max-Age=2147483648; Path=/"
|
&b"; HttpOnly; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
||||||
};
|
};
|
||||||
let mut encoded = [0u8; 64];
|
let mut encoded = [0u8; 64];
|
||||||
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
||||||
let mut cookie = BytesMut::with_capacity("s=".len() + encoded.len() + s_suffix.len());
|
let mut cookie = BytesMut::with_capacity("s=".len() + encoded.len() + s_suffix.len());
|
||||||
cookie.put("s=");
|
cookie.put(&b"s="[..]);
|
||||||
cookie.put(&encoded[..]);
|
cookie.put(&encoded[..]);
|
||||||
cookie.put(s_suffix);
|
cookie.put(s_suffix);
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header(header::SET_COOKIE, cookie.freeze())
|
.header(header::SET_COOKIE, HeaderValue::from_maybe_shared(cookie.freeze())
|
||||||
|
.expect("cookie can't have invalid bytes"))
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(b""[..].into()).unwrap())
|
.body(b""[..].into()).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logout(&self, req: &Request<hyper::Body>, body: hyper::Chunk) -> ResponseResult {
|
fn logout(&self, req: &Request<hyper::Body>, body: Bytes) -> ResponseResult {
|
||||||
// Parse parameters.
|
// Parse parameters.
|
||||||
let mut csrf = None;
|
let mut csrf = None;
|
||||||
for (key, value) in form_urlencoded::parse(&body) {
|
for (key, value) in form_urlencoded::parse(&body) {
|
||||||
@ -649,7 +655,7 @@ impl ServiceInner {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_signals(&self, req: &Request<hyper::Body>, caller: Caller, body: hyper::Chunk)
|
fn post_signals(&self, req: &Request<hyper::Body>, caller: Caller, body: Bytes)
|
||||||
-> ResponseResult {
|
-> ResponseResult {
|
||||||
if !caller.permissions.update_signals {
|
if !caller.permissions.update_signals {
|
||||||
return Err(plain_response(StatusCode::UNAUTHORIZED, "update_signals required"));
|
return Err(plain_response(StatusCode::UNAUTHORIZED, "update_signals required"));
|
||||||
@ -769,13 +775,10 @@ fn extract_sid(req: &Request<hyper::Body>) -> Option<auth::RawSessionId> {
|
|||||||
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
|
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
|
||||||
///
|
///
|
||||||
/// Use with `and_then` to chain logic which consumes the form body.
|
/// Use with `and_then` to chain logic which consumes the form body.
|
||||||
fn with_form_body(mut req: Request<hyper::Body>)
|
async fn with_form_body(mut req: Request<hyper::Body>)
|
||||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
-> Result<(Request<hyper::Body>, Bytes), Response<Body>> {
|
||||||
Error = Response<Body>> +
|
|
||||||
Send + 'static> {
|
|
||||||
if *req.method() != http::method::Method::POST {
|
if *req.method() != http::method::Method::POST {
|
||||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
return Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected"));
|
||||||
"POST expected")));
|
|
||||||
}
|
}
|
||||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||||
Some(t) if t == "application/x-www-form-urlencoded" => true,
|
Some(t) if t == "application/x-www-form-urlencoded" => true,
|
||||||
@ -783,25 +786,21 @@ fn with_form_body(mut req: Request<hyper::Body>)
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if !correct_mime_type {
|
if !correct_mime_type {
|
||||||
return Box::new(future::err(bad_req(
|
return Err(bad_req("expected application/x-www-form-urlencoded request body"));
|
||||||
"expected application/x-www-form-urlencoded request body")));
|
|
||||||
}
|
}
|
||||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||||
Box::new(b.concat2()
|
match hyper::body::to_bytes(b).await {
|
||||||
.map(|b| (req, b))
|
Ok(b) => Ok((req, b)),
|
||||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
Err(e) => Err(internal_server_err(format_err!("unable to read request body: {}", e))),
|
||||||
e))))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove redundancy with above. Probably better to just always expect requests in json
|
// TODO: remove redundancy with above. Probably better to just always expect requests in json
|
||||||
// format rather than using the form style for login/logout.
|
// format rather than using the form style for login/logout.
|
||||||
fn with_json_body(mut req: Request<hyper::Body>)
|
async fn with_json_body(mut req: Request<hyper::Body>)
|
||||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
-> Result<(Request<hyper::Body>, Bytes), Response<Body>> {
|
||||||
Error = Response<Body>> +
|
|
||||||
Send + 'static> {
|
|
||||||
if *req.method() != http::method::Method::POST {
|
if *req.method() != http::method::Method::POST {
|
||||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
return Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected"));
|
||||||
"POST expected")));
|
|
||||||
}
|
}
|
||||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||||
Some(t) if t == "application/json" => true,
|
Some(t) if t == "application/json" => true,
|
||||||
@ -809,14 +808,13 @@ fn with_json_body(mut req: Request<hyper::Body>)
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if !correct_mime_type {
|
if !correct_mime_type {
|
||||||
return Box::new(future::err(bad_req(
|
return Err(bad_req("expected application/json request body"));
|
||||||
"expected application/json request body")));
|
|
||||||
}
|
}
|
||||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||||
Box::new(b.concat2()
|
match hyper::body::to_bytes(b).await {
|
||||||
.map(|b| (req, b))
|
Ok(b) => Ok((req, b)),
|
||||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
Err(e) => Err(internal_server_err(format_err!("unable to read request body: {}", e))),
|
||||||
e))))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -859,7 +857,6 @@ impl Service {
|
|||||||
db: config.db,
|
db: config.db,
|
||||||
dirs_by_stream_id,
|
dirs_by_stream_id,
|
||||||
ui_files,
|
ui_files,
|
||||||
pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(),
|
|
||||||
allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
|
allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
|
||||||
trust_forward_hdrs: config.trust_forward_hdrs,
|
trust_forward_hdrs: config.trust_forward_hdrs,
|
||||||
time_zone_name: config.time_zone_name,
|
time_zone_name: config.time_zone_name,
|
||||||
@ -914,7 +911,7 @@ impl Service {
|
|||||||
}
|
}
|
||||||
let stream_id;
|
let stream_id;
|
||||||
let open_id;
|
let open_id;
|
||||||
let (sub_tx, sub_rx) = futures::sync::mpsc::unbounded();
|
let (sub_tx, sub_rx) = futures::channel::mpsc::unbounded();
|
||||||
{
|
{
|
||||||
let mut db = self.0.db.lock();
|
let mut db = self.0.db.lock();
|
||||||
open_id = match db.open {
|
open_id = match db.open {
|
||||||
@ -934,9 +931,8 @@ impl Service {
|
|||||||
.expect("stream_id refed by camera");
|
.expect("stream_id refed by camera");
|
||||||
}
|
}
|
||||||
let inner = self.0.clone();
|
let inner = self.0.clone();
|
||||||
let body: crate::body::BodyStream = Box::new(sub_rx
|
let body = sub_rx
|
||||||
.map_err(|()| unreachable!())
|
.map(move |live| -> Result<_, base::Error> {
|
||||||
.and_then(move |live| -> Result<_, base::Error> {
|
|
||||||
let mut builder = mp4::FileBuilder::new(mp4::Type::MediaSegment);
|
let mut builder = mp4::FileBuilder::new(mp4::Type::MediaSegment);
|
||||||
let mut vse_id = None;
|
let mut vse_id = None;
|
||||||
{
|
{
|
||||||
@ -960,7 +956,6 @@ impl Service {
|
|||||||
let mp4 = builder.build(inner.db.clone(), inner.dirs_by_stream_id.clone())?;
|
let mp4 = builder.build(inner.db.clone(), inner.dirs_by_stream_id.clone())?;
|
||||||
let mut hdrs = http::header::HeaderMap::new();
|
let mut hdrs = http::header::HeaderMap::new();
|
||||||
mp4.add_headers(&mut hdrs);
|
mp4.add_headers(&mut hdrs);
|
||||||
//Ok(format!("{:?}\n\n", mp4).into())
|
|
||||||
let mime_type = hdrs.get(http::header::CONTENT_TYPE).unwrap();
|
let mime_type = hdrs.get(http::header::CONTENT_TYPE).unwrap();
|
||||||
let len = mp4.len();
|
let len = mp4.len();
|
||||||
use futures::stream::once;
|
use futures::stream::once;
|
||||||
@ -977,16 +972,17 @@ impl Service {
|
|||||||
live.off_90k.start,
|
live.off_90k.start,
|
||||||
live.off_90k.end,
|
live.off_90k.end,
|
||||||
&vse_id);
|
&vse_id);
|
||||||
let v: Vec<crate::body::BodyStream> = vec![
|
let v: Vec<Pin<crate::body::BodyStream>> = vec![
|
||||||
Box::new(once(Ok(hdr.into()))),
|
Box::pin(once(futures::future::ok(hdr.into()))),
|
||||||
mp4.get_range(0 .. len),
|
Pin::from(mp4.get_range(0 .. len)),
|
||||||
Box::new(once(Ok("\r\n\r\n".into())))
|
Box::pin(once(futures::future::ok("\r\n\r\n".into())))
|
||||||
];
|
];
|
||||||
Ok(futures::stream::iter_ok::<_, crate::body::BoxedError>(v))
|
Ok(futures::stream::iter(v).flatten())
|
||||||
})
|
});
|
||||||
.map_err(|e| Box::new(e.compat()))
|
let body = body.map_err::<BoxedError, _>(|e| Box::new(e.compat()));
|
||||||
.flatten()
|
let _: &dyn Stream<Item = Result<_, BoxedError>> = &body;
|
||||||
.flatten());
|
let body = body.try_flatten();
|
||||||
|
let body: crate::body::BodyStream = Box::new(body);
|
||||||
let body: Body = body.into();
|
let body: Body = body.into();
|
||||||
Ok(http::Response::builder()
|
Ok(http::Response::builder()
|
||||||
.header("X-Open-Id", open_id.to_string())
|
.header("X-Open-Id", open_id.to_string())
|
||||||
@ -996,32 +992,24 @@ impl Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn signals(&self, req: Request<hyper::Body>, caller: Caller)
|
fn signals(&self, req: Request<hyper::Body>, caller: Caller)
|
||||||
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
|
-> Box<dyn Future<Output = Result<Response<Body>, Response<Body>>> + Send + Sync + 'static> {
|
||||||
use http::method::Method;
|
use http::method::Method;
|
||||||
match *req.method() {
|
match *req.method() {
|
||||||
Method::POST => Box::new(with_json_body(req)
|
Method::POST => Box::new(with_json_body(req)
|
||||||
.and_then({
|
.and_then({
|
||||||
let s = self.0.clone();
|
let s = self.0.clone();
|
||||||
move |(req, b)| s.post_signals(&req, caller, b)
|
move |(req, b)| future::ready(s.post_signals(&req, caller, b))
|
||||||
})),
|
})),
|
||||||
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
|
Method::GET | Method::HEAD => Box::new(future::ready(self.0.get_signals(&req))),
|
||||||
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||||
"POST, GET, or HEAD expected"))),
|
"POST, GET, or HEAD expected"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl ::hyper::service::Service for Service {
|
pub fn serve(&mut self, req: Request<::hyper::Body>) -> BoxedFuture {
|
||||||
type ReqBody = ::hyper::Body;
|
fn wrap<R>(is_private: bool, r: R) -> BoxedFuture
|
||||||
type ResBody = Body;
|
where R: Future<Output = Result<Response<Body>, Response<Body>>> + Send + Sync + 'static {
|
||||||
type Error = BoxedError;
|
return Box::new(r.or_else(|e| futures::future::ok(e)).map_ok(move |mut r| {
|
||||||
type Future = Box<dyn Future<Item = Response<Self::ResBody>, Error = Self::Error> + Send + 'static>;
|
|
||||||
|
|
||||||
fn call(&mut self, req: Request<::hyper::Body>) -> Self::Future {
|
|
||||||
fn wrap<R>(is_private: bool, r: R)
|
|
||||||
-> Box<dyn Future<Item = Response<Body>, Error = BoxedError> + Send + 'static>
|
|
||||||
where R: Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static {
|
|
||||||
return Box::new(r.or_else(|e| Ok(e)).map(move |mut r| {
|
|
||||||
if is_private {
|
if is_private {
|
||||||
r.headers_mut().insert("Cache-Control", HeaderValue::from_static("private"));
|
r.headers_mut().insert("Cache-Control", HeaderValue::from_static("private"));
|
||||||
}
|
}
|
||||||
@ -1030,8 +1018,8 @@ impl ::hyper::service::Service for Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wrap_r(is_private: bool, r: ResponseResult)
|
fn wrap_r(is_private: bool, r: ResponseResult)
|
||||||
-> Box<dyn Future<Item = Response<Body>, Error = BoxedError> + Send + 'static> {
|
-> Box<dyn Future<Output = Result<Response<Body>, BoxedError>> + Send + Sync + 'static> {
|
||||||
return wrap(is_private, future::result(r))
|
return wrap(is_private, future::ready(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
let p = Path::decode(req.uri().path());
|
let p = Path::decode(req.uri().path());
|
||||||
@ -1066,13 +1054,13 @@ impl ::hyper::service::Service for Service {
|
|||||||
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
||||||
Path::Login => wrap(true, with_form_body(req).and_then({
|
Path::Login => wrap(true, with_form_body(req).and_then({
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
move |(req, b)| { s.0.login(&req, b) }
|
move |(req, b)| future::ready(s.0.login(&req, b))
|
||||||
})),
|
})),
|
||||||
Path::Logout => wrap(true, with_form_body(req).and_then({
|
Path::Logout => wrap(true, with_form_body(req).and_then({
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
move |(req, b)| { s.0.logout(&req, b) }
|
move |(req, b)| future::ready(s.0.logout(&req, b))
|
||||||
})),
|
})),
|
||||||
Path::Signals => wrap(true, self.signals(req, caller)),
|
Path::Signals => wrap(true, Pin::from(self.signals(req, caller))),
|
||||||
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1081,11 +1069,9 @@ impl ::hyper::service::Service for Service {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use db::testutil::{self, TestDb};
|
use db::testutil::{self, TestDb};
|
||||||
use futures::Future;
|
use futures::future::FutureExt;
|
||||||
use http::header;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error as StdError;
|
|
||||||
use super::Segments;
|
use super::Segments;
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
@ -1093,14 +1079,13 @@ mod tests {
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
//test_camera_uuid: Uuid,
|
//test_camera_uuid: Uuid,
|
||||||
handle: Option<::std::thread::JoinHandle<()>>,
|
handle: Option<::std::thread::JoinHandle<()>>,
|
||||||
shutdown_tx: Option<futures::sync::oneshot::Sender<()>>,
|
shutdown_tx: Option<futures::channel::oneshot::Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
fn new(allow_unauthenticated_permissions: Option<db::Permissions>) -> Server {
|
fn new(allow_unauthenticated_permissions: Option<db::Permissions>) -> Server {
|
||||||
let db = TestDb::new(base::clock::RealClocks {});
|
let db = TestDb::new(base::clock::RealClocks {});
|
||||||
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
|
let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel::<()>();
|
||||||
let addr = "127.0.0.1:0".parse().unwrap();
|
|
||||||
let service = super::Service::new(super::Config {
|
let service = super::Service::new(super::Config {
|
||||||
db: db.db.clone(),
|
db: db.db.clone(),
|
||||||
ui_dir: None,
|
ui_dir: None,
|
||||||
@ -1108,12 +1093,22 @@ mod tests {
|
|||||||
trust_forward_hdrs: true,
|
trust_forward_hdrs: true,
|
||||||
time_zone_name: "".to_owned(),
|
time_zone_name: "".to_owned(),
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
let server = hyper::server::Server::bind(&addr)
|
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
||||||
.tcp_nodelay(true)
|
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
||||||
.serve(move || Ok::<_, Box<dyn StdError + Send + Sync>>(service.clone()));
|
let mut s = service.clone();
|
||||||
let addr = server.local_addr(); // resolve port 0 to a real ephemeral port number.
|
move |req| std::pin::Pin::from(s.serve(req))
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let srv = rt.enter(|| {
|
||||||
|
let addr = ([127, 0, 0, 1], 0).into();
|
||||||
|
hyper::server::Server::bind(&addr)
|
||||||
|
.tcp_nodelay(true)
|
||||||
|
.serve(make_svc)
|
||||||
|
});
|
||||||
|
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||||
let handle = ::std::thread::spawn(move || {
|
let handle = ::std::thread::spawn(move || {
|
||||||
::tokio::run(server.with_graceful_shutdown(shutdown_rx).map_err(|e| panic!(e)));
|
rt.block_on(srv.with_graceful_shutdown(shutdown_rx.map(|_| ()))).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a user.
|
// Create a user.
|
||||||
@ -1141,14 +1136,14 @@ mod tests {
|
|||||||
struct SessionCookie(Option<String>);
|
struct SessionCookie(Option<String>);
|
||||||
|
|
||||||
impl SessionCookie {
|
impl SessionCookie {
|
||||||
pub fn new(headers: &http::HeaderMap) -> Self {
|
pub fn new(headers: &reqwest::header::HeaderMap) -> Self {
|
||||||
let mut c = SessionCookie::default();
|
let mut c = SessionCookie::default();
|
||||||
c.update(headers);
|
c.update(headers);
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, headers: &http::HeaderMap) {
|
pub fn update(&mut self, headers: &reqwest::header::HeaderMap) {
|
||||||
for set_cookie in headers.get_all(header::SET_COOKIE) {
|
for set_cookie in headers.get_all(reqwest::header::SET_COOKIE) {
|
||||||
let mut set_cookie = set_cookie.to_str().unwrap().split("; ");
|
let mut set_cookie = set_cookie.to_str().unwrap().split("; ");
|
||||||
let c = set_cookie.next().unwrap();
|
let c = set_cookie.next().unwrap();
|
||||||
let mut clear = false;
|
let mut clear = false;
|
||||||
@ -1256,7 +1251,7 @@ mod tests {
|
|||||||
let s = Server::new(None);
|
let s = Server::new(None);
|
||||||
let cli = reqwest::Client::new();
|
let cli = reqwest::Client::new();
|
||||||
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
|
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1267,29 +1262,29 @@ mod tests {
|
|||||||
let login_url = format!("{}/api/login", &s.base_url);
|
let login_url = format!("{}/api/login", &s.base_url);
|
||||||
|
|
||||||
let resp = cli.get(&login_url).send().unwrap();
|
let resp = cli.get(&login_url).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::METHOD_NOT_ALLOWED);
|
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
|
||||||
let resp = cli.post(&login_url).send().unwrap();
|
let resp = cli.post(&login_url).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
let mut p = HashMap::new();
|
let mut p = HashMap::new();
|
||||||
p.insert("username", "slamb");
|
p.insert("username", "slamb");
|
||||||
p.insert("password", "asdf");
|
p.insert("password", "asdf");
|
||||||
let resp = cli.post(&login_url).form(&p).send().unwrap();
|
let resp = cli.post(&login_url).form(&p).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
p.insert("password", "hunter2");
|
p.insert("password", "hunter2");
|
||||||
let resp = cli.post(&login_url).form(&p).send().unwrap();
|
let resp = cli.post(&login_url).form(&p).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::NO_CONTENT);
|
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
||||||
let cookie = SessionCookie::new(resp.headers());
|
let cookie = SessionCookie::new(resp.headers());
|
||||||
info!("cookie: {:?}", cookie);
|
info!("cookie: {:?}", cookie);
|
||||||
info!("header: {}", cookie.header());
|
info!("header: {}", cookie.header());
|
||||||
|
|
||||||
let resp = cli.get(&format!("{}/api/", &s.base_url))
|
let resp = cli.get(&format!("{}/api/", &s.base_url))
|
||||||
.header(header::COOKIE, cookie.header())
|
.header(reqwest::header::COOKIE, cookie.header())
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::OK);
|
assert_eq!(resp.status(), reqwest::StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1301,38 +1296,38 @@ mod tests {
|
|||||||
p.insert("username", "slamb");
|
p.insert("username", "slamb");
|
||||||
p.insert("password", "hunter2");
|
p.insert("password", "hunter2");
|
||||||
let resp = cli.post(&format!("{}/api/login", &s.base_url)).form(&p).send().unwrap();
|
let resp = cli.post(&format!("{}/api/login", &s.base_url)).form(&p).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::NO_CONTENT);
|
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
||||||
let cookie = SessionCookie::new(resp.headers());
|
let cookie = SessionCookie::new(resp.headers());
|
||||||
|
|
||||||
// A GET shouldn't work.
|
// A GET shouldn't work.
|
||||||
let resp = cli.get(&format!("{}/api/logout", &s.base_url))
|
let resp = cli.get(&format!("{}/api/logout", &s.base_url))
|
||||||
.header(header::COOKIE, cookie.header())
|
.header(reqwest::header::COOKIE, cookie.header())
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::METHOD_NOT_ALLOWED);
|
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
|
||||||
// Neither should a POST without a csrf token.
|
// Neither should a POST without a csrf token.
|
||||||
let resp = cli.post(&format!("{}/api/logout", &s.base_url))
|
let resp = cli.post(&format!("{}/api/logout", &s.base_url))
|
||||||
.header(header::COOKIE, cookie.header())
|
.header(reqwest::header::COOKIE, cookie.header())
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
// But it should work with the csrf token.
|
// But it should work with the csrf token.
|
||||||
// Retrieve that from the toplevel API request.
|
// Retrieve that from the toplevel API request.
|
||||||
let toplevel: serde_json::Value = cli.post(&format!("{}/api/", &s.base_url))
|
let toplevel: serde_json::Value = cli.post(&format!("{}/api/", &s.base_url))
|
||||||
.header(header::COOKIE, cookie.header())
|
.header(reqwest::header::COOKIE, cookie.header())
|
||||||
.send().unwrap()
|
.send().unwrap()
|
||||||
.json().unwrap();
|
.json().unwrap();
|
||||||
let csrf = toplevel.get("session").unwrap().get("csrf").unwrap().as_str();
|
let csrf = toplevel.get("session").unwrap().get("csrf").unwrap().as_str();
|
||||||
let mut p = HashMap::new();
|
let mut p = HashMap::new();
|
||||||
p.insert("csrf", csrf);
|
p.insert("csrf", csrf);
|
||||||
let resp = cli.post(&format!("{}/api/logout", &s.base_url))
|
let resp = cli.post(&format!("{}/api/logout", &s.base_url))
|
||||||
.header(header::COOKIE, cookie.header())
|
.header(reqwest::header::COOKIE, cookie.header())
|
||||||
.form(&p)
|
.form(&p)
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::NO_CONTENT);
|
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
||||||
let mut updated_cookie = cookie.clone();
|
let mut updated_cookie = cookie.clone();
|
||||||
updated_cookie.update(resp.headers());
|
updated_cookie.update(resp.headers());
|
||||||
|
|
||||||
@ -1341,10 +1336,10 @@ mod tests {
|
|||||||
|
|
||||||
// It should also be invalidated server-side.
|
// It should also be invalidated server-side.
|
||||||
let resp = cli.get(&format!("{}/api/", &s.base_url))
|
let resp = cli.get(&format!("{}/api/", &s.base_url))
|
||||||
.header(header::COOKIE, cookie.header())
|
.header(reqwest::header::COOKIE, cookie.header())
|
||||||
.send()
|
.send()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1357,7 +1352,7 @@ mod tests {
|
|||||||
let resp = cli.get(
|
let resp = cli.get(
|
||||||
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))
|
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))
|
||||||
.send().unwrap();
|
.send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1366,10 +1361,8 @@ mod bench {
|
|||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
use db::testutil::{self, TestDb};
|
use db::testutil::{self, TestDb};
|
||||||
use futures::Future;
|
|
||||||
use hyper;
|
use hyper;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::error::Error as StdError;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
@ -1382,7 +1375,6 @@ mod bench {
|
|||||||
let db = TestDb::new(::base::clock::RealClocks {});
|
let db = TestDb::new(::base::clock::RealClocks {});
|
||||||
let test_camera_uuid = db.test_camera_uuid;
|
let test_camera_uuid = db.test_camera_uuid;
|
||||||
testutil::add_dummy_recordings_to_db(&db.db, 1440);
|
testutil::add_dummy_recordings_to_db(&db.db, 1440);
|
||||||
let addr = "127.0.0.1:0".parse().unwrap();
|
|
||||||
let service = super::Service::new(super::Config {
|
let service = super::Service::new(super::Config {
|
||||||
db: db.db.clone(),
|
db: db.db.clone(),
|
||||||
ui_dir: None,
|
ui_dir: None,
|
||||||
@ -1390,12 +1382,22 @@ mod bench {
|
|||||||
trust_forward_hdrs: false,
|
trust_forward_hdrs: false,
|
||||||
time_zone_name: "".to_owned(),
|
time_zone_name: "".to_owned(),
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
let server = hyper::server::Server::bind(&addr)
|
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
||||||
.tcp_nodelay(true)
|
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
||||||
.serve(move || Ok::<_, Box<dyn StdError + Send + Sync>>(service.clone()));
|
let mut s = service.clone();
|
||||||
let addr = server.local_addr(); // resolve port 0 to a real ephemeral port number.
|
move |req| std::pin::Pin::from(s.serve(req))
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let srv = rt.enter(|| {
|
||||||
|
let addr = ([127, 0, 0, 1], 0).into();
|
||||||
|
hyper::server::Server::bind(&addr)
|
||||||
|
.tcp_nodelay(true)
|
||||||
|
.serve(make_svc)
|
||||||
|
});
|
||||||
|
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||||
::std::thread::spawn(move || {
|
::std::thread::spawn(move || {
|
||||||
::tokio::run(server.map_err(|e| panic!(e)));
|
rt.block_on(srv).unwrap();
|
||||||
});
|
});
|
||||||
Server {
|
Server {
|
||||||
base_url: format!("http://{}:{}", addr.ip(), addr.port()),
|
base_url: format!("http://{}:{}", addr.ip(), addr.port()),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user