Skip to content

Commit dad66bb

Browse files
committed
First working iteration
1 parent 4d0c049 commit dad66bb

File tree

8 files changed

+557
-0
lines changed

8 files changed

+557
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
Cargo.lock
3+
*.zip

Cargo.toml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "stm_inbox"
3+
version = "0.1.1"
4+
authors = ["rimutaka <[email protected]>"]
5+
edition = "2018"
6+
homepage = "https://stackmuncher.com"
7+
repository = "https://github.com/stackmuncher/stm_inbox"
8+
license = "AGPL-3.0-or-later"
9+
description = "An AWS Lambda function for storing StackMuncher reports in AWS S3. The reports are sent by StackMuncher client app (https://github.com/stackmuncher/stm)."
10+
11+
[dependencies]
12+
serde_json = "1.0.64"
13+
serde = { version = "1.0.126", features = ["derive"] }
14+
tokio = { version = "1.6", features = ["full"] }
15+
tracing = { version = "0.1.26", features = ["log"] }
16+
tracing-subscriber = "0.2.18"
17+
log = "0.4.14"
18+
lambda_runtime = { git = "https://github.com/awslabs/aws-lambda-rust-runtime.git" }
19+
hyper = { version = "0.14.7", features = ["http2"] }
20+
hyper-rustls = "0.22.1"
21+
rusoto_signature = "0.46.0"
22+
rusoto_sqs = { version = "0.46.0", features = ["rustls"], default-features = false }
23+
rusoto_core = { version = "0.46.0", features = ["rustls"], default-features = false }
24+
rusoto_s3 = { version = "0.46.0", features = ["rustls"], default-features = false }
25+
base64 = "0.13.0"
26+
bs58 = "0.4.0"
27+
ring = "0.16.20"
28+
chrono = { version = "0.4.19" }

README.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# StackMuncher Inbox for Report Submissions
2+
3+
This app is used as a Lambda function for receiving user report submissions.
4+
5+
The client app signs and submits one or more reports at the end of its stack analysis. The submissions contain the report in the body of HTTP POST message and several meta-headers:
6+
7+
* **StackMuncher_Key**: the public key of the user (required)
8+
* **StackMuncher_Sig**: the signature generated by the client app (required)
9+
10+
The sole purpose of this app is to save validated stack report submissions from devs and return a response as soon as possible.
11+
The validation is limited to checking the signature using the public key included in the submission.
12+
Validated reports are saved as-is with the key and timestamp of the submission.
13+
E.g. `s3://stm-reports-dev/queue/1621680890_7prBWD7pzYk2czeXZeXzjxjDQbnuka2RLShdW5AxWuk7.json`, where:
14+
* queue: prefix for new, unprocessed reports
15+
* 1621680890: an epoch timestamp of the submission
16+
* 7prBWD7pzYk2czeXZeXzjxjDQbnuka2RLShdW5AxWuk7: dev's public key in base58 format
17+
18+
## Deployment
19+
20+
The deployment should be automated. This section is a quick memo for manual deployment.
21+
22+
#### Lambda deployment
23+
24+
Create function called `stm_inbox` with `stm_inbox` role, a custom runtime and customize these settings:
25+
* env vars: see [config.rs](./src/config.rs) for the full list
26+
* timeout: 30s
27+
* reserved concurrency: 25
28+
* async invocation: ... TBC
29+
30+
```
31+
cargo build --release --target x86_64-unknown-linux-gnu
32+
cp ./target/x86_64-unknown-linux-gnu/release/stm_inbox ./bootstrap && zip proxy.zip bootstrap && rm bootstrap
33+
aws lambda update-function-code --region us-east-1 --function-name stm_inbox --zip-file fileb://proxy.zip
34+
```
35+
36+
#### API Gateway
37+
38+
* HTTP API with Lambda
39+
* ANY /{proxy+}
40+
* `stm_inbox` Lambda
41+
* `$default` stage
42+
43+
## Debugging
44+
45+
This app relies on https://github.com/rimutaka/lambda-debug-proxy to run a local copy on your dev machine connected to the GatewayAPI via SQS.
46+
This is a bit of a hack. Watch https://github.com/awslabs/aws-lambda-rust-runtime/issues/260 for possible standardization of this feature.
47+
48+
1. Deploy https://github.com/rimutaka/lambda-debug-proxy in place of *stm_inbox*
49+
2. Configure the request and response SQS queues
50+
3. Add `STM_INBOX_LAMBDA_PROXY_REQ` and `STM_INBOX_LAMBDA_PROXY_RESP` with the queue URLs to your *.bashrc*
51+
4. Use `cargo run` to launch *stm_inbox* app locally
52+
5. Send a request to the GWAPI endpoint to invoke *stm_inbox*
53+
54+
The above steps should trigger a chain of requests and responses:
55+
> APIGW -> Lambda *stm_inbox* proxy -> SQS Request Queue -> the locally run *stm_inbox* app -> SQS Response Queue -> Lambda *stm_inbox* proxy -> APIGW
56+
57+
[main.rs](./src/main.rs) has sections of code annotated with `#[cfg(debug_assertions)]` to use *lambda-debug-proxy* feature in DEBUG mode or exclude it when built with `--release`.

