use anyhow::Result;
use fastly::http::{header, Method, StatusCode};
use fastly::{ConfigStore, Error, Request, Response};
use hmac_sha256::{Hash, HMAC};
const BACKEND_GCS: &str = "origin_0";
const EMPTY_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
struct GcsConfig {
access_key_id: String,
secret_access_key: String,
region: String,
bucket: String,
}
impl GcsConfig {
fn load_config() -> Self {
let cs = ConfigStore::open("gcs_config");
Self {
access_key_id: cs.get("access_key_id").expect("access key id configured"),
secret_access_key: cs.get("secret_access_key").expect("secret access key configured"),
region: cs.get("region").expect("region configured"),
bucket: cs.get("bucket").expect("bucket configured"),
}
}
}
#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
log_fastly::init_simple("my_log", log::LevelFilter::Info);
fastly::log::set_panic_endpoint("my_log")?;
let gcs_config = GcsConfig::load_config();
let resp = match req.get_method() {
&Method::GET | &Method::HEAD => {
req = authorize_gcs_request(
req,
&gcs_config.access_key_id,
&gcs_config.secret_access_key,
&gcs_config.region,
&gcs_config.bucket,
)?;
req.send(BACKEND_GCS)?
}
m if m == "PURGE" => {
req.remove_query();
req.set_header(
header::HOST,
format!("{}.storage.googleapis.com", gcs_config.bucket),
);
req.send(BACKEND_GCS)?
}
_ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED)
.with_header(header::ALLOW, "GET, HEAD"),
};
Ok(resp)
}
fn sign(key: &[u8], message: &str) -> [u8; 32] {
HMAC::mac(message.as_bytes(), key)
}
fn authorize_gcs_request(
mut req: Request,
access_key_id: &str,
secret_access_key: &str,
region: &str,
bucket: &str,
) -> Result<Request> {
req.remove_query();
let canonical_querystring = "";
let x_amz_content_sha256 = EMPTY_HASH;
let amz_date = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
let host = format!("{}.storage.googleapis.com", bucket);
let path_url_decoded = urlencoding::decode(req.get_path())?;
let path_url_encoded = urlencoding::encode(&path_url_decoded);
let canonical_uri = path_url_encoded.replace("%2F", "/");
let authorization_value = generate_s3_authorization_header(
access_key_id,
secret_access_key,
region,
&host,
&canonical_uri,
canonical_querystring,
x_amz_content_sha256,
"GET",
&amz_date,
"s3",
);
req.set_method(Method::GET);
req.set_header(header::HOST, &host);
req.set_header(header::AUTHORIZATION, &authorization_value);
req.set_header("x-amz-content-sha256", x_amz_content_sha256);
req.set_header("x-amz-date", &amz_date);
log::info!(
"Path: {}, Host: {}, x-amz-date: {}, Authorization: {}",
req.get_path(),
host,
amz_date,
authorization_value,
);
Ok(req)
}
#[allow(clippy::too_many_arguments)]
fn generate_s3_authorization_header(
access_key_id: &str,
secret_access_key: &str,
region: &str,
host: &str,
canonical_uri: &str,
canonical_querystring: &str,
x_amz_content_sha256: &str,
method: &str,
amz_date: &str,
service: &str,
) -> String {
let canonical_headers = format!(
"host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n",
host, x_amz_content_sha256, amz_date
);
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
let canonical_request = format!(
"{}\n{}\n{}\n{}\n{}\n{}",
method,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
x_amz_content_sha256
);
let credential_date = &amz_date[..8];
let credential_scope = format!("{}/{}/{}/aws4_request", credential_date, region, service);
let canonical_request_hash = hex::encode(Hash::hash(canonical_request.as_bytes()));
let string_to_sign = format!(
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
amz_date, credential_scope, canonical_request_hash
);
let aws4_secret = format!("AWS4{}", secret_access_key).into_bytes();
let date_key = sign(&aws4_secret, credential_date);
let region_key = sign(&date_key, region);
let service_key = sign(®ion_key, service);
let signing_key = sign(&service_key, "aws4_request");
let signature = hex::encode(sign(&signing_key, &string_to_sign));
format!(
"AWS4-HMAC-SHA256 Credential={}/{},SignedHeaders={},Signature={}",
access_key_id, credential_scope, signed_headers, signature
)
}