mirror of
https://github.com/mozilla-services/syncstorage-rs.git
synced 2026-05-05 12:16:21 +02:00
bug: add tag info to sentry error messages (#372)
* bug: add tag info to sentry error messages * set tag info using middleware, fetch tag info from request and response Issue #329
This commit is contained in:
parent
a3be0ac869
commit
b834c54af6
2639
Cargo.lock
generated
2639
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -43,6 +43,7 @@ mozsvc-common = "0.1"
|
||||
num_cpus = "1.11"
|
||||
# must match what's used by googleapis-raw
|
||||
protobuf = "2.7.0"
|
||||
openssl ="0.10"
|
||||
rand = "0.7"
|
||||
regex = "1.3"
|
||||
sentry = { version = "0.17.0", features = ["with_curl_transport"] }
|
||||
|
||||
@ -34,7 +34,10 @@ Mozilla Sync Storage built with [Rust](https://rust-lang.org).
|
||||
- Pkg-config
|
||||
- Openssl
|
||||
|
||||
Depending on your OS, you may also need to install `libgrpcdev`, and `protobuf-compiler-grpc`.
|
||||
Depending on your OS, you may also need to install `libgrpcdev`,
|
||||
`libcurl4-openssl-dev`, and `protobuf-compiler-grpc`. *Note*: if the
|
||||
code complies cleanly, but generates a Segmentation Fault within
|
||||
Sentry init, you probably are missing `libcurl4-openssl-dev`.
|
||||
|
||||
## Local Setup
|
||||
|
||||
|
||||
1
db-tests/Cargo.lock
generated
1
db-tests/Cargo.lock
generated
@ -2531,6 +2531,7 @@ dependencies = [
|
||||
"mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mozsvc-common 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl 0.10.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"protobuf 2.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
||||
@ -55,7 +55,11 @@ async fn create_delete() -> Result<()> {
|
||||
let uid = 1;
|
||||
let coll = "clients";
|
||||
let id = db.create_batch(cb(uid, coll, vec![])).compat().await?;
|
||||
assert!(db.validate_batch(vb(uid, coll, id.clone())).compat().await?);
|
||||
assert!(
|
||||
db.validate_batch(vb(uid, coll, id.clone()))
|
||||
.compat()
|
||||
.await?
|
||||
);
|
||||
|
||||
db.delete_batch(params::DeleteBatch {
|
||||
user_id: hid(uid),
|
||||
@ -77,7 +81,11 @@ async fn expiry() -> Result<()> {
|
||||
let id = with_delta!(db, -(BATCH_LIFETIME + 11), {
|
||||
db.create_batch(cb(uid, coll, vec![])).compat().await
|
||||
})?;
|
||||
assert!(!db.validate_batch(vb(uid, coll, id.clone())).compat().await?);
|
||||
assert!(
|
||||
!db.validate_batch(vb(uid, coll, id.clone()))
|
||||
.compat()
|
||||
.await?
|
||||
);
|
||||
let result = db.get_batch(gb(uid, coll, id.clone())).compat().await?;
|
||||
assert!(result.is_none());
|
||||
|
||||
@ -101,7 +109,11 @@ async fn update() -> Result<()> {
|
||||
let uid = 1;
|
||||
let coll = "clients";
|
||||
let id = db.create_batch(cb(uid, coll, vec![])).compat().await?;
|
||||
assert!(db.get_batch(gb(uid, coll, id.clone())).compat().await?.is_some());
|
||||
assert!(db
|
||||
.get_batch(gb(uid, coll, id.clone()))
|
||||
.compat()
|
||||
.await?
|
||||
.is_some());
|
||||
// XXX: now bogus under spanner
|
||||
//assert_eq!(batch.bsos, "".to_owned());
|
||||
|
||||
@ -109,7 +121,9 @@ async fn update() -> Result<()> {
|
||||
postbso("b0", Some("payload 0"), Some(10), None),
|
||||
postbso("b1", Some("payload 1"), Some(1_000_000_000), None),
|
||||
];
|
||||
db.append_to_batch(ab(uid, coll, id.clone(), bsos)).compat().await?;
|
||||
db.append_to_batch(ab(uid, coll, id.clone(), bsos))
|
||||
.compat()
|
||||
.await?;
|
||||
|
||||
assert!(db.get_batch(gb(uid, coll, id)).compat().await?.is_some());
|
||||
// XXX: now bogus under spanner
|
||||
|
||||
@ -4,8 +4,8 @@ use futures::compat::Future01CompatExt;
|
||||
use syncstorage::{
|
||||
db::{params, util::SyncTimestamp, Db, Sorting},
|
||||
error::ApiError,
|
||||
settings::{Secrets, ServerLimits, Settings},
|
||||
server::metrics,
|
||||
settings::{Secrets, ServerLimits, Settings},
|
||||
web::extractors::{BsoQueryParams, HawkIdentifier},
|
||||
};
|
||||
|
||||
|
||||
@ -81,13 +81,7 @@ impl From<Context<DbErrorKind>> for DbError {
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let error = Self { inner, status };
|
||||
|
||||
if status == StatusCode::INTERNAL_SERVER_ERROR {
|
||||
sentry::integrations::failure::capture_fail(&error);
|
||||
}
|
||||
|
||||
error
|
||||
Self { inner, status }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1266,8 +1266,7 @@ impl SpannerDb {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
.to_string();
|
||||
);
|
||||
q = format!(
|
||||
"{}{}",
|
||||
q,
|
||||
@ -1279,8 +1278,7 @@ impl SpannerDb {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
.to_string();
|
||||
);
|
||||
q = format!(
|
||||
"{}{}",
|
||||
q,
|
||||
@ -1291,8 +1289,7 @@ impl SpannerDb {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
.to_string();
|
||||
);
|
||||
q = format!(
|
||||
"{}{}",
|
||||
q,
|
||||
@ -1302,8 +1299,7 @@ impl SpannerDb {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
.to_string();
|
||||
);
|
||||
|
||||
if q.is_empty() {
|
||||
// Nothing to update
|
||||
|
||||
@ -111,7 +111,12 @@ impl ApiError {
|
||||
fn weave_error_code(&self) -> WeaveError {
|
||||
match self.kind() {
|
||||
ApiErrorKind::Validation(ver) => match ver.kind() {
|
||||
ValidationErrorKind::FromDetails(ref description, ref location, name) => {
|
||||
ValidationErrorKind::FromDetails(
|
||||
ref description,
|
||||
ref location,
|
||||
name,
|
||||
ref _tags,
|
||||
) => {
|
||||
if description == "size-limit-exceeded" {
|
||||
return WeaveError::SizeLimitExceeded;
|
||||
}
|
||||
@ -123,7 +128,7 @@ impl ApiError {
|
||||
}
|
||||
WeaveError::UnknownError
|
||||
}
|
||||
ValidationErrorKind::FromValidationErrors(ref _err, ref location) => {
|
||||
ValidationErrorKind::FromValidationErrors(ref _err, ref location, ref _tags) => {
|
||||
if *location == RequestErrorLocation::Body {
|
||||
WeaveError::InvalidWbo
|
||||
} else {
|
||||
|
||||
@ -34,7 +34,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Avoid its default reqwest transport for now due to issues w/
|
||||
// likely grpcio's boringssl
|
||||
let curl_transport_factory = |options: &sentry::ClientOptions| {
|
||||
Box::new(sentry::transports::CurlHttpTransport::new(options))
|
||||
// Note: set options.debug = true when diagnosing sentry issues.
|
||||
Box::new(sentry::transports::CurlHttpTransport::new(&options))
|
||||
as Box<dyn sentry::internals::Transport>
|
||||
};
|
||||
let sentry = sentry::init(sentry::ClientOptions {
|
||||
|
||||
@ -1,24 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::UdpSocket;
|
||||
use std::time::Instant;
|
||||
|
||||
use actix_web::{error::ErrorInternalServerError, http, Error, HttpRequest};
|
||||
use actix_web::{error::ErrorInternalServerError, Error, HttpRequest};
|
||||
use cadence::{
|
||||
BufferedUdpMetricSink, Counted, Metric, NopMetricSink, QueuingMetricSink, StatsdClient, Timed,
|
||||
};
|
||||
|
||||
use crate::error::ApiError;
|
||||
use crate::server::user_agent::parse_user_agent;
|
||||
use crate::server::ServerState;
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub type Tags = HashMap<String, String>;
|
||||
use crate::web::tags::Tags;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MetricTimer {
|
||||
pub label: String,
|
||||
pub start: Instant,
|
||||
pub tags: Option<Tags>,
|
||||
pub tags: Tags,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -30,14 +27,20 @@ pub struct Metrics {
|
||||
|
||||
impl Drop for Metrics {
|
||||
fn drop(&mut self) {
|
||||
let tags = self.tags.clone().unwrap_or_default();
|
||||
if let Some(client) = self.client.as_ref() {
|
||||
if let Some(timer) = self.timer.as_ref() {
|
||||
let lapse = (Instant::now() - timer.start).as_millis() as u64;
|
||||
trace!("⌚ Ending timer at nanos: {:?} : {:?}", &timer.label, lapse);
|
||||
trace!("⌚ Ending timer at nanos: {:?} : {:?}", &timer.label, lapse;
|
||||
"ua.os.family" => tags.get("ua.os.family"),
|
||||
"ua.browser.family" => tags.get("ua.browser.family"),
|
||||
"ua.name" => tags.get("ua.name"),
|
||||
"ua.os.ver" => tags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => tags.get("ua.browser.ver"));
|
||||
let mut tagged = client.time_with_tags(&timer.label, lapse);
|
||||
// Include any "hard coded" tags.
|
||||
// tagged = tagged.with_tag("version", env!("CARGO_PKG_VERSION"));
|
||||
let tags = timer.tags.clone().unwrap_or_default();
|
||||
let tags = timer.tags.tags.clone();
|
||||
let keys = tags.keys();
|
||||
for tag in keys {
|
||||
tagged = tagged.with_tag(tag, &tags.get(tag).unwrap())
|
||||
@ -58,11 +61,9 @@ impl Drop for Metrics {
|
||||
|
||||
impl From<&HttpRequest> for Metrics {
|
||||
fn from(req: &HttpRequest) -> Self {
|
||||
let ua = req.headers().get(http::header::USER_AGENT);
|
||||
let mut tags: Option<Tags> = None;
|
||||
if let Some(ua_string) = ua {
|
||||
tags = Some(Self::default_tags(ua_string.to_str().unwrap_or("")));
|
||||
}
|
||||
let exts = req.extensions();
|
||||
let def_tags = Tags::from_request_head(req.head());
|
||||
let tags = exts.get::<Tags>().unwrap_or_else(|| &def_tags);
|
||||
Metrics {
|
||||
client: match req.app_data::<ServerState>() {
|
||||
Some(v) => Some(*v.metrics.clone()),
|
||||
@ -71,7 +72,7 @@ impl From<&HttpRequest> for Metrics {
|
||||
None
|
||||
}
|
||||
},
|
||||
tags,
|
||||
tags: Some(tags.clone()),
|
||||
timer: None,
|
||||
}
|
||||
}
|
||||
@ -102,22 +103,6 @@ impl Metrics {
|
||||
StatsdClient::builder("", NopMetricSink).build()
|
||||
}
|
||||
|
||||
pub fn default_tags(user_agent: &str) -> Tags {
|
||||
let mut tags = Tags::new();
|
||||
|
||||
let (ua_result, metrics_os, metrics_browser) = parse_user_agent(user_agent);
|
||||
|
||||
tags.insert("ua.os.family".to_owned(), metrics_os.to_owned());
|
||||
tags.insert("ua.browser.family".to_owned(), metrics_browser.to_owned());
|
||||
tags.insert("ua.name".to_owned(), ua_result.name.to_owned());
|
||||
tags.insert(
|
||||
"ua.os.ver".to_owned(),
|
||||
ua_result.os_version.to_owned().to_string(),
|
||||
);
|
||||
tags.insert("ua.browser.ver".to_owned(), ua_result.version.to_owned());
|
||||
tags
|
||||
}
|
||||
|
||||
pub fn noop() -> Self {
|
||||
Self {
|
||||
client: Some(Self::sink()),
|
||||
@ -129,13 +114,19 @@ impl Metrics {
|
||||
pub fn start_timer(&mut self, label: &str, tags: Option<Tags>) {
|
||||
let mut mtags = self.tags.clone().unwrap_or_default();
|
||||
if let Some(t) = tags {
|
||||
mtags.extend(t)
|
||||
mtags.extend(t.tags)
|
||||
}
|
||||
trace!("⌚ Starting timer... {:?}", &label);
|
||||
|
||||
trace!("⌚ Starting timer... {:?}", &label;
|
||||
"ua.os.family" => mtags.get("ua.os.family"),
|
||||
"ua.browser.family" => mtags.get("ua.browser.family"),
|
||||
"ua.name" => mtags.get("ua.name"),
|
||||
"ua.os.ver" => mtags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => mtags.get("ua.browser.ver"));
|
||||
self.timer = Some(MetricTimer {
|
||||
label: label.to_owned(),
|
||||
start: Instant::now(),
|
||||
tags: if !mtags.is_empty() { Some(mtags) } else { None },
|
||||
tags: mtags,
|
||||
});
|
||||
}
|
||||
|
||||
@ -147,18 +138,27 @@ impl Metrics {
|
||||
pub fn incr_with_tags(self, label: &str, tags: Option<Tags>) {
|
||||
if let Some(client) = self.client.as_ref() {
|
||||
let mut tagged = client.incr_with_tags(label);
|
||||
let mut mtags = self.tags.clone().unwrap_or_default();
|
||||
mtags.extend(tags.unwrap_or_default());
|
||||
let keys = mtags.keys();
|
||||
for tag in keys {
|
||||
tagged = tagged.with_tag(tag, &mtags.get(tag).unwrap())
|
||||
let mut mtags = self.tags.clone().unwrap_or_default().tags;
|
||||
if let Some(t) = tags {
|
||||
mtags.extend(t.tags)
|
||||
}
|
||||
let tag_keys = mtags.keys();
|
||||
for key in tag_keys.clone() {
|
||||
// REALLY wants a static here, or at least a well defined ref.
|
||||
tagged = tagged.with_tag(&key, &mtags.get(key).unwrap());
|
||||
}
|
||||
// Include any "hard coded" tags.
|
||||
// incr = incr.with_tag("version", env!("CARGO_PKG_VERSION"));
|
||||
match tagged.try_send() {
|
||||
Err(e) => {
|
||||
// eat the metric, but log the error
|
||||
debug!("⚠️ Metric {} error: {:?} ", label, e);
|
||||
debug!("⚠️ Metric {} error: {:?} ", label, e;
|
||||
"ua.os.family" => mtags.get("ua.os.family"),
|
||||
"ua.browser.family" => mtags.get("ua.browser.family"),
|
||||
"ua.name" => mtags.get("ua.name"),
|
||||
"ua.os.ver" => mtags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => mtags.get("ua.browser.ver")
|
||||
);
|
||||
}
|
||||
Ok(v) => trace!("☑️ {:?}", v.as_metric_str()),
|
||||
}
|
||||
@ -201,16 +201,31 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_tags() {
|
||||
let tags = Metrics::default_tags(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0",
|
||||
use actix_web::dev::RequestHead;
|
||||
use actix_web::http::{header, uri::Uri};
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut rh = RequestHead::default();
|
||||
let path = "/1.5/42/storage/meta/global";
|
||||
rh.uri = Uri::from_static(path);
|
||||
rh.headers.insert(
|
||||
header::USER_AGENT,
|
||||
header::HeaderValue::from_static(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0",
|
||||
),
|
||||
);
|
||||
|
||||
let tags = Tags::from_request_head(&rh);
|
||||
|
||||
let mut result = HashMap::<String, String>::new();
|
||||
result.insert("ua.os.ver".to_owned(), "NT 10.0".to_owned());
|
||||
result.insert("ua.os.family".to_owned(), "Windows".to_owned());
|
||||
result.insert("ua.browser.ver".to_owned(), "72.0".to_owned());
|
||||
result.insert("ua.name".to_owned(), "Firefox".to_owned());
|
||||
result.insert("ua.browser.family".to_owned(), "Firefox".to_owned());
|
||||
result.insert("uri.path".to_owned(), path.to_owned());
|
||||
result.insert("uri.method".to_owned(), "GET".to_owned());
|
||||
|
||||
assert_eq!(tags, result)
|
||||
assert_eq!(tags.tags, result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,6 +67,7 @@ macro_rules! build_app {
|
||||
.wrap(middleware::PreConditionCheck::new())
|
||||
.wrap(middleware::DbTransaction::new())
|
||||
.wrap(middleware::WeaveTimestamp::new())
|
||||
.wrap(middleware::SentryWrapper::new())
|
||||
// Followed by the "official middleware" so they run first.
|
||||
.wrap(Cors::default())
|
||||
.service(
|
||||
@ -148,6 +149,7 @@ macro_rules! build_app {
|
||||
.body(include_str!("../../version.json"))
|
||||
})),
|
||||
)
|
||||
.service(web::resource("/__error__").route(web::get().to_async(handlers::test_error)))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ fn get_test_settings() -> Settings {
|
||||
debug: true,
|
||||
port,
|
||||
host,
|
||||
database_url: settings.database_url.clone(),
|
||||
database_url: settings.database_url,
|
||||
database_pool_max_size: Some(pool_size + 1),
|
||||
database_use_test_transactions: true,
|
||||
limits: ServerLimits::default(),
|
||||
@ -113,7 +113,7 @@ fn create_hawk_header(method: &str, port: u16, path: &str) -> String {
|
||||
let host = TEST_HOST;
|
||||
let payload = HawkPayload {
|
||||
expires: (Utc::now().timestamp() + 5) as f64,
|
||||
node: format!("http://{}:{}", host, port).to_string(),
|
||||
node: format!("http://{}:{}", host, port),
|
||||
salt: "wibble".to_string(),
|
||||
user_id: 42,
|
||||
fxa_uid: "xxx_test".to_owned(),
|
||||
|
||||
@ -15,6 +15,7 @@ use time::Duration;
|
||||
use actix_http::http::Uri;
|
||||
use actix_web::dev::ConnectionInfo;
|
||||
|
||||
use super::tags::Tags;
|
||||
use super::{
|
||||
error::{HawkErrorKind, ValidationErrorKind},
|
||||
extractors::RequestErrorLocation,
|
||||
@ -159,6 +160,7 @@ impl HawkPayload {
|
||||
secrets: &Secrets,
|
||||
ci: &ConnectionInfo,
|
||||
uri: &Uri,
|
||||
tags: Option<Tags>,
|
||||
) -> ApiResult<Self> {
|
||||
let host_port: Vec<_> = ci.host().splitn(2, ':').collect();
|
||||
let host = host_port[0];
|
||||
@ -168,6 +170,7 @@ impl HawkPayload {
|
||||
"Invalid port (hostname:port) specified".to_owned(),
|
||||
RequestErrorLocation::Header,
|
||||
None,
|
||||
tags,
|
||||
)
|
||||
})?
|
||||
} else if ci.scheme() == "https" {
|
||||
|
||||
@ -16,6 +16,7 @@ use serde_json::{Error as JsonError, Value};
|
||||
use validator;
|
||||
|
||||
use super::extractors::RequestErrorLocation;
|
||||
use super::tags::Tags;
|
||||
use crate::error::ApiError;
|
||||
|
||||
/// An error occurred during HAWK authentication.
|
||||
@ -70,8 +71,8 @@ pub enum HawkErrorKind {
|
||||
/// An error occurred in an Actix extractor.
|
||||
#[derive(Debug)]
|
||||
pub struct ValidationError {
|
||||
inner: Context<ValidationErrorKind>,
|
||||
pub status: StatusCode,
|
||||
inner: Context<ValidationErrorKind>,
|
||||
}
|
||||
|
||||
impl ValidationError {
|
||||
@ -84,10 +85,14 @@ impl ValidationError {
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum ValidationErrorKind {
|
||||
#[fail(display = "{}", _0)]
|
||||
FromDetails(String, RequestErrorLocation, Option<String>),
|
||||
FromDetails(String, RequestErrorLocation, Option<String>, Option<Tags>),
|
||||
|
||||
#[fail(display = "{}", _0)]
|
||||
FromValidationErrors(#[cause] validator::ValidationErrors, RequestErrorLocation),
|
||||
FromValidationErrors(
|
||||
#[cause] validator::ValidationErrors,
|
||||
RequestErrorLocation,
|
||||
Option<Tags>,
|
||||
),
|
||||
}
|
||||
|
||||
failure_boilerplate!(HawkError, HawkErrorKind);
|
||||
@ -109,7 +114,7 @@ impl From<Context<ValidationErrorKind>> for ValidationError {
|
||||
fn from(inner: Context<ValidationErrorKind>) -> Self {
|
||||
debug!("Validation Error: {:?}", inner.get_context());
|
||||
let status = match inner.get_context() {
|
||||
ValidationErrorKind::FromDetails(ref _description, ref location, Some(ref name))
|
||||
ValidationErrorKind::FromDetails(ref _description, ref location, Some(ref name), _)
|
||||
if *location == RequestErrorLocation::Header =>
|
||||
{
|
||||
match name.to_ascii_lowercase().as_str() {
|
||||
@ -118,7 +123,7 @@ impl From<Context<ValidationErrorKind>> for ValidationError {
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
ValidationErrorKind::FromDetails(ref _description, ref location, Some(ref name))
|
||||
ValidationErrorKind::FromDetails(ref _description, ref location, Some(ref name), _)
|
||||
if *location == RequestErrorLocation::Path
|
||||
&& ["bso", "collection"].contains(&name.as_ref()) =>
|
||||
{
|
||||
@ -175,16 +180,17 @@ impl Serialize for ValidationErrorKind {
|
||||
let mut seq = serializer.serialize_seq(None)?;
|
||||
|
||||
match *self {
|
||||
ValidationErrorKind::FromDetails(ref description, ref location, ref name) => {
|
||||
ValidationErrorKind::FromDetails(ref description, ref location, ref name, ref tags) => {
|
||||
seq.serialize_element(&SerializedValidationError {
|
||||
description,
|
||||
location,
|
||||
name: name.as_ref().map(|name| &**name),
|
||||
value: None,
|
||||
tags: tags.as_ref(),
|
||||
})?;
|
||||
}
|
||||
|
||||
ValidationErrorKind::FromValidationErrors(ref errors, ref location) => {
|
||||
ValidationErrorKind::FromValidationErrors(ref errors, ref location, ref tags) => {
|
||||
for (field, field_errors) in errors.clone().field_errors().iter() {
|
||||
for field_error in field_errors.iter() {
|
||||
seq.serialize_element(&SerializedValidationError {
|
||||
@ -192,6 +198,7 @@ impl Serialize for ValidationErrorKind {
|
||||
location,
|
||||
name: Some(field),
|
||||
value: field_error.params.get("value"),
|
||||
tags: tags.clone().as_ref(),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@ -208,4 +215,5 @@ struct SerializedValidationError<'e> {
|
||||
pub location: &'e RequestErrorLocation,
|
||||
pub name: Option<&'e str>,
|
||||
pub value: Option<&'e Value>,
|
||||
pub tags: Option<&'e Tags>,
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
use std::{self, collections::HashMap, str::FromStr};
|
||||
|
||||
use actix_web::{
|
||||
dev::{ConnectionInfo, Extensions, Payload},
|
||||
dev::{ConnectionInfo, Extensions, Payload, RequestHead},
|
||||
error::ErrorInternalServerError,
|
||||
http::{
|
||||
header::{qitem, Accept, ContentType, Header, HeaderMap},
|
||||
@ -32,6 +32,7 @@ use crate::settings::{Secrets, ServerLimits};
|
||||
use crate::web::{
|
||||
auth::HawkPayload,
|
||||
error::{HawkErrorKind, ValidationErrorKind},
|
||||
tags::Tags,
|
||||
X_WEAVE_RECORDS,
|
||||
};
|
||||
|
||||
@ -154,6 +155,7 @@ impl FromRequest for BsoBodies {
|
||||
format!("Unreadable Content-Type: {:?}", e),
|
||||
RequestErrorLocation::Header,
|
||||
Some("Content-Type".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
@ -168,6 +170,7 @@ impl FromRequest for BsoBodies {
|
||||
format!("Invalid Content-Type {:?}", content_type),
|
||||
RequestErrorLocation::Header,
|
||||
Some("Content-Type".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
@ -180,16 +183,18 @@ impl FromRequest for BsoBodies {
|
||||
"Mimetype/encoding/content-length error".to_owned(),
|
||||
RequestErrorLocation::Header,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.into()
|
||||
});
|
||||
|
||||
// Avoid duplicating by defining our error func now, doesn't need the box wrapper
|
||||
fn make_error() -> Error {
|
||||
fn make_error(tags: Option<Tags>) -> Error {
|
||||
ValidationErrorKind::FromDetails(
|
||||
"Invalid JSON in request body".to_owned(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bsos".to_owned()),
|
||||
tags,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
@ -208,6 +213,7 @@ impl FromRequest for BsoBodies {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("app_data".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
@ -226,7 +232,7 @@ impl FromRequest for BsoBodies {
|
||||
bsos.push(raw_json);
|
||||
} else {
|
||||
// Per Python version, BSO's must json deserialize
|
||||
return future::err(make_error());
|
||||
return future::err(make_error(None));
|
||||
}
|
||||
}
|
||||
bsos
|
||||
@ -234,7 +240,7 @@ impl FromRequest for BsoBodies {
|
||||
json_vals
|
||||
} else {
|
||||
// Per Python version, BSO's must json deserialize
|
||||
return future::err(make_error());
|
||||
return future::err(make_error(None));
|
||||
};
|
||||
|
||||
// Validate all the BSO's, move invalid to our other list. Assume they'll all make
|
||||
@ -254,7 +260,7 @@ impl FromRequest for BsoBodies {
|
||||
for bso in bsos {
|
||||
// Error out if its not a JSON mapping type
|
||||
if !bso.is_object() {
|
||||
return future::err(make_error());
|
||||
return future::err(make_error(None));
|
||||
}
|
||||
// Save all id's we get, check for missing id, or duplicate.
|
||||
let bso_id = if let Some(id) = bso.get("id").and_then(serde_json::Value::as_str) {
|
||||
@ -265,6 +271,7 @@ impl FromRequest for BsoBodies {
|
||||
"Input BSO has duplicate ID".to_owned(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bsos".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
@ -278,6 +285,7 @@ impl FromRequest for BsoBodies {
|
||||
"Input BSO has no ID".to_owned(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bsos".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
@ -334,7 +342,6 @@ impl FromRequest for BsoBody {
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
// Only try and parse the body if its a valid content-type
|
||||
|
||||
let ctype = match ContentType::parse(req) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@ -343,6 +350,7 @@ impl FromRequest for BsoBody {
|
||||
format!("Unreadable Content-Type: {:?}", e),
|
||||
RequestErrorLocation::Header,
|
||||
Some("Content-Type".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
@ -355,6 +363,7 @@ impl FromRequest for BsoBody {
|
||||
"Invalid Content-Type".to_owned(),
|
||||
RequestErrorLocation::Header,
|
||||
Some("Content-Type".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
@ -368,6 +377,7 @@ impl FromRequest for BsoBody {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("app_data".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
@ -383,6 +393,7 @@ impl FromRequest for BsoBody {
|
||||
e.to_string(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bso".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into();
|
||||
err.into()
|
||||
@ -400,14 +411,18 @@ impl FromRequest for BsoBody {
|
||||
"payload too large".to_owned(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bso".to_owned()),
|
||||
None,
|
||||
)
|
||||
.into();
|
||||
return future::err(err.into());
|
||||
}
|
||||
if let Err(e) = bso.validate() {
|
||||
let err: ApiError =
|
||||
ValidationErrorKind::FromValidationErrors(e, RequestErrorLocation::Body)
|
||||
.into();
|
||||
let err: ApiError = ValidationErrorKind::FromValidationErrors(
|
||||
e,
|
||||
RequestErrorLocation::Body,
|
||||
None,
|
||||
)
|
||||
.into();
|
||||
return future::err(err.into());
|
||||
}
|
||||
future::ok(bso.into_inner())
|
||||
@ -425,46 +440,70 @@ pub struct BsoParam {
|
||||
}
|
||||
|
||||
impl BsoParam {
|
||||
pub fn bsoparam_from_path(uri: &Uri) -> Result<Self, Error> {
|
||||
pub fn bsoparam_from_path(uri: &Uri, tags: &Tags) -> Result<Self, Error> {
|
||||
// TODO: replace with proper path parser
|
||||
// path: "/1.5/{uid}/storage/{collection}/{bso}"
|
||||
let elements: Vec<&str> = uri.path().split('/').collect();
|
||||
let elem = elements.get(3);
|
||||
if elem.is_none() || elem != Some(&"storage") || elements.len() != 6 {
|
||||
warn!("⚠️ Unexpected BSO URI: {:?}", uri.path());
|
||||
warn!("⚠️ Unexpected BSO URI: {:?}", uri.path();
|
||||
"ua.os.family" => tags.get("ua.os.family"),
|
||||
"ua.browser.family" => tags.get("ua.browser.family"),
|
||||
"ua.name" => tags.get("ua.name"),
|
||||
"ua.os.ver" => tags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => tags.get("ua.browser.ver"));
|
||||
return Err(ValidationErrorKind::FromDetails(
|
||||
"Invalid BSO".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("bso".to_owned()),
|
||||
Some(tags.clone()),
|
||||
))?;
|
||||
}
|
||||
if let Some(v) = elements.get(5) {
|
||||
let sv = String::from_str(v).map_err(|e| {
|
||||
warn!("⚠️ BsoParam Error element:{:?} error:{:?}", v, e);
|
||||
let sv = String::from_str(v).map_err(|_| {
|
||||
warn!("⚠️ Invalid BsoParam Error: {:?}", v;
|
||||
"ua.os.family" => tags.get("ua.os.family"),
|
||||
"ua.browser.family" => tags.get("ua.browser.family"),
|
||||
"ua.name" => tags.get("ua.name"),
|
||||
"ua.os.ver" => tags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => tags.get("ua.browser.ver"));
|
||||
ValidationErrorKind::FromDetails(
|
||||
"Invalid BSO".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("bso".to_owned()),
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?;
|
||||
Ok(Self { bso: sv })
|
||||
} else {
|
||||
warn!("⚠️ Missing BSO: {:?}", uri.path());
|
||||
warn!("⚠️ Missing BSO: {:?}", uri.path();
|
||||
"ua.os.family" => tags.get("ua.os.family"),
|
||||
"ua.browser.family" => tags.get("ua.browser.family"),
|
||||
"ua.name" => tags.get("ua.name"),
|
||||
"ua.os.ver" => tags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => tags.get("ua.browser.ver"));
|
||||
Err(ValidationErrorKind::FromDetails(
|
||||
"Missing BSO".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("bso".to_owned()),
|
||||
Some(tags.clone()),
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extrude(uri: &Uri, extensions: &mut Extensions) -> Result<Self, Error> {
|
||||
pub fn extrude(head: &RequestHead, extensions: &mut Extensions) -> Result<Self, Error> {
|
||||
let uri = head.uri.clone();
|
||||
let tags = Tags::from_request_head(head);
|
||||
if let Some(bso) = extensions.get::<BsoParam>() {
|
||||
return Ok(bso.clone());
|
||||
}
|
||||
let bso = Self::bsoparam_from_path(uri)?;
|
||||
let bso = Self::bsoparam_from_path(&uri, &tags)?;
|
||||
bso.validate().map_err(|e| {
|
||||
ValidationErrorKind::FromValidationErrors(e, RequestErrorLocation::Path)
|
||||
ValidationErrorKind::FromValidationErrors(
|
||||
e,
|
||||
RequestErrorLocation::Path,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?;
|
||||
extensions.insert(bso.clone());
|
||||
Ok(bso)
|
||||
@ -477,7 +516,7 @@ impl FromRequest for BsoParam {
|
||||
type Future = Result<Self, Self::Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
Self::extrude(&req.uri(), &mut req.extensions_mut())
|
||||
Self::extrude(req.head(), &mut req.extensions_mut())
|
||||
}
|
||||
}
|
||||
|
||||
@ -489,7 +528,7 @@ pub struct CollectionParam {
|
||||
}
|
||||
|
||||
impl CollectionParam {
|
||||
fn col_from_path(uri: &Uri) -> Result<Option<CollectionParam>, Error> {
|
||||
fn col_from_path(uri: &Uri, tags: &Tags) -> Result<Option<CollectionParam>, Error> {
|
||||
// TODO: replace with proper path parser.
|
||||
// path: "/1.5/{uid}/storage/{collection}"
|
||||
let elements: Vec<&str> = uri.path().split('/').collect();
|
||||
@ -503,6 +542,7 @@ impl CollectionParam {
|
||||
"Missing Collection".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("collection".to_owned()),
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?;
|
||||
Ok(Some(Self { collection: sv }))
|
||||
@ -511,19 +551,28 @@ impl CollectionParam {
|
||||
"Missing Collection".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("collection".to_owned()),
|
||||
Some(tags.clone()),
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extrude(uri: &Uri, extensions: &mut Extensions) -> Result<Option<Self>, Error> {
|
||||
pub fn extrude(
|
||||
uri: &Uri,
|
||||
extensions: &mut Extensions,
|
||||
tags: &Tags,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
if let Some(collection) = extensions.get::<Option<Self>>() {
|
||||
return Ok(collection.clone());
|
||||
}
|
||||
|
||||
let collection = Self::col_from_path(&uri)?;
|
||||
let collection = Self::col_from_path(&uri, tags)?;
|
||||
let result = if let Some(collection) = collection {
|
||||
collection.validate().map_err(|e| {
|
||||
ValidationErrorKind::FromValidationErrors(e, RequestErrorLocation::Path)
|
||||
ValidationErrorKind::FromValidationErrors(
|
||||
e,
|
||||
RequestErrorLocation::Path,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?;
|
||||
Some(collection)
|
||||
} else {
|
||||
@ -539,14 +588,16 @@ impl FromRequest for CollectionParam {
|
||||
type Error = Error;
|
||||
type Future = Result<Self, Self::Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
if let Some(collection) = Self::extrude(&req.uri(), &mut req.extensions_mut())? {
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let tags = Tags::from_request(req, payload)?;
|
||||
if let Some(collection) = Self::extrude(&req.uri(), &mut req.extensions_mut(), &tags)? {
|
||||
Ok(collection)
|
||||
} else {
|
||||
Err(ValidationErrorKind::FromDetails(
|
||||
"Missing Collection".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("collection".to_owned()),
|
||||
Some(tags),
|
||||
))?
|
||||
}
|
||||
}
|
||||
@ -560,6 +611,7 @@ pub struct MetaRequest {
|
||||
pub user_id: HawkIdentifier,
|
||||
pub db: Box<dyn Db>,
|
||||
pub metrics: metrics::Metrics,
|
||||
pub tags: Tags,
|
||||
}
|
||||
|
||||
impl FromRequest for MetaRequest {
|
||||
@ -569,6 +621,13 @@ impl FromRequest for MetaRequest {
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
// Call the precondition stuff to init database handles and what-not
|
||||
let tags = {
|
||||
let exts = req.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
}
|
||||
};
|
||||
let user_id = HawkIdentifier::from_request(req, payload)?;
|
||||
let db = extrude_db(&req.extensions())?;
|
||||
Ok({
|
||||
@ -576,6 +635,7 @@ impl FromRequest for MetaRequest {
|
||||
user_id,
|
||||
db,
|
||||
metrics: metrics::Metrics::from(req),
|
||||
tags,
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -598,6 +658,7 @@ pub struct CollectionRequest {
|
||||
pub query: BsoQueryParams,
|
||||
pub reply: ReplyFormat,
|
||||
pub metrics: metrics::Metrics,
|
||||
pub tags: Option<Tags>,
|
||||
}
|
||||
|
||||
impl FromRequest for CollectionRequest {
|
||||
@ -610,6 +671,13 @@ impl FromRequest for CollectionRequest {
|
||||
let db = <Box<dyn Db>>::from_request(req, payload)?;
|
||||
let query = BsoQueryParams::from_request(req, payload)?;
|
||||
let collection = CollectionParam::from_request(req, payload)?.collection;
|
||||
let tags = {
|
||||
let exts = req.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
}
|
||||
};
|
||||
|
||||
let accept = get_accepted(req, &ACCEPTED_CONTENT_TYPES, "application/json");
|
||||
let reply = match accept.as_str() {
|
||||
@ -620,6 +688,7 @@ impl FromRequest for CollectionRequest {
|
||||
"Invalid accept".to_string(),
|
||||
RequestErrorLocation::Header,
|
||||
Some("accept".to_string()),
|
||||
Some(tags),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -632,6 +701,7 @@ impl FromRequest for CollectionRequest {
|
||||
query,
|
||||
reply,
|
||||
metrics: metrics::Metrics::from(req),
|
||||
tags: Some(tags),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -662,6 +732,10 @@ impl FromRequest for CollectionPostRequest {
|
||||
/// - Any valid BSO's beyond `BATCH_MAX_RECORDS` are moved to invalid
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
let tags = match req.extensions().get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
};
|
||||
let state = match req.app_data::<ServerState>() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@ -671,6 +745,7 @@ impl FromRequest for CollectionPostRequest {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("app_data".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
@ -697,6 +772,7 @@ impl FromRequest for CollectionPostRequest {
|
||||
"Known-bad BSO payload".to_owned(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bsos".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
@ -792,7 +868,6 @@ impl FromRequest for BsoPutRequest {
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let metrics = metrics::Metrics::from(req);
|
||||
|
||||
let fut = <(
|
||||
HawkIdentifier,
|
||||
Box<dyn Db>,
|
||||
@ -800,8 +875,9 @@ impl FromRequest for BsoPutRequest {
|
||||
BsoQueryParams,
|
||||
BsoParam,
|
||||
BsoBody,
|
||||
Tags,
|
||||
)>::from_request(req, payload)
|
||||
.and_then(|(user_id, db, collection, query, bso, body)| {
|
||||
.and_then(|(user_id, db, collection, query, bso, body, tags)| {
|
||||
let collection = collection.collection;
|
||||
if collection == "crypto" {
|
||||
// Verify the client didn't mess up the crypto if we have a payload
|
||||
@ -812,6 +888,7 @@ impl FromRequest for BsoPutRequest {
|
||||
"Known-bad BSO payload".to_owned(),
|
||||
RequestErrorLocation::Body,
|
||||
Some("bsos".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
@ -843,6 +920,14 @@ impl FromRequest for ConfigRequest {
|
||||
type Future = Result<Self, Self::Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let tags = {
|
||||
let exts = req.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
}
|
||||
};
|
||||
|
||||
let state = match req.app_data::<ServerState>() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@ -851,6 +936,7 @@ impl FromRequest for ConfigRequest {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("state".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -883,6 +969,13 @@ impl FromRequest for HeartbeatRequest {
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let headers = req.headers().clone();
|
||||
let tags = {
|
||||
let exts = req.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
}
|
||||
};
|
||||
|
||||
let state = match req.app_data::<ServerState>() {
|
||||
Some(s) => s,
|
||||
@ -893,6 +986,7 @@ impl FromRequest for HeartbeatRequest {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("state".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
@ -907,6 +1001,34 @@ impl FromRequest for HeartbeatRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestErrorRequest {
|
||||
pub headers: HeaderMap,
|
||||
pub tags: Option<Tags>,
|
||||
}
|
||||
|
||||
impl FromRequest for TestErrorRequest {
|
||||
type Config = ();
|
||||
type Error = Error;
|
||||
type Future = Box<dyn Future<Item = Self, Error = Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let headers = req.headers().clone();
|
||||
let tags = {
|
||||
let exts = req.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
}
|
||||
};
|
||||
|
||||
Box::new(future::ok(TestErrorRequest {
|
||||
headers,
|
||||
tags: Some(tags),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a user-identifier from the authentication token and validate against the URL
|
||||
///
|
||||
/// This token should be adapted as needed for the storage system to store data
|
||||
@ -940,7 +1062,7 @@ impl HawkIdentifier {
|
||||
}
|
||||
}
|
||||
|
||||
fn uid_from_path(uri: &Uri) -> Result<u64, Error> {
|
||||
fn uid_from_path(uri: &Uri, tags: Option<Tags>) -> Result<u64, Error> {
|
||||
// TODO: replace with proper path parser.
|
||||
// path: "/1.5/{uid}"
|
||||
let elements: Vec<&str> = uri.path().split('/').collect();
|
||||
@ -951,6 +1073,7 @@ impl HawkIdentifier {
|
||||
"Invalid UID".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("uid".to_owned()),
|
||||
tags.clone(),
|
||||
)
|
||||
.into()
|
||||
})
|
||||
@ -960,6 +1083,7 @@ impl HawkIdentifier {
|
||||
"Missing UID".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("uid".to_owned()),
|
||||
tags,
|
||||
))?
|
||||
}
|
||||
}
|
||||
@ -970,6 +1094,7 @@ impl HawkIdentifier {
|
||||
uri: &Uri,
|
||||
ci: &ConnectionInfo,
|
||||
state: &ServerState,
|
||||
tags: Option<Tags>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
T: HttpMessage,
|
||||
@ -990,7 +1115,7 @@ impl HawkIdentifier {
|
||||
.map_err(|e| -> ApiError { HawkErrorKind::Header(e).into() })?,
|
||||
_ => "",
|
||||
};
|
||||
let identifier = Self::generate(&state.secrets, method, auth_header, ua, ci, uri)?;
|
||||
let identifier = Self::generate(&state.secrets, method, auth_header, ua, ci, uri, tags)?;
|
||||
msg.extensions_mut().insert(identifier.clone());
|
||||
Ok(identifier)
|
||||
}
|
||||
@ -1002,15 +1127,18 @@ impl HawkIdentifier {
|
||||
ua: &str,
|
||||
connection_info: &ConnectionInfo,
|
||||
uri: &Uri,
|
||||
tags: Option<Tags>,
|
||||
) -> Result<Self, Error> {
|
||||
let payload = HawkPayload::extrude(header, method, secrets, connection_info, uri)?;
|
||||
let puid = Self::uid_from_path(&uri)?;
|
||||
let payload =
|
||||
HawkPayload::extrude(header, method, secrets, connection_info, uri, tags.clone())?;
|
||||
let puid = Self::uid_from_path(&uri, tags.clone())?;
|
||||
if payload.user_id != puid {
|
||||
info!("⚠️ Hawk UID not in URI: {:?} {:?}", payload.user_id, uri);
|
||||
Err(ValidationErrorKind::FromDetails(
|
||||
"conflicts with payload".to_owned(),
|
||||
RequestErrorLocation::Path,
|
||||
Some("uid".to_owned()),
|
||||
tags,
|
||||
))?;
|
||||
}
|
||||
|
||||
@ -1030,7 +1158,9 @@ impl FromRequest for HawkIdentifier {
|
||||
type Future = Result<Self, Self::Error>;
|
||||
|
||||
/// Use HawkPayload extraction and format as HawkIdentifier.
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let tags = Tags::from_request(req, payload)?;
|
||||
|
||||
let state = match req.app_data::<ServerState>() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@ -1039,6 +1169,7 @@ impl FromRequest for HawkIdentifier {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("state".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -1047,7 +1178,7 @@ impl FromRequest for HawkIdentifier {
|
||||
let connection_info = req.connection_info().clone();
|
||||
let method = req.method().as_str();
|
||||
let uri = req.uri();
|
||||
Self::extrude(req, method, uri, &connection_info, &state)
|
||||
Self::extrude(req, method, uri, &connection_info, &state, Some(tags))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1119,17 +1250,24 @@ impl FromRequest for BsoQueryParams {
|
||||
|
||||
/// Extract and validate the query parameters
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let tags = Tags::from_request(req, payload)?;
|
||||
|
||||
let params = Query::<BsoQueryParams>::from_request(req, payload)
|
||||
.map_err(|e| {
|
||||
ValidationErrorKind::FromDetails(
|
||||
e.to_string(),
|
||||
RequestErrorLocation::QueryString,
|
||||
None,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?
|
||||
.into_inner();
|
||||
params.validate().map_err(|e| {
|
||||
ValidationErrorKind::FromValidationErrors(e, RequestErrorLocation::QueryString)
|
||||
ValidationErrorKind::FromValidationErrors(
|
||||
e,
|
||||
RequestErrorLocation::QueryString,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?;
|
||||
Ok(params)
|
||||
}
|
||||
@ -1160,12 +1298,16 @@ impl FromRequest for BatchRequestOpt {
|
||||
type Future = Result<BatchRequestOpt, Self::Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let tags = Tags::from_request(req, payload)?;
|
||||
// let tags = Tags::from_request_head(req.head());
|
||||
let ftags = tags.clone();
|
||||
let params = Query::<BatchParams>::from_request(req, payload)
|
||||
.map_err(|e| {
|
||||
ValidationErrorKind::FromDetails(
|
||||
e.to_string(),
|
||||
RequestErrorLocation::QueryString,
|
||||
None,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?
|
||||
.into_inner();
|
||||
@ -1177,6 +1319,7 @@ impl FromRequest for BatchRequestOpt {
|
||||
"Internal error".to_owned(),
|
||||
RequestErrorLocation::Unknown,
|
||||
Some("state".to_owned()),
|
||||
Some(tags),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -1197,6 +1340,7 @@ impl FromRequest for BatchRequestOpt {
|
||||
e.to_string(),
|
||||
RequestErrorLocation::Header,
|
||||
Some((*header).to_owned()),
|
||||
Some(tags.clone()),
|
||||
)
|
||||
.into();
|
||||
err
|
||||
@ -1208,6 +1352,7 @@ impl FromRequest for BatchRequestOpt {
|
||||
format!("Invalid integer value: {}", value),
|
||||
RequestErrorLocation::Header,
|
||||
Some((*header).to_owned()),
|
||||
Some(tags.clone()),
|
||||
)
|
||||
.into();
|
||||
err
|
||||
@ -1217,6 +1362,7 @@ impl FromRequest for BatchRequestOpt {
|
||||
"size-limit-exceeded".to_owned(),
|
||||
RequestErrorLocation::Header,
|
||||
None,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -1231,12 +1377,17 @@ impl FromRequest for BatchRequestOpt {
|
||||
"Commit with no batch specified".to_string(),
|
||||
RequestErrorLocation::Path,
|
||||
None,
|
||||
Some(tags),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
params.validate().map_err(|e| {
|
||||
ValidationErrorKind::FromValidationErrors(e, RequestErrorLocation::QueryString)
|
||||
ValidationErrorKind::FromValidationErrors(
|
||||
e,
|
||||
RequestErrorLocation::QueryString,
|
||||
Some(tags.clone()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let id = match params.batch {
|
||||
@ -1249,6 +1400,7 @@ impl FromRequest for BatchRequestOpt {
|
||||
format!(r#"Invalid batch ID: "{}""#, batch),
|
||||
RequestErrorLocation::QueryString,
|
||||
Some("batch".to_owned()),
|
||||
Some(ftags),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -1284,7 +1436,7 @@ pub struct PreConditionHeaderOpt {
|
||||
}
|
||||
|
||||
impl PreConditionHeaderOpt {
|
||||
pub fn extrude(headers: &HeaderMap) -> Result<Self, Error> {
|
||||
pub fn extrude(headers: &HeaderMap, tags: Option<Tags>) -> Result<Self, Error> {
|
||||
let modified = headers.get("X-If-Modified-Since");
|
||||
let unmodified = headers.get("X-If-Unmodified-Since");
|
||||
if modified.is_some() && unmodified.is_some() {
|
||||
@ -1293,6 +1445,7 @@ impl PreConditionHeaderOpt {
|
||||
"conflicts with X-If-Modified-Since".to_owned(),
|
||||
RequestErrorLocation::Header,
|
||||
Some("X-If-Unmodified-Since".to_owned()),
|
||||
tags,
|
||||
)
|
||||
.into());
|
||||
};
|
||||
@ -1315,6 +1468,7 @@ impl PreConditionHeaderOpt {
|
||||
"value is negative".to_owned(),
|
||||
RequestErrorLocation::Header,
|
||||
Some("X-If-Modified-Since".to_owned()),
|
||||
tags,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@ -1325,6 +1479,7 @@ impl PreConditionHeaderOpt {
|
||||
e.to_string(),
|
||||
RequestErrorLocation::Header,
|
||||
Some(field_name.to_owned()),
|
||||
tags.clone(),
|
||||
)
|
||||
.into()
|
||||
})
|
||||
@ -1334,6 +1489,7 @@ impl PreConditionHeaderOpt {
|
||||
e.to_string(),
|
||||
RequestErrorLocation::Header,
|
||||
Some(field_name.to_owned()),
|
||||
tags.clone(),
|
||||
)
|
||||
.into()
|
||||
})
|
||||
@ -1355,8 +1511,9 @@ impl FromRequest for PreConditionHeaderOpt {
|
||||
type Future = Result<Self, Self::Error>;
|
||||
|
||||
/// Extract and validate the precondition headers
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
Self::extrude(req.headers()).map_err(Into::into)
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let tags = Tags::from_request(req, payload)?;
|
||||
Self::extrude(req.headers(), Some(tags)).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1935,7 +2092,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_invalid_precondition_headers() {
|
||||
fn assert_invalid_header(req: HttpRequest, _error_header: &str, _error_message: &str) {
|
||||
let result = PreConditionHeaderOpt::extrude(&req.headers());
|
||||
let tags = match req.extensions().get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
};
|
||||
let result = PreConditionHeaderOpt::extrude(&req.headers(), Some(tags));
|
||||
assert!(result.is_err());
|
||||
let response: HttpResponse = result.err().unwrap().into();
|
||||
assert_eq!(response.status(), 400);
|
||||
@ -1975,7 +2136,7 @@ mod tests {
|
||||
.data(make_state())
|
||||
.header("X-If-Modified-Since", "32.1")
|
||||
.to_http_request();
|
||||
let result = PreConditionHeaderOpt::extrude(&req.headers())
|
||||
let result = PreConditionHeaderOpt::extrude(&req.headers(), None)
|
||||
.unwrap()
|
||||
.opt
|
||||
.unwrap();
|
||||
@ -1987,7 +2148,7 @@ mod tests {
|
||||
.data(make_state())
|
||||
.header("X-If-Unmodified-Since", "32.14")
|
||||
.to_http_request();
|
||||
let result = PreConditionHeaderOpt::extrude(&req.headers())
|
||||
let result = PreConditionHeaderOpt::extrude(&req.headers(), None)
|
||||
.unwrap()
|
||||
.opt
|
||||
.unwrap();
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
//! API Handlers
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::{http::StatusCode, Error, HttpResponse};
|
||||
use actix_web::{http::StatusCode, Error, HttpRequest, HttpResponse};
|
||||
use futures::future::{self, Either, Future};
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::db::{params, results::Paginated, DbError, DbErrorKind};
|
||||
use crate::error::ApiError;
|
||||
use crate::error::{ApiError, ApiErrorKind};
|
||||
use crate::web::extractors::{
|
||||
BsoPutRequest, BsoRequest, CollectionPostRequest, CollectionRequest, ConfigRequest,
|
||||
HeartbeatRequest, MetaRequest, ReplyFormat,
|
||||
HeartbeatRequest, MetaRequest, ReplyFormat, TestErrorRequest,
|
||||
};
|
||||
use crate::web::{X_LAST_MODIFIED, X_WEAVE_NEXT_OFFSET, X_WEAVE_RECORDS};
|
||||
|
||||
@ -431,3 +431,30 @@ pub fn heartbeat(hb: HeartbeatRequest) -> impl Future<Item = HttpResponse, Error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn test_error(
|
||||
_req: HttpRequest,
|
||||
ter: TestErrorRequest,
|
||||
) -> impl Future<Item = HttpResponse, Error = ApiError> {
|
||||
// generate an error for sentry.
|
||||
|
||||
/* The various error log macros only can take a string.
|
||||
Additional values can be expressed as KV (key value) after a `;`
|
||||
e.g.
|
||||
```
|
||||
error!("Something Bad {:?}", err;
|
||||
"ua.os.family" => wtags.get("ua.os.family"),
|
||||
"ua.browser.family" => wtags.get("ua.browser.family"),
|
||||
"ua.name" => wtags.get("ua.name"),
|
||||
"ua.os.ver" => wtags.get("ua.os.ver"),
|
||||
"ua.browser.ver" => wtags.get("ua.browser.ver"))
|
||||
```
|
||||
TODO: find some way to transform Tags into error::KV
|
||||
*/
|
||||
error!("Test Error: {:?}", &ter.tags);
|
||||
|
||||
// ApiError will call the middleware layer to auto-append the tags.
|
||||
let err = ApiError::from(ApiErrorKind::Internal("Oh Noes!".to_owned()));
|
||||
|
||||
future::result(Err(err))
|
||||
}
|
||||
|
||||
@ -8,12 +8,11 @@ use actix_service::{Service, Transform};
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{self, HeaderMap},
|
||||
header::{self, HeaderMap, HeaderValue},
|
||||
Method, StatusCode,
|
||||
},
|
||||
Error, HttpMessage, HttpResponse,
|
||||
};
|
||||
|
||||
use futures::{
|
||||
future::{self, Either, FutureResult},
|
||||
Future, Poll,
|
||||
@ -22,9 +21,12 @@ use futures::{
|
||||
use crate::db::{params, util::SyncTimestamp};
|
||||
use crate::error::{ApiError, ApiErrorKind};
|
||||
use crate::server::{metrics, ServerState};
|
||||
use crate::web::extractors::{
|
||||
extrude_db, BsoParam, CollectionParam, HawkIdentifier, PreConditionHeader,
|
||||
PreConditionHeaderOpt,
|
||||
use crate::web::{
|
||||
extractors::{
|
||||
extrude_db, BsoParam, CollectionParam, HawkIdentifier, PreConditionHeader,
|
||||
PreConditionHeaderOpt,
|
||||
},
|
||||
tags::Tags,
|
||||
};
|
||||
use crate::web::{X_LAST_MODIFIED, X_WEAVE_TIMESTAMP};
|
||||
|
||||
@ -33,7 +35,12 @@ pub struct WeaveTimestampMiddleware<S> {
|
||||
}
|
||||
|
||||
// Known DockerFlow commands for Ops callbacks
|
||||
const DOCKER_FLOW_ENDPOINTS: [&str; 3] = ["/__heartbeat__", "/__lbheartbeat__", "/__version__"];
|
||||
const DOCKER_FLOW_ENDPOINTS: [&str; 4] = [
|
||||
"/__heartbeat__",
|
||||
"/__lbheartbeat__",
|
||||
"/__version__",
|
||||
"/__error__",
|
||||
];
|
||||
|
||||
impl<S, B> Service for WeaveTimestampMiddleware<S>
|
||||
where
|
||||
@ -185,12 +192,24 @@ where
|
||||
}
|
||||
|
||||
fn call(&mut self, sreq: ServiceRequest) -> Self::Future {
|
||||
let no_agent = HeaderValue::from_str("NONE").unwrap();
|
||||
let useragent = sreq
|
||||
.headers()
|
||||
.get("user-agent")
|
||||
.unwrap_or(&no_agent)
|
||||
.to_str()
|
||||
.unwrap_or("NONE");
|
||||
info!(">>> testing db middleware"; "user_agent" => useragent);
|
||||
if DOCKER_FLOW_ENDPOINTS.contains(&sreq.uri().path().to_lowercase().as_str()) {
|
||||
let mut service = Rc::clone(&self.service);
|
||||
return Box::new(service.call(sreq));
|
||||
}
|
||||
|
||||
let col_result = CollectionParam::extrude(&sreq.uri(), &mut sreq.extensions_mut());
|
||||
let tags = match sreq.extensions().get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(sreq.head()),
|
||||
};
|
||||
let col_result = CollectionParam::extrude(&sreq.uri(), &mut sreq.extensions_mut(), &tags);
|
||||
let state = match &sreq.app_data::<ServerState>() {
|
||||
Some(v) => v.clone(),
|
||||
None => {
|
||||
@ -224,7 +243,7 @@ where
|
||||
let hawk_user_id = match sreq.get_hawk_id() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
debug!("⚠️ Bad Hawk Id: {:?}", e);
|
||||
debug!("⚠️ Bad Hawk Id: {:?}", e; "user_agent"=> useragent);
|
||||
return Box::new(future::ok(
|
||||
sreq.into_response(
|
||||
HttpResponse::Unauthorized()
|
||||
@ -274,6 +293,103 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SentryWrapper;
|
||||
|
||||
impl SentryWrapper {
|
||||
pub fn new() -> Self {
|
||||
SentryWrapper::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SentryWrapper {
|
||||
fn default() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S> for SentryWrapper
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Transform = SentryWrapperMiddleware<S>;
|
||||
type Future = FutureResult<Self::Transform, Self::InitError>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
future::ok(SentryWrapperMiddleware {
|
||||
service: Rc::new(RefCell::new(service)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SentryWrapperMiddleware<S> {
|
||||
service: Rc<RefCell<S>>,
|
||||
}
|
||||
|
||||
impl<S, B> Service for SentryWrapperMiddleware<S>
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
self.service.poll_ready()
|
||||
}
|
||||
|
||||
fn call(&mut self, sreq: ServiceRequest) -> Self::Future {
|
||||
let mut tags = Tags::from_request_head(sreq.head());
|
||||
sreq.extensions_mut().insert(tags.clone());
|
||||
|
||||
Box::new(self.service.call(sreq).and_then(move |sresp| {
|
||||
// handed an actix_error::error::Error;
|
||||
// Fetch out the tags (in case any have been added.)
|
||||
match sresp.response().error() {
|
||||
None => {}
|
||||
Some(e) => {
|
||||
// The extensions defined in the request do not get populated
|
||||
// into the response. There can be two different, and depending
|
||||
// on where a tag may be set, only one set may be available.
|
||||
// Base off of the request, then overwrite/suppliment with the
|
||||
// response.
|
||||
if let Some(t) = sresp.request().extensions().get::<Tags>() {
|
||||
debug!("Found request tags: {:?}", &t.tags);
|
||||
for (k, v) in t.tags.clone() {
|
||||
tags.tags.insert(k, v);
|
||||
}
|
||||
};
|
||||
if let Some(t) = sresp.response().extensions().get::<Tags>() {
|
||||
debug!("Found response tags: {:?}", &t.tags);
|
||||
for (k, v) in t.tags.clone() {
|
||||
tags.tags.insert(k, v);
|
||||
}
|
||||
};
|
||||
// deriving the sentry event from a fail directly from the error
|
||||
// is not currently thread safe. Downcasting the error to an
|
||||
// ApiError resolves this.
|
||||
let apie: Option<&ApiError> = e.as_error();
|
||||
if let Some(apie) = apie {
|
||||
let mut event = sentry::integrations::failure::event_from_fail(apie);
|
||||
event.tags = tags.as_btree();
|
||||
sentry::capture_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
sresp
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// The resource in question's Timestamp
|
||||
pub struct ResourceTimestamp(SyncTimestamp);
|
||||
|
||||
@ -339,7 +455,15 @@ where
|
||||
}
|
||||
|
||||
// Pre check
|
||||
let precondition = match PreConditionHeaderOpt::extrude(&sreq.headers()) {
|
||||
let tags = {
|
||||
let exts = sreq.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(sreq.head()),
|
||||
}
|
||||
};
|
||||
let precondition = match PreConditionHeaderOpt::extrude(&sreq.headers(), Some(tags.clone()))
|
||||
{
|
||||
Ok(precond) => match precond.opt {
|
||||
Some(p) => p,
|
||||
None => PreConditionHeader::NoHeader,
|
||||
@ -386,7 +510,7 @@ where
|
||||
}
|
||||
};
|
||||
let uri = &sreq.uri();
|
||||
let col_result = CollectionParam::extrude(&uri, &mut sreq.extensions_mut());
|
||||
let col_result = CollectionParam::extrude(&uri, &mut sreq.extensions_mut(), &tags);
|
||||
let collection = match col_result {
|
||||
Ok(v) => v.map(|c| c.collection),
|
||||
Err(e) => {
|
||||
@ -401,7 +525,7 @@ where
|
||||
));
|
||||
}
|
||||
};
|
||||
let bso = BsoParam::extrude(&sreq.uri(), &mut sreq.extensions_mut()).ok();
|
||||
let bso = BsoParam::extrude(sreq.head(), &mut sreq.extensions_mut()).ok();
|
||||
let bso_opt = bso.map(|b| b.bso);
|
||||
|
||||
let mut service = Rc::clone(&self.service);
|
||||
@ -474,7 +598,8 @@ impl SyncServerRequest for ServiceRequest {
|
||||
let state = &self.app_data::<ServerState>().ok_or_else(|| -> ApiError {
|
||||
ApiErrorKind::Internal("No app_data ServerState".to_owned()).into()
|
||||
})?;
|
||||
HawkIdentifier::extrude(self, &method.as_str(), &self.uri(), &ci, &state)
|
||||
let tags = Tags::from_request_head(self.head());
|
||||
HawkIdentifier::extrude(self, &method.as_str(), &self.uri(), &ci, &state, Some(tags))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ pub mod error;
|
||||
pub mod extractors;
|
||||
pub mod handlers;
|
||||
pub mod middleware;
|
||||
pub mod tags;
|
||||
|
||||
// header statics must be lower case, numbers and symbols per the RFC spec. This reduces chance of error.
|
||||
pub static X_LAST_MODIFIED: &str = "x-last-modified";
|
||||
|
||||
108
src/web/tags.rs
Normal file
108
src/web/tags.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use actix_web::{
|
||||
dev::{Payload, RequestHead},
|
||||
http::header::USER_AGENT,
|
||||
Error, FromRequest, HttpRequest,
|
||||
};
|
||||
use serde::{
|
||||
ser::{SerializeMap, Serializer},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
use crate::server::user_agent::parse_user_agent;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tags {
|
||||
pub tags: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for Tags {
|
||||
fn default() -> Tags {
|
||||
Tags {
|
||||
tags: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Tags {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_map(Some(self.tags.len()))?;
|
||||
for tag in self.tags.clone() {
|
||||
seq.serialize_entry(&tag.0, &tag.1)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Tags {
|
||||
pub fn from_request_head(req_head: &RequestHead) -> Tags {
|
||||
// Return an Option<> type because the later consumers (ApiErrors) presume that
|
||||
// tags are optional and wrapped by an Option<> type.
|
||||
let mut tags = HashMap::new();
|
||||
if let Some(ua) = req_head.headers().get(USER_AGENT) {
|
||||
if let Ok(uas) = ua.to_str() {
|
||||
let (ua_result, metrics_os, metrics_browser) = parse_user_agent(uas);
|
||||
|
||||
tags.insert("ua.os.family".to_owned(), metrics_os.to_owned());
|
||||
tags.insert("ua.browser.family".to_owned(), metrics_browser.to_owned());
|
||||
tags.insert("ua.name".to_owned(), ua_result.name.to_owned());
|
||||
tags.insert(
|
||||
"ua.os.ver".to_owned(),
|
||||
ua_result.os_version.to_owned().to_string(),
|
||||
);
|
||||
tags.insert("ua.browser.ver".to_owned(), ua_result.version.to_owned());
|
||||
}
|
||||
}
|
||||
tags.insert("uri.path".to_owned(), req_head.uri.to_string());
|
||||
tags.insert("uri.method".to_owned(), req_head.method.to_string());
|
||||
Tags { tags }
|
||||
}
|
||||
|
||||
pub fn with_tags(tags: HashMap<String, String>) -> Tags {
|
||||
if tags.is_empty() {
|
||||
return Tags::default();
|
||||
}
|
||||
Tags { tags }
|
||||
}
|
||||
|
||||
pub fn as_btree(&self) -> BTreeMap<String, String> {
|
||||
let mut result = BTreeMap::new();
|
||||
|
||||
for (k, v) in &self.tags {
|
||||
result.insert(k.clone(), v.clone());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get(&self, label: &str) -> String {
|
||||
let none = "None".to_owned();
|
||||
self.tags.get(label).map(String::from).unwrap_or(none)
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, tags: HashMap<String, String>) {
|
||||
self.tags.extend(tags);
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for Tags {
|
||||
type Config = ();
|
||||
type Error = Error;
|
||||
type Future = Result<Self, Self::Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let tags = {
|
||||
let exts = req.extensions();
|
||||
match exts.get::<Tags>() {
|
||||
Some(t) => t.clone(),
|
||||
None => Tags::from_request_head(req.head()),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user