api-req-sample.json

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"body": "eyJ0ZWNoIjpbeyJsYW5ndWFnZSI6IlJ1c3QiLCJtdW5jaGVyX25hbWUiOiJydXN0LnJzIiwibXVuY2hlcl9oYXNoIjoyNzQ2ODk1MjU1NjI1ODQ0OTYsImZpbGVzIjozLCJ0b3RhbF9saW5lcyI6MTM1OSwiYmxhbmtfbGluZXMiOjE0NywiYnJhY2tldF9vbmx5X2xpbmVzIjoxNTUsImNvZGVfbGluZXMiOjg1NSwiaW5saW5lX2NvbW1lbnRzIjowLCJsaW5lX2NvbW1lbnRzIjoxMzEsImJsb2NrX2NvbW1lbnRzIjowLCJkb2NzX2NvbW1lbnRzIjo3MSwia2V5d29yZHMiOlt7ImsiOiJjcmF0ZSIsImMiOjZ9LHsiayI6InB1YiIsImMiOjUxfSx7ImsiOiJmbiIsImMiOjI2fSx7ImsiOiJtb2QiLCJjIjoxMn0seyJrIjoiYnJlYWsiLCJjIjoxfSx7ImsiOiJmYWxzZSIsImMiOjh9LHsiayI6ImZvciIsImMiOjMzfSx7ImsiOiJ0cnVlIiwiYyI6NX0seyJrIjoidXNlIiwiYyI6Mjd9LHsiayI6Im11dCIsImMiOjQwfSx7ImsiOiJzZWxmIiwiYyI6NDR9LHsiayI6ImltcGwiLCJjIjozfSx7ImsiOiJlbHNlIiwiYyI6MjJ9LHsiayI6ImFzeW5jIiwiYyI6Nn0seyJrIjoiaW4iLCJjIjo0Mn0seyJrIjoiYXMiLCJjIjoyfSx7ImsiOiJjb25zdCIsImMiOjN9LHsiayI6InJldHVybiIsImMiOjE1fSx7ImsiOiJzdGF0aWMiLCJjIjozfSx7ImsiOiJTZWxmIiwiYyI6MTB9LHsiayI6InN1cGVyIiwiYyI6NH0seyJrIjoiY29udGludWUiLCJjIjo1fSx7ImsiOiJzdHJ1Y3QiLCJjIjoxfSx7ImsiOiJpZiIsImMiOjY5fSx7ImsiOiJsb29wIiwiYyI6MX0seyJrIjoibWF0Y2giLCJjIjoxMH0seyJrIjoibGV0IiwiYyI6MTEyfSx7ImsiOiJhd2FpdCIsImMiOjEyfV19XSwicGVyX2ZpbGVfdGVjaCI6W3siZmlsZV9uYW1lIjoic3RhY2ttdW5jaGVyL3NyYy9saWIucnMiLCJsYW5ndWFnZSI6IlJ1c3QiLCJtdW5jaGVyX25hbWUiOiJydXN0LnJzIiwibXVuY2hlcl9oYXNoIjoyNzQ2ODk1MjU1NjI1ODQ0OTYsImNvbW1pdF9zaGExIjoiZDMzZmIzNmE3ZTk4YmQ5OWZhOTg4NWM1OWRkYTg5NjVmM2UxOTBhNiIsImNvbW1pdF9kYXRlX2Vwb2NoIjoxNjExMDI0MjY0LCJjb21taXRfZGF0ZV9pc28iOiIyMDIxLTAxLTE5VDAyOjQ0OjI0KzAwOjAwIiwiZmlsZXMiOjEsInRvdGFsX2xpbmVzIjo0NjIsImJsYW5rX2xpbmVzIjo0OSwiYnJhY2tldF9vbmx5X2xpbmVzIjo0MywiY29kZV9saW5lcyI6Mjk1LCJpbmxpbmVfY29tbWVudHMiOjAsImxpbmVfY29tbWVudHMiOjU0LCJibG9ja19jb21tZW50cyI6MCwiZG9jc19jb21tZW50cyI6MjEsImtleXdvcmRzIjpbeyJrIjoiY3JhdGUiLCJjIjoyfSx7ImsiOiJwdWIiLCJjIjoxNX0seyJrIjoiZm4iLCJjIjo2fSx7ImsiOiJtb2QiLCJjIjoxMX0seyJrIjoiZmFsc2UiLCJjIjozfSx7ImsiOiJmb3IiLCJjIjoxM30seyJrIjoidHJ1ZSIsImMiOjV9LHsiayI6InVzZSIsImMiOjd9LHsiayI6Im11dCIsImMiOjE0fSx7ImsiOiJzZWxmIiwiYyI6MTF9LHsiayI6ImltcGwiLCJjIjoxfSx7ImsiOiJlbHNlIiwiYyI6Nn0seyJrIjoiYXN5bmMiLCJjIjozfSx7ImsiOiJpbiIsImMiOjEwfSx7ImsiOiJhcyIsImMiOjF9LHsiayI6InJldHVybiIsImMiOjZ9LHsiayI6IlNlbGYiLCJjIjoyfSx7ImsiOiJjb250aW51ZSIsImMiOjJ9LHsiayI6ImlmIiwiYyI6MjV9LHsiayI6ImxldCIsImMiOjQ1fSx7ImsiOiJhd2FpdCIsImMiOjh9XX0seyJmaWxlX25hbWUiOiJzdGFja211bmNoZXIvc3JjL3JlcG9ydC5ycyIsImxhbmd1YWdlIjoiUnVzdCIsIm11bmNoZXJfbmFtZSI6InJ1c3QucnMiLCJtdW5jaGVyX2hhc2giOjI3NDY4OTUyNTU2MjU4NDQ5NiwiY29tbWl0X3NoYTEiOiJkMzNmYjM2YTdlOThiZDk5ZmE5ODg1YzU5ZGRhODk2NWYzZTE5MGE2IiwiY29tbWl0X2RhdGVfZXBvY2giOjE2MTEwMjQyNjQsImNvbW1pdF9kYXRlX2lzbyI6IjIwMjEtMDEtMTlUMDI6NDQ6MjQrMDA6MDAiLCJmaWxlcyI6MSwidG90YWxfbGluZXMiOjY3NiwiYmxhbmtfbGluZXMiOjcwLCJicmFja2V0X29ubHlfbGluZXMiOjg2LCJjb2RlX2xpbmVzIjo0MTQsImlubGluZV9jb21tZW50cyI6MCwibGluZV9jb21tZW50cyI6NTgsImJsb2NrX2NvbW1lbnRzIjowLCJkb2NzX2NvbW1lbnRzIjo0OCwia2V5d29yZHMiOlt7ImsiOiJjcmF0ZSIsImMiOjR9LHsiayI6InB1YiIsImMiOjM1fSx7ImsiOiJpbiIsImMiOjI3fSx7ImsiOiJpbXBsIiwiYyI6Mn0seyJrIjoiZm4iLCJjIjoxN30seyJrIjoidXNlIiwiYyI6MTZ9LHsiayI6IlNlbGYiLCJjIjo4fSx7ImsiOiJlbHNlIiwiYyI6MTR9LHsiayI6Im1vZCIsImMiOjF9LHsiayI6Im11dCIsImMiOjIxfSx7ImsiOiJhcyIsImMiOjF9LHsiayI6ImxldCIsImMiOjQ5fSx7ImsiOiJmYWxzZSIsImMiOjR9LHsiayI6InNlbGYiLCJjIjozM30seyJrIjoibWF0Y2giLCJjIjo3fSx7ImsiOiJyZXR1cm4iLCJjIjo3fSx7ImsiOiJjb25zdCIsImMiOjF9LHsiayI6ImF3YWl0IiwiYyI6MX0seyJrIjoiaWYiLCJjIjozNX0seyJrIjoiZm9yIiwiYyI6MTd9LHsiayI6InN0YXRpYyIsImMiOjF9LHsiayI6InN1cGVyIiwiYyI6NH0seyJrIjoiY29udGludWUiLCJjIjoxfSx7ImsiOiJhc3luYyIsImMiOjJ9LHsiayI6InN0cnVjdCIsImMiOjF9XX0seyJmaWxlX25hbWUiOiJzdG1hcHAvc3JjL21haW4ucnMiLCJsYW5ndWFnZSI6IlJ1c3QiLCJtdW5jaGVyX25hbWUiOiJydXN0LnJzIiwibXVuY2hlcl9oYXNoIjoyNzQ2ODk1MjU1NjI1ODQ0OTYsImNvbW1pdF9zaGExIjoiNmYzNGY4ODA4OTM4NGE3YzgwYmFkNGU0YmVmMjUyMTA1NGRhYjk4NCIsImNvbW1pdF9kYXRlX2Vwb2NoIjoxNjEwOTM0MjY3LCJjb21taXRfZGF0ZV9pc28iOiIyMDIxLTAxLTE4VDAxOjQ0OjI3KzAwOjAwIiwiZmlsZXMiOjEsInRvdGFsX2xpbmVzIjoyMjEsImJsYW5rX2xpbmVzIjoyOCwiYnJhY2tldF9vbmx5X2xpbmVzIjoyNiwiY29kZV9saW5lcyI6MTQ2LCJpbmxpbmVfY29tbWVudHMiOjAsImxpbmVfY29tbWVudHMiOjE5LCJibG9ja19jb21tZW50cyI6MCwiZG9jc19jb21tZW50cyI6Miwia2V5d29yZHMiOlt7ImsiOiJhc3luYyIsImMiOjF9LHsiayI6ImlmIiwiYyI6OX0seyJrIjoibGV0IiwiYyI6MTh9LHsiayI6Im11dCIsImMiOjV9LHsiayI6ImZuIiwiYyI6M30seyJrIjoiZm9yIiwiYyI6M30seyJrIjoidXNlIiwiYyI6NH0seyJrIjoicHViIiwiYyI6MX0seyJrIjoiaW4iLCJjIjo1fSx7ImsiOiJlbHNlIiwiYyI6Mn0seyJrIjoicmV0dXJuIiwiYyI6Mn0seyJrIjoibWF0Y2giLCJjIjozfSx7ImsiOiJjb25zdCIsImMiOjJ9LHsiayI6Imxvb3AiLCJjIjoxfSx7ImsiOiJmYWxzZSIsImMiOjF9LHsiayI6ImF3YWl0IiwiYyI6M30seyJrIjoiY29udGludWUiLCJjIjoyfSx7ImsiOiJicmVhayIsImMiOjF9LHsiayI6InN0YXRpYyIsImMiOjJ9XX1dLCJ0aW1lc3RhbXAiOiIyMDIxLTA1LTIxVDAxOjM2OjA5LjA5MDI0MzkzNSswMDowMCIsInJlbW90ZV91cmxfaGFzaGVzIjpbImIwYzMxN2I3YThiZmFiZjk1MTU0YmIyN2I3MDE5NzU2MWUzMjE4MTMiLCIyOWYwOGQ4ZDdiZGQxZDg5NWYxNTIwOGMwY2VjNmJiMTJjNzFiMmE2Il0sInJlcG9ydF9pZCI6ImRkZTEzYjAzLTliMmQtNGFhNS1iNWQxLTU5MzYyZGMxZTgxYyIsInJlcG9ydF9jb21taXRfc2hhMSI6IjBmZGRjZGE1YmEwMTBkNWMzMmE0ZTk4NDkzNjEwODU2NGVmODZmMDgiLCJsb2dfaGFzaCI6ImM1N2Y0NWE5N2FiMjg1N2Q4OTcxZDEwZGFiNGZhMzY4ODUyMTdiMjgiLCJnaXRfaWRzX2luY2x1ZGVkIjpbInVidW50dUBpcC0xNzItMzEtNDctMjguYXAtc291dGhlYXN0LTIuY29tcHV0ZS5pbnRlcm5hbCJdLCJpc19zaW5nbGVfY29tbWl0Ijp0cnVlLCJsYXN0X2NvbW1pdF9hdXRob3IiOiJtYXhAb25lYnJvLm1lIn0=",
3+
"headers": {
4+
"content-length": "3839",
5+
"host": "emvuertw1ec.execute-api.us-east-1.amazonaws.com",
6+
"stackmuncher_key": "EFY9NXEytYgBgGsyAeGfXzkBEBQzC9NXFyj47EPdmVLB",
7+
"stackmuncher_sig": "3phLLQyiquyX4xge3CXYGCfb1KdrXQ8cTgBbvE8obwCkcm7vPdLsKT6JtNCdF9qeyjcgF2b4kTRXEsoMTHcQr43n",
8+
"x-amzn-trace-id": "Root=1-60a70e8a-7a28629c2fce00f17f26aa16",
9+
"x-forwarded-for": "13.54.209.171",
10+
"x-forwarded-port": "443",
11+
"x-forwarded-proto": "https"
12+
},
13+
"isBase64Encoded": true,
14+
"pathParameters": {
15+
"proxy": ""
16+
},
17+
"rawPath": "/",
18+
"rawQueryString": "",
19+
"requestContext": {
20+
"accountId": "11111111111",
21+
"apiId": "emvuertw1ec",
22+
"domainName": "emvuertw1ec.execute-api.us-east-1.amazonaws.com",
23+
"domainPrefix": "emvuertw1ec",
24+
"http": {
25+
"method": "POST",
26+
"path": "/",
27+
"protocol": "HTTP/1.1",
28+
"sourceIp": "13.54.209.171",
29+
"userAgent": ""
30+
},
31+
"requestId": "fp81nhsBoAMESKw=",
32+
"routeKey": "POST /{proxy+}",
33+
"stage": "$default",
34+
"time": "21/May/2021:01:36:10 +0000",
35+
"timeEpoch": 1621560970266
36+
},
37+
"routeKey": "POST /{proxy+}",
38+
"version": "2.0"
39+
}

