//! Compute@Edge service which renders markdown from external sources. use fastly::http::{header, Method, StatusCode}; use fastly::{mime, Body, Error, Request, Response}; use pulldown_cmark::{html, Options, Parser}; use std::io::Read; /// The configured backend name. This is set up in the Fastly interface, and in fastly.toml /// for local development. const BACKEND_NAME: &str = "upstream_ssl"; /// The URL of the header markdown content. const HEADER_URL: &str = "http://nora.codes/edgeblog/_header.md"; /// The URL of the footer markdown content. const FOOTER_URL: &str = "http://nora.codes/edgeblog/_footer.md"; /// This function is invoked for every request. If it returns Err(), the user will see a 500. fn main() -> Result<(), Error> { // Get the request sent by the client. This hooks into the Compute@Edge runtime interface. let req = Request::from_client(); // Filter out unrelated request methods. match req.get_method() { // Allow GET and HEAD requests. &Method::GET | &Method::HEAD => (), // Deny anything else. _ => { Response::from_status(StatusCode::METHOD_NOT_ALLOWED) .with_header(header::ALLOW, "GET, HEAD") .with_body_text_plain("This method is not allowed\n") .send_to_client(); return Ok(()); } }; // Rewrite the / path to index.md, and construct a Request for the main body content. let upstream_req = match req.get_path() { "/" => Request::get("http://nora.codes/edgeblog/index.md"), path => Request::get(format!("http://nora.codes/edgeblog/{}", path)), }; // Get the data for the body portion of the page, and write the content into the Markdown // document. // This backend response is used as the template for the synthetic response. let mut beresp = upstream_req.send(BACKEND_NAME)?; // It's important to set Markdown's MIME type as "text" on your webserver; // I used "text/markdown". // Here, we check whether or not the object being requested is text. If so, we'll render it // inline; otherwise, we'll serve it as is. This allows supporting images, other documents, // and so forth without any extra effort. if beresp .get_content_type() .map(|mime| mime.type_() != mime::TEXT) .unwrap_or(true) { beresp.send_to_client(); return Ok(()); } // Get the data for the header portion of the page, making a backend request to the origin // server. This will be cached. let mut header_body = match Request::get(HEADER_URL).send(BACKEND_NAME) { Err(_e) => Body::new(), Ok(mut r) => r.take_body(), }; // Start off the Markdown document with the header content. let mut md_assembled = String::new(); header_body.read_to_string(&mut md_assembled)?; md_assembled.push_str("\n"); beresp.take_body().read_to_string(&mut md_assembled)?; // Get the data for the footer portion of the page, making a backend request to the origin // server. This will be cached. let mut footer_body = match Request::get(FOOTER_URL).send(BACKEND_NAME) { Err(_e) => Body::new(), Ok(mut r) => r.take_body(), }; // Write the footer into the synthetic body md_assembled.push_str("\n"); footer_body.read_to_string(&mut md_assembled)?; // Get the geoip info for the client, if possible. const UNKNOWN_LOCATION_STRING: &str = "unknown"; let geotext: String = match req.get_client_ip_addr() { Some(ip) => match fastly::geo::geo_lookup(ip) { None => UNKNOWN_LOCATION_STRING.into(), Some(geo) => format!( "AS{} '{}', in {}", geo.as_number(), geo.as_name(), geo.city() ) .into(), }, None => UNKNOWN_LOCATION_STRING.into(), }; md_assembled.push_str(&format!("\n
Your provider: {}
", geotext)); let parser = Parser::new_ext(&md_assembled, Options::empty()); let mut client_body = beresp .clone_without_body() .with_content_type(mime::TEXT_HTML_UTF_8) .stream_to_client(); html::write_html(&mut client_body, parser)?; Ok(()) }