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:
JR Conlin 2019-12-31 15:56:04 -08:00 committed by GitHub
parent a3be0ac869
commit b834c54af6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1929 additions and 1435 deletions

2639
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"] }

View File

@ -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
View File

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

View File

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

View File

@ -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},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>,
}

View File

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

View File

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

View File

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

View File

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