src/config.rs

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use hyper_rustls::HttpsConnector;
2+
use rusoto_core::credential::DefaultCredentialsProvider;
3+
use rusoto_core::HttpClient;
4+
use rusoto_core::Region;
5+
use rusoto_s3::S3Client;
6+
use std::str::FromStr;
7+
use std::time::Duration;
8+
9+
/// Name of a required env variable (STM_INBOX_S3_REGION)
10+
/// E.g. `us-east-1`
11+
pub const S3_REGION_ENV: &str = "STM_INBOX_S3_REGION";
12+
/// Name of a required env variable (STM_INBOX_S3_BUCKET)
13+
/// E.g. `stm-reports-prod`
14+
pub const S3_BUCKET_ENV: &str = "STM_INBOX_S3_BUCKET";
15+
/// Name of a required env variable (STM_INBOX_S3_BUCKET)
16+
/// The storage tree with any additional folders is placed under this prefix.
17+
/// E.g. `queue`, leading/trailing `/` are removed
18+
pub const S3_PREFIX_ENV: &str = "STM_INBOX_S3_PREFIX";
19+
20+
/// A struct with all the config info passed around as a single param
21+
pub struct Config {
22+
/// The name of the bucket for storing contributor reports.
23+
/// E.g. `stm-subs-j5awwhv9pb9np7d`
24+
pub s3_bucket: String,
25+
/// The base prefix within the bucket to the root of the report storage.
26+
/// The storage tree with any additional folders is placed under this prefix.
27+
/// E.g. `queue`, leading/trailing `/` are removed
28+
pub s3_prefix: String,
29+
/// Contains an initialized S3 Client for reuse. Doesn't need to be public.
30+
pub s3_client: S3Client,
31+
}
32+
33+
impl Config {
34+
/// Initializes a new Config struct from the environment. Panics on invalid config values.
35+
pub fn new() -> Self {
36+
let s3_region = std::env::var(S3_REGION_ENV)
37+
.expect(&format!(
38+
"Missing {} env var with S3 region name, e.g. us-east-1",
39+
S3_REGION_ENV
40+
))
41+
.trim()
42+
.to_string();
43+
44+
let s3_region = Region::from_str(&s3_region)
45+
.expect("Invalid S3 Region value. Must look like `us-east-1`.");
46+
47+
Config {
48+
s3_bucket: std::env::var(S3_BUCKET_ENV)
49+
.expect(&format!(
50+
"Missing {} env var with S3 bucket name, e.g. stm-subs-j5awwhv9pb9np7d",
51+
S3_BUCKET_ENV
52+
))
53+
.trim()
54+
.trim_end_matches("/")
55+
.to_string(),
56+
s3_prefix: std::env::var(S3_PREFIX_ENV)
57+
.expect(&format!(
58+
"Missing {} env var with S3 prefix, e.g. `queue`",
59+
S3_PREFIX_ENV
60+
))
61+
.trim()
62+
.trim_end_matches("/")
63+
.to_string(),
64+
s3_client: generate_s3_client(s3_region),
65+
}
66+
}
67+
}
68+
69+
/// Generates an S3Client with custom settings to match AWS server defaults.
70+
fn generate_s3_client(s3_region: Region) -> S3Client {
71+
let https_connector = HttpsConnector::with_native_roots();
72+
73+
let cred_prov =
74+
DefaultCredentialsProvider::new().expect("Cannot unwrap DefaultCredentialsProvider");
75+
76+
let mut builder = hyper::Client::builder();
77+
builder.pool_idle_timeout(Duration::from_secs(15));
78+
builder.http2_keep_alive_interval(Duration::from_secs(5));
79+
builder.http2_keep_alive_timeout(Duration::from_secs(3));
80+
81+
let http_client = HttpClient::from_builder(builder, https_connector);
82+
83+
S3Client::new_with(http_client, cred_prov, s3_region)
84+
}

src/handler.rs

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use crate::s3;
2+
use crate::{config::Config, Error};
3+
use base64::decode;
4+
use lambda_runtime::Context;
5+
use ring::signature;
6+
use serde::{Deserialize, Serialize};
7+
use serde_json::Value;
8+
use std::collections::HashMap;
9+
use tracing::{debug, error, info};
10+
11+
#[derive(Serialize, Debug)]
12+
#[serde(rename_all = "camelCase")]
13+
struct ApiGatewayResponse {
14+
is_base64_encoded: bool,
15+
status_code: u32,
16+
headers: HashMap<String, String>,
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
body: Option<String>,
19+
}
20+
21+
#[derive(Deserialize, Debug)]
22+
struct ApiGatewayRequestHeaders {
23+
/// The user public key, which may or may not be known to us at the time of submission.
24+
/// A base58 encoded string, e.g. "EFY9NXEytYgBgGsyAeGfXzkBEBQzC9NXFyj47EPdmVLB"
25+
stackmuncher_key: Option<String>,
26+
/// The signature for the content sent in the body, base58 encoded, e.g.
27+
/// "3phLLQyiquyX4xge3CXYGCfb1KdrXQ8cTgBbvE8obwCkcm7vPdLsKT6JtNCdF9qeyjcgF2b4kTRXEsoMTHcQr43n"
28+
stackmuncher_sig: Option<String>,
29+
/// The IP address of the user. Apparently it is preferred over sourceIp field.
30+
/// See https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html
31+
#[serde(rename = "x-forwarded-for")]
32+
x_forwarded_for: Option<String>,
33+
}
34+
35+
#[derive(Deserialize, Debug)]
36+
#[serde(rename_all = "camelCase")]
37+
struct ApiGatewayRequest {
38+
headers: ApiGatewayRequestHeaders,
39+
is_base64_encoded: bool,
40+
body: Option<String>,
41+
}
42+
43+
/// A generic error message sent to the user when the request cannot be processed for a reason the user can't do much about.
44+
const ERROR_500_MSG: &str = "stackmuncher.com failed to process the report. If the error persists, can you log an issue at https://github.com/stackmuncher/stm_inbox/issues?";
45+
46+
pub(crate) async fn my_handler(event: Value, ctx: Context) -> Result<Value, Error> {
47+
// these 2 lines are for debugging only to see the raw APIGW request
48+
debug!("Event: {}", event);
49+
debug!("Context: {:?}", ctx);
50+
51+
// parse the request
52+
let api_request = match serde_json::from_value::<ApiGatewayRequest>(event.clone()) {
53+
Err(e) => {
54+
error!(
55+
"Failed to deser APIGW request due to {}. Request: {}",
56+
e, event
57+
);
58+
return gw_response(Some(ERROR_500_MSG.to_owned()), 500);
59+
}
60+
Ok(v) => v,
61+
};
62+
63+
info!("Report from IP: {:?}", api_request.headers.x_forwarded_for);
64+
65+
// these 2 headers are required no matter what
66+
if api_request.headers.stackmuncher_key.is_none()
67+
|| api_request.headers.stackmuncher_sig.is_none()
68+
{
69+
error!(
70+
"Missing a header. Key: {:?}, Sig: {:?}",
71+
api_request.headers.stackmuncher_key, api_request.headers.stackmuncher_sig
72+
);
73+
return gw_response(
74+
Some("stackmuncher.com failed to process the report: missing required HTTP headers. If you have not modified the source code it's a bug at stackmuncher.com end.".to_owned()),
75+
500,
76+
);
77+
}
78+
79+
// get the body contents and decode it if needed
80+
let body = match api_request.body {
81+
Some(v) => v,
82+
None => {
83+
error!("Empty body");
84+
return gw_response(
85+
Some("stackmuncher.com: no report found in the request. It's a bug in the app. Can you log an issue at https://github.com/stackmuncher/stm_inbox/issues?".to_owned()),
86+
500,
87+
);
88+
}
89+
};
90+
let body = if api_request.is_base64_encoded {
91+
match decode(body) {
92+
Ok(v) => v,
93+
Err(e) => {
94+
error!("Failed to decode the body due to: {}", e);
95+
return gw_response(Some(ERROR_500_MSG.to_owned()), 500);
96+
}
97+
}
98+
} else {
99+
body.as_bytes().into()
100+
};
101+
102+
info!("Body len: {}", body.len());
103+
debug!("Body: {}", String::from_utf8_lossy(&body));
104+
105+
// convert the public key from base58 into bytes
106+
let pub_key_bs58 = api_request
107+
.headers
108+
.stackmuncher_key
109+
.expect("Cannot unwrap stackmuncher_key. It's a bug.");
110+
111+
info!("Report for pub key: {}", pub_key_bs58);
112+
113+
let pub_key = match bs58::decode(pub_key_bs58.clone()).into_vec() {
114+
Ok(v) => v,
115+
Err(e) => {
116+
error!(
117+
"Failed to decode the stackmuncher_key from based58 due to: {}",
118+
e
119+
);
120+
return gw_response(Some(ERROR_500_MSG.to_owned()), 500);
121+
}
122+
};
123+
124+
// convert the signature from base58 into bytes
125+
let signature = match bs58::decode(
126+
api_request
127+
.headers
128+
.stackmuncher_sig
129+
.expect("Cannot unwrap stackmuncher_sig. It's a bug."),
130+
)
131+
.into_vec()
132+
{
133+
Ok(v) => v,
134+
Err(e) => {
135+
error!(
136+
"Failed to decode the stackmuncher_key from based58 due to: {}",
137+
e
138+
);
139+
return gw_response(Some(ERROR_500_MSG.to_owned()), 500);
140+
}
141+
};
142+
143+
// validate the signature
144+
let pub_key = signature::UnparsedPublicKey::new(&signature::ED25519, pub_key);
145+
match pub_key.verify(&body, &signature) {
146+
Ok(_) => {
147+
info!("Signature OK");
148+
}
149+
Err(e) => {
150+
error!("Invalid signature: {}", e);
151+
return gw_response(Some("Invalid StackMuncher signature. If the error persists, can you log an issue at https://github.com/stackmuncher/stm_inbox/issues?".to_owned()), 500);
152+
}
153+
};
154+
155+
let config = Config::new();
156+
s3::upload_to_s3(&config, body, pub_key_bs58).await;
157+
158+
// render the prepared data as HTML
159+
info!("Report stored");
160+
161+
// Submission accepted - return 200 with no body
162+
gw_response(None, 200)
163+
}
164+
165+
/// Prepares the response with the status and text or json body. May fail and return an error.
166+
fn gw_response(body: Option<String>, status_code: u32) -> Result<Value, Error> {
167+
let mut headers: HashMap<String, String> = HashMap::new();
168+
headers.insert("Content-Type".to_owned(), "text/text".to_owned());
169+
headers.insert("Cache-Control".to_owned(), "no-store".to_owned());
170+
171+
let resp = ApiGatewayResponse {
172+
is_base64_encoded: false,
173+
status_code,
174+
headers,
175+
body,
176+
};
177+
178+
Ok(serde_json::to_value(resp).expect("Failed to serialize response"))
179+
}

0 commit comments

Comments
 (0)