From 3cc28ab392bf066a727fa53f17716c3bd5271094 Mon Sep 17 00:00:00 2001 From: Asone Date: Fri, 11 Mar 2022 08:49:23 +0100 Subject: [PATCH 01/12] feature: First draft of multipart handling with Juniper --- Cargo.lock | 37 ++- Cargo.toml | 4 + src/app.rs | 104 ++++++- src/graphql/mod.rs | 1 + src/graphql/multipart/mod.rs | 1 + src/graphql/multipart/upload_request.bak | 262 ++++++++++++++++++ src/graphql/multipart/upload_request.rs | 334 +++++++++++++++++++++++ src/main.rs | 7 +- 8 files changed, 727 insertions(+), 23 deletions(-) create mode 100644 src/graphql/multipart/mod.rs create mode 100644 src/graphql/multipart/upload_request.bak create mode 100644 src/graphql/multipart/upload_request.rs diff --git a/Cargo.lock b/Cargo.lock index 38cc2cd..9508b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,10 +849,13 @@ dependencies = [ "juniper_rocket", "lazy_static", "lightning-invoice", + "multer", "rocket", + "rocket-multipart-form-data", "rocket_sync_db_pools", "serde", "serde_json", + "tokio-util", "tonic", "tonic_lnd", "uuid", @@ -952,9 +955,9 @@ dependencies = [ [[package]] name = "multer" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "408327e2999b839cd1af003fc01b2019a6c10a1361769542203f6fedc5179680" +checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" dependencies = [ "bytes", "encoding_rs", @@ -962,11 +965,11 @@ dependencies = [ "http", "httparse", "log", + "memchr", "mime", "spin 0.9.2", "tokio", "tokio-util", - "twoway", "version_check", ] @@ -1464,6 +1467,18 @@ dependencies = [ "yansi", ] +[[package]] +name = "rocket-multipart-form-data" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bec4978d352a6bd60b23b9e518096082f194abfbb03d0fdc189b5440dbb2798" +dependencies = [ + "mime", + "multer", + "rocket", + "tokio-util", +] + [[package]] name = "rocket_codegen" version = "0.5.0-rc.1" @@ -2172,16 +2187,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" -[[package]] -name = "twoway" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" -dependencies = [ - "memchr", - "unchecked-index", -] - [[package]] name = "ubyte" version = "0.10.1" @@ -2207,12 +2212,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unchecked-index" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" - [[package]] name = "unicode-bidi" version = "0.3.6" diff --git a/Cargo.toml b/Cargo.toml index 72e7db2..33d2280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,11 @@ serde = "1.0.136" serde_json = "1.0.68" lightning-invoice = "0.12.0" diesel_migrations = "1.4.0" +rocket-multipart-form-data = "0.10.0" uuid = { version = "0.8.2", features = ["serde", "v4"] } +multer = "2.0.2" +[dependencies.tokio-util] +version = "0.6.6" [dependencies.rocket_sync_db_pools] version = "0.1.0-rc.1" features = ["diesel_postgres_pool"] diff --git a/src/app.rs b/src/app.rs index cbbb871..ed0fb20 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,14 +1,19 @@ +use std::collections::HashMap; + use crate::{ graphql::{context::GQLContext, mutation::Mutation, query::Query}, lnd::client::LndClient, }; -use rocket::{response::content, State}; +use lightning_invoice::Sha256; +use rocket::{response::content, State, Data, data::ToByteUnit, form::Form, fs::TempFile, http::ContentType}; +use rocket_multipart_form_data::{MultipartFormDataOptions, MultipartFormData, MultipartFormDataField, FileField}; pub type Schema = RootNode<'static, Query, Mutation, EmptySubscription>; use crate::db::PostgresConn; use crate::requests::header::PaymentRequestHeader; use juniper::{EmptySubscription, RootNode}; use juniper_rocket::GraphQLResponse; +use serde_json::{Value}; #[rocket::get("/")] pub fn graphiql() -> content::Html { @@ -69,3 +74,100 @@ pub async fn payable_post_graphql_handler( .execute(&*schema, &GQLContext { pool: db, lnd: lnd }) .await } +#[rocket::post("/upload", data = "")] +pub async fn upload<'r>( + request: crate::graphql::multipart::upload_request::GraphQLUploadRequest, + schema: &State, + db: PostgresConn, + lnd: LndClient) -> GraphQLResponse { + + + request.execute(&*schema, &GQLContext { pool: db, lnd: lnd }) + .await +} +// #[derive(Debug, FromForm)] +// pub struct UploadedFile { +// operations: String, +// map: String +// } + +// Uploads a file to the server. +// #[rocket::post("/upload", data = "")] +// pub async fn upload<'r>(content_type: &ContentType, data: Data<'_>) -> &'static str { + // MultipartFormData expects us to provide a mapping of the multipart form data + // in order to be able to parse the boundaries from the request body. + + // As we do not know how many files are uploaded initially in the request nor their names + // what we'll do here is to to first parse the Data aknowledging the mapping object for files + // and parse the request content a second time once the mapping applied to the Content Form Data. + +// let mut options = MultipartFormDataOptions::new(); +// // println!("{}",data.as_str()); +// options.allowed_fields.push(MultipartFormDataField::text("map")); +// options.allowed_fields.push(MultipartFormDataField::text("operations")); + +// let multipart_form_data = MultipartFormData::parse(content_type, data, options).await; + + +// match multipart_form_data { +// Ok(mfp) => { + +// let mappings = mfp.texts.get("map").unwrap(); +// // let files = mfp.files.get("file").unwrap(); // .get(&"file".to_string()); + +// // for (i,f) in mapping.iter().enumerate() { + +// // println!("index key : {}; data: {}",i,&f.text); + + +// // for item in &v { +// // println!("{:?}\n", item); +// // } +// // } +// for mapping in mappings { +// let d = mapping.text.as_str(); +// println!("{}",d); +// let hmap: HashMap<&str, Value> = serde_json::from_str(d).unwrap(); +// let mut options2 = MultipartFormDataOptions::new(); +// // Gets the files mapping from request +// for (k,v) in hmap.iter() { +// options2.allowed_fields.push(MultipartFormDataField::file(k)); +// } +// // // println!("{}",data); +// // let multipart_form_data_2 = MultipartFormData::parse(content_type, data, options2).await; +// // match multipart_form_data_2 { +// // Ok(mfp2) => { +// // let t = mfp2; +// // }, +// // Err(_) => {} +// // } +// } + +// // for f in files { +// // let t = f.content_type.as_ref().unwrap(); +// // println!("{}",t); +// // } +// // if let Some(file) = file { +// // match file { +// // FileField::Single(f) => { +// // let _content_type = &file.content_type; +// // let _file_name = &file.file_name; +// // let _path = &file.path; +// // } +// // } +// // } +// }, +// Err(_) => {} +// }; +// // // println!("{:?}", w); +// // let byt = file.open(10.megabytes()); +// // let o = byt.into_bytes().await.unwrap().to_vec(); +// // // println!("{:?}", o); +// // let we = String::from_utf8(o.clone()).unwrap(); +// // println!("utf8-> {}", we); +// // let hasher = Sha256::new().chain(&o); +// // let result = hasher.finalize(); +// // println!("hash sha256: {:x}", &result); +// // Ok(()) +// "ok" +// } \ No newline at end of file diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index a774c0c..4a2d7b3 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -3,3 +3,4 @@ pub mod mutation; pub mod paywall_context; pub mod query; pub mod types; +pub mod multipart; \ No newline at end of file diff --git a/src/graphql/multipart/mod.rs b/src/graphql/multipart/mod.rs new file mode 100644 index 0000000..9292175 --- /dev/null +++ b/src/graphql/multipart/mod.rs @@ -0,0 +1 @@ +pub mod upload_request; \ No newline at end of file diff --git a/src/graphql/multipart/upload_request.bak b/src/graphql/multipart/upload_request.bak new file mode 100644 index 0000000..893bb81 --- /dev/null +++ b/src/graphql/multipart/upload_request.bak @@ -0,0 +1,262 @@ +//! Utilities for building HTTP endpoints in a library-agnostic manner + +// pub mod playground; + +use juniper::http::GraphQLResponse; +use serde::{ + de, + ser::{self, SerializeMap}, + Deserialize, Serialize, +}; + +use juniper::{ + // ast::InputValue, + executor::{ExecutionError, ValuesStream}, + // value::{DefaultScalarValue, ScalarValue}, + FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode, + Value, Variables, ScalarValue, DefaultScalarValue, InputValue, http::{GraphQLBatchResponse, GraphQLResponse}, +}; + +/// The expected structure of the decoded JSON document for either POST or GET requests. +/// +/// For POST, you can use Serde to deserialize the incoming JSON data directly +/// into this struct - it derives Deserialize for exactly this reason. +/// +/// For GET, you will need to parse the query string and extract "query", +/// "operationName", and "variables" manually. +#[derive(Deserialize, Clone, Serialize, PartialEq, Debug)] +pub struct GraphQLUploadRequest +where + S: ScalarValue, +{ + query: String, + #[serde(rename = "operationName")] + operation_name: Option, + #[serde(bound(deserialize = "InputValue: Deserialize<'de> + Serialize"))] + variables: Option>, +} + +impl GraphQLUploadRequest +where + S: ScalarValue, +{ + /// Returns the `operation_name` associated with this request. + pub fn operation_name(&self) -> Option<&str> { + self.operation_name.as_deref() + } + + fn variables(&self) -> Variables { + self.variables + .as_ref() + .and_then(|iv| { + iv.to_object_value().map(|o| { + o.into_iter() + .map(|(k, v)| (k.to_owned(), v.clone())) + .collect() + }) + }) + .unwrap_or_default() + } + + /// Construct a new GraphQL request from parts + pub fn new( + query: String, + operation_name: Option, + variables: Option>, + ) -> Self { + GraphQLUploadRequest { + query, + operation_name, + variables, + } + } + + /// Execute a GraphQL request synchronously using the specified schema and context + /// + /// This is a simple wrapper around the `execute_sync` function exposed at the + /// top level of this crate. + pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode, + context: &QueryT::Context, + ) -> GraphQLResponse + where + S: ScalarValue, + QueryT: GraphQLType, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + { + GraphQLResponse(juniper::execute_sync( + &self.query, + self.operation_name(), + root_node, + &self.variables(), + context, + )) + } + + /// Execute a GraphQL request using the specified schema and context + /// + /// This is a simple wrapper around the `execute` function exposed at the + /// top level of this crate. + pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>, + context: &'a QueryT::Context, + ) -> GraphQLResponse + where + QueryT: GraphQLTypeAsync, + QueryT::TypeInfo: Sync, + QueryT::Context: Sync, + MutationT: GraphQLTypeAsync, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLType + Sync, + SubscriptionT::TypeInfo: Sync, + S: ScalarValue + Send + Sync, + { + let op = self.operation_name(); + let vars = &self.variables(); + let res = juniper::execute(&self.query, op, root_node, vars, context).await; + GraphQLResponse(res) + } +} + +/// Resolve a GraphQL subscription into `Value` using the +/// specified schema and context. +/// This is a wrapper around the `resolve_into_stream` function exposed at the top +/// level of this crate. +pub async fn resolve_into_stream<'req, 'rn, 'ctx, 'a, QueryT, MutationT, SubscriptionT, S>( + req: &'req GraphQLUploadRequest, + root_node: &'rn RootNode<'a, QueryT, MutationT, SubscriptionT, S>, + context: &'ctx QueryT::Context, +) -> Result<(Value>, Vec>), GraphQLError<'a>> +where + 'req: 'a, + 'rn: 'a, + 'ctx: 'a, + QueryT: GraphQLTypeAsync, + QueryT::TypeInfo: Sync, + QueryT::Context: Sync, + MutationT: GraphQLTypeAsync, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLSubscriptionType, + SubscriptionT::TypeInfo: Sync, + S: ScalarValue + Send + Sync, +{ + let op = req.operation_name(); + let vars = req.variables(); + + juniper::resolve_into_stream(&req.query, op, root_node, &vars, context).await +} + +/// Simple wrapper around the result from executing a GraphQL query +/// +/// This struct implements Serialize, so you can simply serialize this +/// to JSON and send it over the wire. Use the `is_ok` method to determine +/// whether to send a 200 or 400 HTTP status code. + + +/// Simple wrapper around GraphQLUploadRequest to allow the handling of Batch requests. +#[derive(Debug, Deserialize, PartialEq)] +#[serde(untagged)] +#[serde(bound = "InputValue: Deserialize<'de>")] +pub enum GraphQLBatchRequest +where + S: ScalarValue, +{ + /// A single operation request. + Single(GraphQLUploadRequest), + + /// A batch operation request. + /// + /// Empty batch is considered as invalid value, so cannot be deserialized. + #[serde(deserialize_with = "deserialize_non_empty_vec")] + Batch(Vec>), +} + +fn deserialize_non_empty_vec<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, + T: Deserialize<'de>, +{ + use de::Error as _; + + let v = Vec::::deserialize(deserializer)?; + if v.is_empty() { + Err(D::Error::invalid_length(0, &"a positive integer")) + } else { + Ok(v) + } +} + +impl GraphQLBatchRequest +where + S: ScalarValue, +{ + /// Execute a GraphQL batch request synchronously using the specified schema and context + /// + /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLUploadRequest. + pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode, + context: &QueryT::Context, + ) -> GraphQLBatchResponse<'a, S> + where + QueryT: GraphQLType, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + { + match *self { + Self::Single(ref req) => { + GraphQLBatchResponse::Single(req.execute_sync(root_node, context)) + } + Self::Batch(ref reqs) => GraphQLBatchResponse::Batch( + reqs.iter() + .map(|req| req.execute_sync(root_node, context)) + .collect(), + ), + } + } + + /// Executes a GraphQL request using the specified schema and context + /// + /// This is a simple wrapper around the `execute` function exposed in + /// GraphQLUploadRequest + pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>, + context: &'a QueryT::Context, + ) -> GraphQLBatchResponse<'a, S> + where + QueryT: GraphQLTypeAsync, + QueryT::TypeInfo: Sync, + QueryT::Context: Sync, + MutationT: GraphQLTypeAsync, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLSubscriptionType, + SubscriptionT::TypeInfo: Sync, + S: Send + Sync, + { + match self { + Self::Single(req) => { + let resp = req.execute(root_node, context).await; + GraphQLBatchResponse::Single(resp) + } + Self::Batch(reqs) => { + let resps = futures::future::join_all( + reqs.iter().map(|req| req.execute(root_node, context)), + ) + .await; + GraphQLBatchResponse::Batch(resps) + } + } + } + + /// The operation names of the request. + pub fn operation_names(&self) -> Vec> { + match self { + Self::Single(req) => vec![req.operation_name()], + Self::Batch(reqs) => reqs.iter().map(|req| req.operation_name()).collect(), + } + } +} diff --git a/src/graphql/multipart/upload_request.rs b/src/graphql/multipart/upload_request.rs new file mode 100644 index 0000000..49001f0 --- /dev/null +++ b/src/graphql/multipart/upload_request.rs @@ -0,0 +1,334 @@ +/*! + +# juniper_rocket + +This repository contains the [Rocket][Rocket] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. + +## Documentation + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [Api documentation][documentation]. + +## Examples + +Check [examples/rocket_server.rs][example] for example code of a working Rocket +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [Api Reference][documentation] +* [Rocket][Rocket] + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Rocket]: https://rocket.rs +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_rocket +[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs + +*/ + +#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.7.1")] + +use std::{borrow::Cow, io::Cursor, sync::Arc}; + +use juniper_rocket::GraphQLResponse; +use multer::Multipart; +use rocket::{ + data::{self, FromData, ToByteUnit}, + form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField}, + http::{ContentType, Status}, + outcome::Outcome::{Failure, Forward, Success}, + response::{self, content, Responder, Response}, + Data, Request, +}; + +use juniper::{ + http::{self, GraphQLBatchRequest}, + DefaultScalarValue, FieldError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, + InputValue, RootNode, ScalarValue, +}; + +enum FormType{ + IS_JSON, + NOT_JSON, + MULTIPART +} + +/// Simple wrapper around an incoming GraphQL request +/// +/// See the `http` module for more information. This type can be constructed +/// automatically from both GET and POST routes by implementing the `FromForm` +/// and `FromData` traits. +#[derive(Debug, PartialEq)] +pub struct GraphQLUploadRequest(GraphQLBatchRequest) +where + S: ScalarValue; + +/// Simple wrapper around the result of executing a GraphQL query +// pub struct GraphQLResponse(pub Status, pub String); + + +impl GraphQLUploadRequest +where + S: ScalarValue, +{ + /// Synchronously execute an incoming GraphQL query. + pub fn execute_sync( + &self, + root_node: &RootNode, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLType, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + { + let response = self.0.execute_sync(root_node, context); + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string(&response).unwrap(); + + GraphQLResponse(status, json) + } + + /// Asynchronously execute an incoming GraphQL query. + pub async fn execute( + &self, + root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLTypeAsync, + QueryT::TypeInfo: Sync, + MutationT: GraphQLTypeAsync, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLSubscriptionType, + SubscriptionT::TypeInfo: Sync, + CtxT: Sync, + S: Send + Sync, + { + let response = self.0.execute(root_node, context).await; + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string(&response).unwrap(); + + GraphQLResponse(status, json) + } + + /// Returns the operation names associated with this request. + /// + /// For batch requests there will be multiple names. + pub fn operation_names(&self) -> Vec> { + self.0.operation_names() + } +} + +pub struct GraphQLContext<'f, S: ScalarValue> { + opts: Options, + query: Option, + operation_name: Option, + variables: Option>, + errors: Errors<'f>, +} + +impl<'f, S: ScalarValue> GraphQLContext<'f, S> { + fn query(&mut self, value: String) { + if self.query.is_some() { + let error = Error::from(ErrorKind::Duplicate).with_name("query"); + + self.errors.push(error) + } else { + self.query = Some(value); + } + } + + fn operation_name(&mut self, value: String) { + if self.operation_name.is_some() { + let error = Error::from(ErrorKind::Duplicate).with_name("operation_name"); + + self.errors.push(error) + } else { + self.operation_name = Some(value); + } + } + + fn variables(&mut self, value: String) { + if self.variables.is_some() { + let error = Error::from(ErrorKind::Duplicate).with_name("variables"); + + self.errors.push(error) + } else { + let parse_result = serde_json::from_str::>(&value); + + match parse_result { + Ok(variables) => self.variables = Some(variables), + Err(e) => { + let error = Error::from(ErrorKind::Validation(Cow::Owned(e.to_string()))) + .with_name("variables"); + + self.errors.push(error); + } + } + } + } +} + +#[rocket::async_trait] +impl<'f, S> FromForm<'f> for GraphQLUploadRequest +where + S: ScalarValue + Send, +{ + type Context = GraphQLContext<'f, S>; + + fn init(opts: Options) -> Self::Context { + GraphQLContext { + opts, + query: None, + operation_name: None, + variables: None, + errors: Errors::new(), + } + } + + fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) { + match field.name.key().map(|key| key.as_str()) { + Some("query") => ctx.query(field.value.to_owned()), + Some("operation_name") => ctx.operation_name(field.value.to_owned()), + Some("variables") => ctx.variables(field.value.to_owned()), + Some(key) => { + if ctx.opts.strict { + let error = Error::from(ErrorKind::Unknown).with_name(key); + + ctx.errors.push(error) + } + } + None => { + if ctx.opts.strict { + let error = Error::from(ErrorKind::Unexpected); + + ctx.errors.push(error) + } + } + } + } + + async fn push_data(ctx: &mut Self::Context, field: DataField<'f, '_>) { + if ctx.opts.strict { + let error = Error::from(ErrorKind::Unexpected).with_name(field.name); + + ctx.errors.push(error) + } + } + + fn finalize(mut ctx: Self::Context) -> rocket::form::Result<'f, Self> { + if ctx.query.is_none() { + let error = Error::from(ErrorKind::Missing).with_name("query"); + + ctx.errors.push(error) + } + + match ctx.errors.is_empty() { + true => Ok(GraphQLUploadRequest(GraphQLBatchRequest::Single( + http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables), + ))), + false => Err(ctx.errors), + } + } +} + +const BODY_LIMIT: u64 = 1024 * 100; + + + +#[rocket::async_trait] +impl<'r, S> FromData<'r> for GraphQLUploadRequest +where + S: ScalarValue, +{ + type Error = String; + + async fn from_data( + req: &'r Request<'_>, + data: Data<'r>, + ) -> data::Outcome<'r, Self, Self::Error> { + use rocket::tokio::io::AsyncReadExt as _; + + let content_type = req.content_type().unwrap(); + // let content_type_value = content_type + // .map(|ct| (ct.top().as_str(), ct.sub().as_str())); + + let content_type_value = (content_type.top().as_str(),content_type.sub().as_str()); + let content_type_enum_value = match content_type_value { + ("application", "json") => FormType::IS_JSON, + ("application", "graphql") => FormType::NOT_JSON, + ("multipart","form-data") => FormType::MULTIPART, + _ => return Box::pin(async move { Forward(data) }).await, + }; + + + + + Box::pin(async move { + + Success(GraphQLUploadRequest({ + + match content_type_enum_value { + FormType::IS_JSON => { + let mut body = String::new(); + let mut reader = data.open(BODY_LIMIT.bytes()); + + match serde_json::from_str(&body) { + Ok(req) => req, + Err(e) => return Failure((Status::BadRequest, format!("{}", e))), + } + }, + FormType::NOT_JSON => { + let mut body = String::new(); + let mut reader = data.open(BODY_LIMIT.bytes()); + + GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) + }, + FormType::MULTIPART => { + let (_, boundary) = match content_type.params().find(|&(k, _)| k == "boundary") { + Some(s) => s, + None => return Failure((Status::InternalServerError, format!("An error happened"))), + }; + + let mut body = String::new(); + let mut reader = data.open(BODY_LIMIT.bytes()); + let stream = tokio_util::io::ReaderStream::new(reader); + let mut multipart = Multipart::new(stream, boundary); + + + while let Some(mut entry) = multipart.next_field().await.unwrap() { + let field_name = match entry.name() { + Some(name) => Arc::<&str>::from(name), + None => continue, + }; + } + + GraphQLBatchRequest::Single(http::GraphQLRequest::new("{{}}".to_string(), None, None)) + } + } + + })) + }) + .await + + } +} diff --git a/src/main.rs b/src/main.rs index 88ef8e8..8559ae1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ extern crate diesel; extern crate diesel_derive_enum; extern crate dotenv; extern crate tonic; - +extern crate tokio_util; mod app; mod catchers; mod cors; @@ -33,7 +33,7 @@ use crate::{ use crate::app::{ get_graphql_handler, graphiql, options_handler, payable_post_graphql_handler, - post_graphql_handler, + post_graphql_handler, upload }; use crate::db::PostgresConn; @@ -64,7 +64,8 @@ async fn main() { graphiql, get_graphql_handler, post_graphql_handler, - payable_post_graphql_handler + payable_post_graphql_handler, + upload ], ) .attach(Cors) From 7f3599ba2d4e98a695c4de72e8b5d965b07a2726 Mon Sep 17 00:00:00 2001 From: Asone Date: Sat, 12 Mar 2022 19:34:35 +0100 Subject: [PATCH 02/12] feature: wip --- src/graphql/multipart/upload_request.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/graphql/multipart/upload_request.rs b/src/graphql/multipart/upload_request.rs index 49001f0..797707d 100644 --- a/src/graphql/multipart/upload_request.rs +++ b/src/graphql/multipart/upload_request.rs @@ -253,8 +253,6 @@ where const BODY_LIMIT: u64 = 1024 * 100; - - #[rocket::async_trait] impl<'r, S> FromData<'r> for GraphQLUploadRequest where @@ -315,11 +313,16 @@ where let mut multipart = Multipart::new(stream, boundary); - while let Some(mut entry) = multipart.next_field().await.unwrap() { - let field_name = match entry.name() { - Some(name) => Arc::<&str>::from(name), - None => continue, - }; + 'outer: while let Some(mut entry) = multipart.next_field().await.unwrap() { + let field_name = match entry.name() { + Some(name) => { + let m = Arc::<&str>::from(name); + m + }, + None => continue, + }; + + if(field_name = "operations") {} } GraphQLBatchRequest::Single(http::GraphQLRequest::new("{{}}".to_string(), None, None)) From 5c9d43d79a20c95df912428bf3e65ca7c37d3db9 Mon Sep 17 00:00:00 2001 From: Asone Date: Thu, 17 Mar 2022 09:01:55 +0100 Subject: [PATCH 03/12] update: draft a working version that parses the operations field on multipart/form-data request --- src/app.rs | 88 +----------- src/graphql/multipart/upload_request.rs | 179 +++++++----------------- 2 files changed, 48 insertions(+), 219 deletions(-) diff --git a/src/app.rs b/src/app.rs index ed0fb20..b2b9973 100644 --- a/src/app.rs +++ b/src/app.rs @@ -84,90 +84,4 @@ pub async fn upload<'r>( request.execute(&*schema, &GQLContext { pool: db, lnd: lnd }) .await -} -// #[derive(Debug, FromForm)] -// pub struct UploadedFile { -// operations: String, -// map: String -// } - -// Uploads a file to the server. -// #[rocket::post("/upload", data = "")] -// pub async fn upload<'r>(content_type: &ContentType, data: Data<'_>) -> &'static str { - // MultipartFormData expects us to provide a mapping of the multipart form data - // in order to be able to parse the boundaries from the request body. - - // As we do not know how many files are uploaded initially in the request nor their names - // what we'll do here is to to first parse the Data aknowledging the mapping object for files - // and parse the request content a second time once the mapping applied to the Content Form Data. - -// let mut options = MultipartFormDataOptions::new(); -// // println!("{}",data.as_str()); -// options.allowed_fields.push(MultipartFormDataField::text("map")); -// options.allowed_fields.push(MultipartFormDataField::text("operations")); - -// let multipart_form_data = MultipartFormData::parse(content_type, data, options).await; - - -// match multipart_form_data { -// Ok(mfp) => { - -// let mappings = mfp.texts.get("map").unwrap(); -// // let files = mfp.files.get("file").unwrap(); // .get(&"file".to_string()); - -// // for (i,f) in mapping.iter().enumerate() { - -// // println!("index key : {}; data: {}",i,&f.text); - - -// // for item in &v { -// // println!("{:?}\n", item); -// // } -// // } -// for mapping in mappings { -// let d = mapping.text.as_str(); -// println!("{}",d); -// let hmap: HashMap<&str, Value> = serde_json::from_str(d).unwrap(); -// let mut options2 = MultipartFormDataOptions::new(); -// // Gets the files mapping from request -// for (k,v) in hmap.iter() { -// options2.allowed_fields.push(MultipartFormDataField::file(k)); -// } -// // // println!("{}",data); -// // let multipart_form_data_2 = MultipartFormData::parse(content_type, data, options2).await; -// // match multipart_form_data_2 { -// // Ok(mfp2) => { -// // let t = mfp2; -// // }, -// // Err(_) => {} -// // } -// } - -// // for f in files { -// // let t = f.content_type.as_ref().unwrap(); -// // println!("{}",t); -// // } -// // if let Some(file) = file { -// // match file { -// // FileField::Single(f) => { -// // let _content_type = &file.content_type; -// // let _file_name = &file.file_name; -// // let _path = &file.path; -// // } -// // } -// // } -// }, -// Err(_) => {} -// }; -// // // println!("{:?}", w); -// // let byt = file.open(10.megabytes()); -// // let o = byt.into_bytes().await.unwrap().to_vec(); -// // // println!("{:?}", o); -// // let we = String::from_utf8(o.clone()).unwrap(); -// // println!("utf8-> {}", we); -// // let hasher = Sha256::new().chain(&o); -// // let result = hasher.finalize(); -// // println!("hash sha256: {:x}", &result); -// // Ok(()) -// "ok" -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/graphql/multipart/upload_request.rs b/src/graphql/multipart/upload_request.rs index 797707d..98b1224 100644 --- a/src/graphql/multipart/upload_request.rs +++ b/src/graphql/multipart/upload_request.rs @@ -38,9 +38,9 @@ Check the LICENSE file for details. #![doc(html_root_url = "https://docs.rs/juniper_rocket/0.7.1")] -use std::{borrow::Cow, io::Cursor, sync::Arc}; +use std::{borrow::Cow, io::Cursor, sync::Arc, collections::HashMap}; -use juniper_rocket::GraphQLResponse; +use juniper_rocket::{GraphQLResponse, GraphQLContext}; use multer::Multipart; use rocket::{ data::{self, FromData, ToByteUnit}, @@ -59,7 +59,7 @@ use juniper::{ enum FormType{ IS_JSON, - NOT_JSON, + GRAPHQL, MULTIPART } @@ -69,7 +69,7 @@ enum FormType{ /// automatically from both GET and POST routes by implementing the `FromForm` /// and `FromData` traits. #[derive(Debug, PartialEq)] -pub struct GraphQLUploadRequest(GraphQLBatchRequest) +pub struct GraphQLUploadRequest(GraphQLBatchRequest,Option>) where S: ScalarValue; @@ -138,119 +138,6 @@ where } } -pub struct GraphQLContext<'f, S: ScalarValue> { - opts: Options, - query: Option, - operation_name: Option, - variables: Option>, - errors: Errors<'f>, -} - -impl<'f, S: ScalarValue> GraphQLContext<'f, S> { - fn query(&mut self, value: String) { - if self.query.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("query"); - - self.errors.push(error) - } else { - self.query = Some(value); - } - } - - fn operation_name(&mut self, value: String) { - if self.operation_name.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("operation_name"); - - self.errors.push(error) - } else { - self.operation_name = Some(value); - } - } - - fn variables(&mut self, value: String) { - if self.variables.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("variables"); - - self.errors.push(error) - } else { - let parse_result = serde_json::from_str::>(&value); - - match parse_result { - Ok(variables) => self.variables = Some(variables), - Err(e) => { - let error = Error::from(ErrorKind::Validation(Cow::Owned(e.to_string()))) - .with_name("variables"); - - self.errors.push(error); - } - } - } - } -} - -#[rocket::async_trait] -impl<'f, S> FromForm<'f> for GraphQLUploadRequest -where - S: ScalarValue + Send, -{ - type Context = GraphQLContext<'f, S>; - - fn init(opts: Options) -> Self::Context { - GraphQLContext { - opts, - query: None, - operation_name: None, - variables: None, - errors: Errors::new(), - } - } - - fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) { - match field.name.key().map(|key| key.as_str()) { - Some("query") => ctx.query(field.value.to_owned()), - Some("operation_name") => ctx.operation_name(field.value.to_owned()), - Some("variables") => ctx.variables(field.value.to_owned()), - Some(key) => { - if ctx.opts.strict { - let error = Error::from(ErrorKind::Unknown).with_name(key); - - ctx.errors.push(error) - } - } - None => { - if ctx.opts.strict { - let error = Error::from(ErrorKind::Unexpected); - - ctx.errors.push(error) - } - } - } - } - - async fn push_data(ctx: &mut Self::Context, field: DataField<'f, '_>) { - if ctx.opts.strict { - let error = Error::from(ErrorKind::Unexpected).with_name(field.name); - - ctx.errors.push(error) - } - } - - fn finalize(mut ctx: Self::Context) -> rocket::form::Result<'f, Self> { - if ctx.query.is_none() { - let error = Error::from(ErrorKind::Missing).with_name("query"); - - ctx.errors.push(error) - } - - match ctx.errors.is_empty() { - true => Ok(GraphQLUploadRequest(GraphQLBatchRequest::Single( - http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables), - ))), - false => Err(ctx.errors), - } - } -} - const BODY_LIMIT: u64 = 1024 * 100; #[rocket::async_trait] @@ -264,29 +151,28 @@ where req: &'r Request<'_>, data: Data<'r>, ) -> data::Outcome<'r, Self, Self::Error> { - use rocket::tokio::io::AsyncReadExt as _; + // Get content-type of HTTP request let content_type = req.content_type().unwrap(); - // let content_type_value = content_type - // .map(|ct| (ct.top().as_str(), ct.sub().as_str())); + // Split content-type value as a tuple of str let content_type_value = (content_type.top().as_str(),content_type.sub().as_str()); + + // Identify the value to aknowledge which kind of parsing action we + // need to provide let content_type_enum_value = match content_type_value { ("application", "json") => FormType::IS_JSON, - ("application", "graphql") => FormType::NOT_JSON, + ("application", "graphql") => FormType::GRAPHQL, ("multipart","form-data") => FormType::MULTIPART, _ => return Box::pin(async move { Forward(data) }).await, }; - - - Box::pin(async move { Success(GraphQLUploadRequest({ match content_type_enum_value { - FormType::IS_JSON => { + FormType::IS_JSON => { // Content-type is declared as json let mut body = String::new(); let mut reader = data.open(BODY_LIMIT.bytes()); @@ -295,41 +181,70 @@ where Err(e) => return Failure((Status::BadRequest, format!("{}", e))), } }, - FormType::NOT_JSON => { + FormType::GRAPHQL => { // Content-type is declared as graphQL let mut body = String::new(); let mut reader = data.open(BODY_LIMIT.bytes()); GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) }, - FormType::MULTIPART => { + FormType::MULTIPART => { // Content-type is declared as a multipart request + let mut query: String = "".to_string(); + // Get the boundary attached to the multipart form let (_, boundary) = match content_type.params().find(|&(k, _)| k == "boundary") { Some(s) => s, None => return Failure((Status::InternalServerError, format!("An error happened"))), }; + // Reads the datastream of request and converts it to a multipart object let mut body = String::new(); let mut reader = data.open(BODY_LIMIT.bytes()); let stream = tokio_util::io::ReaderStream::new(reader); let mut multipart = Multipart::new(stream, boundary); - - 'outer: while let Some(mut entry) = multipart.next_field().await.unwrap() { + // Iterate through the different fields of the data-form + while let Some(entry) = multipart.next_field().await.unwrap() { let field_name = match entry.name() { Some(name) => { + println!("{}",name); let m = Arc::<&str>::from(name); m }, None => continue, }; - if(field_name = "operations") {} + let name = field_name.to_string(); + // Check if there is some mimetype which should exist when a field is a binary file + if let Some(content_type) = entry.content_type().as_ref() { + let top = content_type.type_(); + let sub = content_type.subtype(); + + let content = entry.bytes().await; + } else { // No mimetype so we expect this to not be a file but rather a json content + let r = entry.text().await; + + match r { + Ok(result) => { + // println!("{}",field_name); + if name.as_str() == "operations" { + query = result; + } + }, + Err(_) => {} + } + // println!("field : {}, with no MimeType",field_name); + } + } + + match serde_json::from_str(&query) { + Ok(req) => req, + Err(e) => return Failure((Status::BadRequest, format!("{}", e))), } - GraphQLBatchRequest::Single(http::GraphQLRequest::new("{{}}".to_string(), None, None)) + // GraphQLBatchRequest::Single(http::GraphQLRequest::new(query, None, None)) } } - })) + },None)) }) .await From fee17f3ab7b8eddab912f04458873d29692fe1fa Mon Sep 17 00:00:00 2001 From: Asone Date: Sat, 19 Mar 2022 23:12:35 +0100 Subject: [PATCH 04/12] update: fully functional multipart/form-data parser --- src/app.rs | 58 +++- src/graphql/context.rs | 1 + src/graphql/mod.rs | 2 +- src/graphql/multipart/mod.rs | 2 +- src/graphql/multipart/upload_request.rs | 374 ++++++++++++------------ src/main.rs | 4 +- 6 files changed, 242 insertions(+), 199 deletions(-) diff --git a/src/app.rs b/src/app.rs index b2b9973..6f1befe 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,16 +4,19 @@ use crate::{ graphql::{context::GQLContext, mutation::Mutation, query::Query}, lnd::client::LndClient, }; -use lightning_invoice::Sha256; -use rocket::{response::content, State, Data, data::ToByteUnit, form::Form, fs::TempFile, http::ContentType}; -use rocket_multipart_form_data::{MultipartFormDataOptions, MultipartFormData, MultipartFormDataField, FileField}; +use rocket::{ + data::ToByteUnit, form::Form, fs::TempFile, http::ContentType, response::content, Data, State, +}; +use rocket_multipart_form_data::{ + FileField, MultipartFormData, MultipartFormDataField, MultipartFormDataOptions, +}; pub type Schema = RootNode<'static, Query, Mutation, EmptySubscription>; use crate::db::PostgresConn; use crate::requests::header::PaymentRequestHeader; use juniper::{EmptySubscription, RootNode}; use juniper_rocket::GraphQLResponse; -use serde_json::{Value}; +use serde_json::Value; #[rocket::get("/")] pub fn graphiql() -> content::Html { @@ -40,7 +43,14 @@ pub async fn get_graphql_handler( lnd: LndClient, ) -> GraphQLResponse { request - .execute(&*schema, &GQLContext { pool: db, lnd: lnd }) + .execute( + &*schema, + &GQLContext { + pool: db, + lnd: lnd, + files: None, + }, + ) .await } @@ -55,7 +65,14 @@ pub async fn post_graphql_handler( lnd: LndClient, ) -> GraphQLResponse { request - .execute(&*schema, &GQLContext { pool: db, lnd: lnd }) + .execute( + &*schema, + &GQLContext { + pool: db, + lnd: lnd, + files: None, + }, + ) .await } @@ -71,17 +88,34 @@ pub async fn payable_post_graphql_handler( _payment_request: PaymentRequestHeader, ) -> GraphQLResponse { request - .execute(&*schema, &GQLContext { pool: db, lnd: lnd }) + .execute( + &*schema, + &GQLContext { + pool: db, + lnd: lnd, + files: None, + }, + ) .await } + #[rocket::post("/upload", data = "")] pub async fn upload<'r>( - request: crate::graphql::multipart::upload_request::GraphQLUploadRequest, + request: crate::graphql::multipart::upload_request::GraphQLUploadDraftedRequest, schema: &State, db: PostgresConn, - lnd: LndClient) -> GraphQLResponse { - + lnd: LndClient, +) -> GraphQLResponse { + let files = request.files.clone(); - request.execute(&*schema, &GQLContext { pool: db, lnd: lnd }) + request + .execute( + &*schema, + &GQLContext { + pool: db, + lnd: lnd, + files: files, + }, + ) .await -} \ No newline at end of file +} diff --git a/src/graphql/context.rs b/src/graphql/context.rs index b94cc18..38b930c 100644 --- a/src/graphql/context.rs +++ b/src/graphql/context.rs @@ -17,6 +17,7 @@ pub struct GQLContext { #[deref] pub pool: PostgresConn, pub lnd: LndClient, + pub files: Option>, } impl juniper::Context for GQLContext {} diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index 4a2d7b3..cccdfbc 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,6 +1,6 @@ pub mod context; +pub mod multipart; pub mod mutation; pub mod paywall_context; pub mod query; pub mod types; -pub mod multipart; \ No newline at end of file diff --git a/src/graphql/multipart/mod.rs b/src/graphql/multipart/mod.rs index 9292175..dedde36 100644 --- a/src/graphql/multipart/mod.rs +++ b/src/graphql/multipart/mod.rs @@ -1 +1 @@ -pub mod upload_request; \ No newline at end of file +pub mod upload_request; diff --git a/src/graphql/multipart/upload_request.rs b/src/graphql/multipart/upload_request.rs index 98b1224..35ed52a 100644 --- a/src/graphql/multipart/upload_request.rs +++ b/src/graphql/multipart/upload_request.rs @@ -1,106 +1,175 @@ -/*! - -# juniper_rocket - -This repository contains the [Rocket][Rocket] web server integration for -[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. - -## Documentation - -For documentation, including guides and examples, check out [Juniper][Juniper]. - -A basic usage example can also be found in the [Api documentation][documentation]. - -## Examples - -Check [examples/rocket_server.rs][example] for example code of a working Rocket -server with GraphQL handlers. - -## Links - -* [Juniper][Juniper] -* [Api Reference][documentation] -* [Rocket][Rocket] - -## License - -This project is under the BSD-2 license. - -Check the LICENSE file for details. - -[Rocket]: https://rocket.rs -[Juniper]: https://github.com/graphql-rust/juniper -[GraphQL]: http://graphql.org -[documentation]: https://docs.rs/juniper_rocket -[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs - +/* +# GraphQL Upload Request handler for rocket_juniper +# */ -#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.7.1")] - -use std::{borrow::Cow, io::Cursor, sync::Arc, collections::HashMap}; - -use juniper_rocket::{GraphQLResponse, GraphQLContext}; +use juniper::GraphQLTypeAsync; +use juniper_rocket::{GraphQLResponse}; use multer::Multipart; use rocket::{ data::{self, FromData, ToByteUnit}, - form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField}, + form::{Error}, http::{ContentType, Status}, outcome::Outcome::{Failure, Forward, Success}, - response::{self, content, Responder, Response}, Data, Request, }; +use std::fs::File; +use std::io::prelude::*; +use std::{sync::Arc}; use juniper::{ http::{self, GraphQLBatchRequest}, - DefaultScalarValue, FieldError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, - InputValue, RootNode, ScalarValue, + DefaultScalarValue, GraphQLSubscriptionType, + RootNode, ScalarValue, }; +use serde::Deserialize; -enum FormType{ - IS_JSON, - GRAPHQL, - MULTIPART +#[derive(Debug)] +pub enum MultipartFormParsingError { + BoundaryParsingError } -/// Simple wrapper around an incoming GraphQL request -/// -/// See the `http` module for more information. This type can be constructed -/// automatically from both GET and POST routes by implementing the `FromForm` -/// and `FromData` traits. -#[derive(Debug, PartialEq)] -pub struct GraphQLUploadRequest(GraphQLBatchRequest,Option>) -where - S: ScalarValue; +enum ProcessorType { + JSON, + GRAPHQL, + MULTIPART, + UKNOWN, +} -/// Simple wrapper around the result of executing a GraphQL query -// pub struct GraphQLResponse(pub Status, pub String); +// This shall be deferable to env +const BODY_LIMIT: u64 = 1024 * 100; +#[derive(Debug, PartialEq, Deserialize)] +pub struct GraphQLUploadDraftedRequest +where + S: ScalarValue, +{ + pub gql_request: GraphQLBatchRequest, + pub files: Option>, +} -impl GraphQLUploadRequest +impl GraphQLUploadDraftedRequest where S: ScalarValue, { - /// Synchronously execute an incoming GraphQL query. - pub fn execute_sync( - &self, - root_node: &RootNode, - context: &CtxT, - ) -> GraphQLResponse - where - QueryT: GraphQLType, - MutationT: GraphQLType, - SubscriptionT: GraphQLType, - { - let response = self.0.execute_sync(root_node, context); - let status = if response.is_ok() { - Status::Ok - } else { - Status::BadRequest - }; - let json = serde_json::to_string(&response).unwrap(); + /** + Body reader for application/json content type. + This method replicates the original handler from juniper_rocket + */ + fn from_json_body<'r>(data: Data<'r>) -> Result<(GraphQLBatchRequest,Option>), serde_json::Error> { + let body = String::new(); + let mut _reader = data.open(BODY_LIMIT.bytes()); + + match serde_json::from_str(&body) { + Ok(req) => Ok(req), + Err(e) => Err(e), + } + } - GraphQLResponse(status, json) + /** + Body reader for application/graphql content type. + This method replicates the original handler from juniper_rocket + */ + fn from_graphql_body<'r>(data: Data<'r>) -> Result, Error> { + let body = String::new(); + let mut _reader = data.open(BODY_LIMIT.bytes()); + + Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + body, None, None, + ))) + } + + /** + Body reader for multipart/form-data content type. + */ + async fn from_multipart_body<'r>( + data: Data<'r>, + content_type: &ContentType, + ) -> Result<(GraphQLBatchRequest,Option>), Error<'r>> { + // Builds a void query for development + let mut query: String = String::new(); + let boundary = Self::get_boundary(content_type).unwrap(); + + // Create and read a datastream from the request body + let reader = data.open(BODY_LIMIT.bytes()); + let stream = tokio_util::io::ReaderStream::new(reader); + + // Create a multipart object based on multer + let mut multipart = Multipart::new(stream, boundary); + let mut files = Vec::::new(); + + // Iterate on the form fields, which can be + // either text content or binary. + while let Some(entry) = multipart.next_field().await.unwrap() { + let field_name = match entry.name() { + Some(name) => Arc::<&str>::from(name), + None => continue, + }; + + let name = field_name.to_string(); + + // Check if there is some mimetype which should exist when a field is a binary file + if let Some(_mime_type) = entry.content_type().as_ref() { + + let file_name = match entry.file_name() { + Some(filename) => filename, + None => continue, + }; + + let path = format!("./tmp/{}", file_name); + let content = entry.bytes().await.unwrap(); + + let mut file = File::create(&path).unwrap(); + file.write_all(&content); + files.push(path); + + } else { + // No mimetype so we expect this to not be a file but rather a json content + let r = entry.text().await; + + // If field name is operations which should be the graphQL Query + // according to spec + match r { + Ok(result) => { + if name.as_str() == "operations" { + query = result; + } + } + Err(_) => {} + }; + } + } + + // Default parser + match serde_json::from_str(&query) { + Ok(req) => Ok((req,Some(files))), + Err(_) => Err(rocket::form::Error::validation("The provided request could not be parsed.")), + } + } + + /** + Returns an enum value for a specific processors based on request Content-type + */ + fn get_processor_type(content_type: &ContentType) -> ProcessorType { + let top = content_type.top().as_str(); + let sub = content_type.sub().as_str(); + + match (top, sub) { + ("application", "json") => ProcessorType::JSON, + ("application", "graphql") => ProcessorType::GRAPHQL, + ("multipart", "form-data") => ProcessorType::MULTIPART, + _ => ProcessorType::UKNOWN, + } + } + + /** + Extracts the boundary for a multipart/form-data request + */ + pub fn get_boundary(content_type: &ContentType) -> Result<&str, MultipartFormParsingError> { + match content_type.params().find(|&(k, _)| k == "boundary") { + Some(s) => Ok(s.1), + None => Err(MultipartFormParsingError::BoundaryParsingError), + } } /// Asynchronously execute an incoming GraphQL query. @@ -119,7 +188,7 @@ where CtxT: Sync, S: Send + Sync, { - let response = self.0.execute(root_node, context).await; + let response = self.gql_request.execute(root_node, context).await; let status = if response.is_ok() { Status::Ok } else { @@ -129,19 +198,10 @@ where GraphQLResponse(status, json) } - - /// Returns the operation names associated with this request. - /// - /// For batch requests there will be multiple names. - pub fn operation_names(&self) -> Vec> { - self.0.operation_names() - } } -const BODY_LIMIT: u64 = 1024 * 100; - #[rocket::async_trait] -impl<'r, S> FromData<'r> for GraphQLUploadRequest +impl<'r, S> FromData<'r> for GraphQLUploadDraftedRequest where S: ScalarValue, { @@ -151,102 +211,50 @@ where req: &'r Request<'_>, data: Data<'r>, ) -> data::Outcome<'r, Self, Self::Error> { - // Get content-type of HTTP request let content_type = req.content_type().unwrap(); - - // Split content-type value as a tuple of str - let content_type_value = (content_type.top().as_str(),content_type.sub().as_str()); - - // Identify the value to aknowledge which kind of parsing action we - // need to provide - let content_type_enum_value = match content_type_value { - ("application", "json") => FormType::IS_JSON, - ("application", "graphql") => FormType::GRAPHQL, - ("multipart","form-data") => FormType::MULTIPART, - _ => return Box::pin(async move { Forward(data) }).await, - }; - - Box::pin(async move { - - Success(GraphQLUploadRequest({ - - match content_type_enum_value { - FormType::IS_JSON => { // Content-type is declared as json - let mut body = String::new(); - let mut reader = data.open(BODY_LIMIT.bytes()); - - match serde_json::from_str(&body) { - Ok(req) => req, - Err(e) => return Failure((Status::BadRequest, format!("{}", e))), - } - }, - FormType::GRAPHQL => { // Content-type is declared as graphQL - let mut body = String::new(); - let mut reader = data.open(BODY_LIMIT.bytes()); - - GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) - }, - FormType::MULTIPART => { // Content-type is declared as a multipart request - let mut query: String = "".to_string(); - // Get the boundary attached to the multipart form - let (_, boundary) = match content_type.params().find(|&(k, _)| k == "boundary") { - Some(s) => s, - None => return Failure((Status::InternalServerError, format!("An error happened"))), - }; - - // Reads the datastream of request and converts it to a multipart object - let mut body = String::new(); - let mut reader = data.open(BODY_LIMIT.bytes()); - let stream = tokio_util::io::ReaderStream::new(reader); - let mut multipart = Multipart::new(stream, boundary); - - // Iterate through the different fields of the data-form - while let Some(entry) = multipart.next_field().await.unwrap() { - let field_name = match entry.name() { - Some(name) => { - println!("{}",name); - let m = Arc::<&str>::from(name); - m - }, - None => continue, - }; - - let name = field_name.to_string(); - // Check if there is some mimetype which should exist when a field is a binary file - if let Some(content_type) = entry.content_type().as_ref() { - let top = content_type.type_(); - let sub = content_type.subtype(); - - let content = entry.bytes().await; - } else { // No mimetype so we expect this to not be a file but rather a json content - let r = entry.text().await; - - match r { - Ok(result) => { - // println!("{}",field_name); - if name.as_str() == "operations" { - query = result; - } - }, - Err(_) => {} - } - // println!("field : {}, with no MimeType",field_name); - } - } - match serde_json::from_str(&query) { - Ok(req) => req, - Err(e) => return Failure((Status::BadRequest, format!("{}", e))), - } + // Split content-type value as a tuple of str - // GraphQLBatchRequest::Single(http::GraphQLRequest::new(query, None, None)) + match Self::get_processor_type(content_type) { + ProcessorType::JSON => { + Box::pin(async move { + match Self::from_json_body(data) { + Ok(result) => Success(GraphQLUploadDraftedRequest { + gql_request: result.0, + files: result.1, + }), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), + } + // Success(Self {}) + }) + .await + } + ProcessorType::GRAPHQL => { + Box::pin(async move { + match Self::from_graphql_body(data) { + Ok(result) => Success(GraphQLUploadDraftedRequest { + gql_request: result, + files: None, + }), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), + } + }) + .await + } + ProcessorType::MULTIPART => { + Box::pin(async move { + match Self::from_multipart_body(data, content_type).await { + Ok(result) => Success(GraphQLUploadDraftedRequest { + gql_request: result.0, + files: result.1 + }), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), } - } - - },None)) - }) - .await - + }) + .await + } + ProcessorType::UKNOWN => Box::pin(async move { Forward(data) }).await, + } } } diff --git a/src/main.rs b/src/main.rs index 8559ae1..27642a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,8 @@ extern crate diesel; extern crate diesel_derive_enum; extern crate dotenv; -extern crate tonic; extern crate tokio_util; +extern crate tonic; mod app; mod catchers; mod cors; @@ -33,7 +33,7 @@ use crate::{ use crate::app::{ get_graphql_handler, graphiql, options_handler, payable_post_graphql_handler, - post_graphql_handler, upload + post_graphql_handler, upload, }; use crate::db::PostgresConn; From fa16a81886a527e66ac37a62cdb93688a9caa85c Mon Sep 17 00:00:00 2001 From: Asone Date: Sat, 19 Mar 2022 23:21:38 +0100 Subject: [PATCH 05/12] fix: missing renaming for GraphQLUploadRequest --- src/app.rs | 2 +- src/graphql/multipart/upload_request.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6f1befe..f0702aa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -101,7 +101,7 @@ pub async fn payable_post_graphql_handler( #[rocket::post("/upload", data = "")] pub async fn upload<'r>( - request: crate::graphql::multipart::upload_request::GraphQLUploadDraftedRequest, + request: crate::graphql::multipart::upload_request::GraphQLUploadRequest, schema: &State, db: PostgresConn, lnd: LndClient, diff --git a/src/graphql/multipart/upload_request.rs b/src/graphql/multipart/upload_request.rs index 35ed52a..1ccf791 100644 --- a/src/graphql/multipart/upload_request.rs +++ b/src/graphql/multipart/upload_request.rs @@ -40,7 +40,7 @@ enum ProcessorType { const BODY_LIMIT: u64 = 1024 * 100; #[derive(Debug, PartialEq, Deserialize)] -pub struct GraphQLUploadDraftedRequest +pub struct GraphQLUploadRequest where S: ScalarValue, { @@ -48,7 +48,7 @@ where pub files: Option>, } -impl GraphQLUploadDraftedRequest +impl GraphQLUploadRequest where S: ScalarValue, { @@ -201,7 +201,7 @@ where } #[rocket::async_trait] -impl<'r, S> FromData<'r> for GraphQLUploadDraftedRequest +impl<'r, S> FromData<'r> for GraphQLUploadRequest where S: ScalarValue, { @@ -220,7 +220,7 @@ where ProcessorType::JSON => { Box::pin(async move { match Self::from_json_body(data) { - Ok(result) => Success(GraphQLUploadDraftedRequest { + Ok(result) => Success(GraphQLUploadRequest { gql_request: result.0, files: result.1, }), @@ -233,7 +233,7 @@ where ProcessorType::GRAPHQL => { Box::pin(async move { match Self::from_graphql_body(data) { - Ok(result) => Success(GraphQLUploadDraftedRequest { + Ok(result) => Success(GraphQLUploadRequest { gql_request: result, files: None, }), @@ -245,7 +245,7 @@ where ProcessorType::MULTIPART => { Box::pin(async move { match Self::from_multipart_body(data, content_type).await { - Ok(result) => Success(GraphQLUploadDraftedRequest { + Ok(result) => Success(GraphQLUploadRequest { gql_request: result.0, files: result.1 }), From 01da87010275b6079adbdfc26423358369417beb Mon Sep 17 00:00:00 2001 From: Asone Date: Mon, 28 Mar 2022 22:36:41 +0200 Subject: [PATCH 06/12] add: wip of juniper file handler for rocket webserver --- juniper_rocket_multipart_handler/Cargo.toml | 20 ++ .../src/graphql_upload_operations_request.rs | 59 ++++ .../src/graphql_upload_wrapper.rs | 258 ++++++++++++++++++ juniper_rocket_multipart_handler/src/lib.rs | 3 + .../src/temp_file.rs | 36 +++ 5 files changed, 376 insertions(+) create mode 100644 juniper_rocket_multipart_handler/Cargo.toml create mode 100644 juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs create mode 100644 juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs create mode 100644 juniper_rocket_multipart_handler/src/lib.rs create mode 100644 juniper_rocket_multipart_handler/src/temp_file.rs diff --git a/juniper_rocket_multipart_handler/Cargo.toml b/juniper_rocket_multipart_handler/Cargo.toml new file mode 100644 index 0000000..9dc25f3 --- /dev/null +++ b/juniper_rocket_multipart_handler/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "juniper_rocket_multipart_handler" +version = "0.0.1" +description = "A juniper multipart handler for Rocket web server" +authors = ["Nelson Herbin "] +edition = "2018" +license = "CC0-1.0" +repository = "https://github.com/Asone/graphQLN" +homepage = "https://github.com/Asone/graphQLN" + +[dependencies] +multer = "2.0.2" +rocket = "0.5.0-rc.1" +juniper = "0.15.7" +serde = "1.0.136" +serde_json = "1.0.68" +juniper_rocket = "0.8.0" + +[dependencies.tokio-util] +version = "0.7.1" \ No newline at end of file diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs b/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs new file mode 100644 index 0000000..4cdea06 --- /dev/null +++ b/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs @@ -0,0 +1,59 @@ +use juniper::{GraphQLTypeAsync}; +use juniper_rocket::{GraphQLResponse}; +use multer::{Multipart, bytes::Bytes, Field}; +use rocket::{ + data::{self, FromData, ToByteUnit}, + form::{Error}, + http::{ContentType, Status}, + outcome::Outcome::{Failure, Forward, Success}, + Data, Request, fs::FileName, Either, +}; +use std::{fs::File, env, num::NonZeroU64, path::PathBuf, collections::HashMap}; +use std::io::prelude::*; +use std::{sync::Arc}; + +use juniper::{ + http::{self, GraphQLBatchRequest}, + DefaultScalarValue, GraphQLSubscriptionType, + RootNode, ScalarValue, +}; +use serde::Deserialize; +#[derive(Debug, PartialEq)] +pub struct GraphQLUploadOperationsRequest +where + S: ScalarValue, +{ + pub gql_request: GraphQLBatchRequest +} + +impl GraphQLUploadOperationsRequest +where + S: ScalarValue, +{ + /// Asynchronously execute an incoming GraphQL query. + pub async fn execute( + &self, + root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLTypeAsync, + QueryT::TypeInfo: Sync, + MutationT: GraphQLTypeAsync, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLSubscriptionType, + SubscriptionT::TypeInfo: Sync, + CtxT: Sync, + S: Send + Sync, + { + let response = self.gql_request.execute(root_node, context).await; + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string(&response).unwrap(); + + GraphQLResponse(status, json) + } +} diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs new file mode 100644 index 0000000..fefa44c --- /dev/null +++ b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs @@ -0,0 +1,258 @@ +use juniper::GraphQLTypeAsync; +use juniper_rocket::{GraphQLResponse}; +use multer::{Multipart, bytes::Bytes, Field}; +use rocket::{ + data::{self, FromData, ToByteUnit}, + form::{Error}, + http::{ContentType, Status}, + outcome::Outcome::{Failure, Forward, Success}, + Data, Request, fs::FileName, Either, +}; +use std::{fs::File, env, num::NonZeroU64, path::PathBuf, collections::HashMap}; +use std::io::prelude::*; +use std::{sync::Arc}; + +use juniper::{ + http::{self, GraphQLBatchRequest}, + DefaultScalarValue, GraphQLSubscriptionType, + RootNode, ScalarValue, +}; +use serde::Deserialize; +use crate::graphql_upload_operations_request::GraphQLUploadOperationsRequest; + +use crate::temp_file::TempFile; + +// This shall be deferable to env +const BODY_LIMIT: u64 = 1024 * 100; + +#[derive(Debug)] +pub enum MultipartFormParsingError { + BoundaryParsingError +} + +enum ProcessorType { + JSON, + GRAPHQL, + MULTIPART, + UKNOWN, +} + + +#[derive(Debug, PartialEq)] +pub struct GraphQLUploadWrapper +where + S: ScalarValue, +{ + pub operations: GraphQLUploadOperationsRequest, + pub files: Option>, +} + +impl GraphQLUploadWrapper +where + S: ScalarValue, +{ + + pub async fn get_files(&self) -> &Option> { + &self.files + } + + /** + Body reader for application/json content type. + This method replicates the original handler from juniper_rocket + */ + fn from_json_body<'r>(data: Data<'r>) -> Result, serde_json::Error> { + let body = String::new(); + let mut _reader = data.open(BODY_LIMIT.bytes()); + + match serde_json::from_str(&body) { + Ok(req) => Ok(req), + Err(e) => Err(e), + } + } + + /** + Body reader for application/graphql content type. + This method replicates the original handler from juniper_rocket + */ + fn from_graphql_body<'r>(data: Data<'r>) -> Result, Error> { + let body = String::new(); + let mut _reader = data.open(BODY_LIMIT.bytes()); + + Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + body, None, None, + ))) + } + + /** + Body reader for multipart/form-data content type. + */ + async fn from_multipart_body<'r>( + data: Data<'r>, + content_type: &ContentType, + ) -> Result<(GraphQLBatchRequest,Option>), Error<'r>> { + // Builds a void query for development + let mut query: String = String::new(); + let boundary = Self::get_boundary(content_type).unwrap(); + + // Create and read a datastream from the request body + let reader = data.open(BODY_LIMIT.bytes()); + let stream = tokio_util::io::ReaderStream::new(reader); + + // Create a multipart object based on multer + let mut multipart = Multipart::new(stream, boundary); + + let mut filesMap: HashMap = HashMap::new(); + + // Iterate on the form fields, which can be + // either text content or binary. + while let Some(entry) = multipart.next_field().await.unwrap() { + let field_name = match entry.name() { + Some(name) => Arc::<&str>::from(name).to_string(), + None => continue, + }; + + let name = field_name; + + + let path = format!("{}",env::temp_dir().display()); + + match entry.content_type().as_ref() { + Some(mimetype) => { + + let file_name = match entry.file_name() { + Some(filename) => Arc::<&str>::from(filename).to_string(), + None => continue, + }; + + let content = match entry.bytes().await { + Ok(d) => d, + Err(e) => { + Bytes::new() + } + }; + + let tmpfile = TempFile { + name: file_name, + size: Some(content.len()), + local_path: PathBuf::from(&path), + content: content + }; + + filesMap.insert(name, tmpfile); + + }, + None => { + let r = entry.text().await; + + // If field name is operations which should be the graphQL Query + // according to spec + match r { + Ok(result) => { + if name.as_str() == "operations" { + query = result; + } + } + Err(_) => {} + }; + } + } + } + + // Default parser + match serde_json::from_str(&query) { + Ok(req) => Ok((req,Some(filesMap))), + Err(_) => Err(rocket::form::Error::validation("The provided request could not be parsed.")), + } + } + + /** + Returns an enum value for a specific processors based on request Content-type + */ + fn get_processor_type(content_type: &ContentType) -> ProcessorType { + let top = content_type.top().as_str(); + let sub = content_type.sub().as_str(); + + match (top, sub) { + ("application", "json") => ProcessorType::JSON, + ("application", "graphql") => ProcessorType::GRAPHQL, + ("multipart", "form-data") => ProcessorType::MULTIPART, + _ => ProcessorType::UKNOWN, + } + } + + /** + Extracts the boundary for a multipart/form-data request + */ + pub fn get_boundary(content_type: &ContentType) -> Result<&str, MultipartFormParsingError> { + match content_type.params().find(|&(k, _)| k == "boundary") { + Some(s) => Ok(s.1), + None => Err(MultipartFormParsingError::BoundaryParsingError), + } + } + +} + +#[rocket::async_trait] +impl<'r, S> FromData<'r> for GraphQLUploadWrapper +where + S: ScalarValue, +{ + type Error = String; + + async fn from_data( + req: &'r Request<'_>, + data: Data<'r>, + ) -> data::Outcome<'r, Self, Self::Error> { + // Get content-type of HTTP request + let content_type = req.content_type().unwrap(); + + // Split content-type value as a tuple of str + + match Self::get_processor_type(content_type) { + ProcessorType::JSON => { + Box::pin(async move { + match Self::from_json_body(data) { + Ok(result) => Success(GraphQLUploadWrapper{ + operations: GraphQLUploadOperationsRequest { + gql_request: result, + }, + files: None + }), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), + } + // Success(Self {}) + }) + .await + } + ProcessorType::GRAPHQL => { + Box::pin(async move { + match Self::from_graphql_body(data) { + Ok(result) => Success(GraphQLUploadWrapper{ + operations: GraphQLUploadOperationsRequest { + gql_request: result, + }, + files: None + }), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), + } + }) + .await + } + ProcessorType::MULTIPART => { + Box::pin(async move { + match Self::from_multipart_body(data, content_type).await { + Ok(result) => Success(GraphQLUploadWrapper{ + operations: GraphQLUploadOperationsRequest { + gql_request: result.0, + }, + files: result.1 + }), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), + } + }) + .await + } + ProcessorType::UKNOWN => Box::pin(async move { Forward(data) }).await, + } + } +} diff --git a/juniper_rocket_multipart_handler/src/lib.rs b/juniper_rocket_multipart_handler/src/lib.rs new file mode 100644 index 0000000..779a86f --- /dev/null +++ b/juniper_rocket_multipart_handler/src/lib.rs @@ -0,0 +1,3 @@ +pub mod graphql_upload_wrapper; +pub mod graphql_upload_operations_request; +pub mod temp_file; \ No newline at end of file diff --git a/juniper_rocket_multipart_handler/src/temp_file.rs b/juniper_rocket_multipart_handler/src/temp_file.rs new file mode 100644 index 0000000..0f9ba2a --- /dev/null +++ b/juniper_rocket_multipart_handler/src/temp_file.rs @@ -0,0 +1,36 @@ +use std::{path::PathBuf, fs::File, io::Write}; + +use multer::bytes::Bytes; + +#[derive(Debug, PartialEq)] +pub struct TempFile{ + pub local_path: PathBuf, + pub name: String, + pub size: Option, + pub content: Bytes +} + +impl TempFile{ + pub fn get_local_path(&self) -> &PathBuf { + &self.local_path + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn get_size(&self) -> &Option { + &self.size + } + + pub fn get_content(&self) -> &Bytes { + &self.content + } + + pub fn persist_file(&self) -> &PathBuf { + let full_path = format!("{}/{}",&self.local_path.to_str().unwrap(),&self.name); + let mut file = File::create(full_path).unwrap(); + file.write_all(&self.content); + &self.local_path + } +} From e0260f6a15b6a214c52e1a07de85e7c5f3e1c902 Mon Sep 17 00:00:00 2001 From: Asone Date: Tue, 29 Mar 2022 05:18:54 +0200 Subject: [PATCH 07/12] update: documentation for upload handler --- .../src/graphql_upload_operations_request.rs | 67 ++++++--- .../src/graphql_upload_wrapper.rs | 138 +++++++++++++----- .../src/temp_file.rs | 31 +++- 3 files changed, 174 insertions(+), 62 deletions(-) diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs b/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs index 4cdea06..67cc6b2 100644 --- a/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs +++ b/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs @@ -1,32 +1,26 @@ -use juniper::{GraphQLTypeAsync}; +use juniper::{GraphQLTypeAsync, GraphQLType}; use juniper_rocket::{GraphQLResponse}; -use multer::{Multipart, bytes::Bytes, Field}; -use rocket::{ - data::{self, FromData, ToByteUnit}, - form::{Error}, - http::{ContentType, Status}, - outcome::Outcome::{Failure, Forward, Success}, - Data, Request, fs::FileName, Either, -}; -use std::{fs::File, env, num::NonZeroU64, path::PathBuf, collections::HashMap}; -use std::io::prelude::*; -use std::{sync::Arc}; - +use rocket::http::Status; use juniper::{ - http::{self, GraphQLBatchRequest}, + http::GraphQLBatchRequest, DefaultScalarValue, GraphQLSubscriptionType, RootNode, ScalarValue, }; -use serde::Deserialize; + +/// A GraphQL operations request. +/// +/// This struct replicates the [`GraphQLRequest`](https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/src/lib.rs#L64) original behavior. +/// It is provided and used with the upload wrapper as the original struct +/// does not provide any constructor and the tuple constructor is private. #[derive(Debug, PartialEq)] -pub struct GraphQLUploadOperationsRequest +pub struct GraphQLOperationsRequest( +pub GraphQLBatchRequest +) where - S: ScalarValue, -{ - pub gql_request: GraphQLBatchRequest -} + S: ScalarValue; -impl GraphQLUploadOperationsRequest + +impl GraphQLOperationsRequest where S: ScalarValue, { @@ -46,7 +40,29 @@ where CtxT: Sync, S: Send + Sync, { - let response = self.gql_request.execute(root_node, context).await; + let response = self.0.execute(root_node, context).await; + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string(&response).unwrap(); + + GraphQLResponse(status, json) + } + + /// Synchronously execute an incoming GraphQL query. + pub fn execute_sync( + &self, + root_node: &RootNode, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLType, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + { + let response = self.0.execute_sync(root_node, context); let status = if response.is_ok() { Status::Ok } else { @@ -56,4 +72,11 @@ where GraphQLResponse(status, json) } + + /// Returns the operation names associated with this request. + /// + /// For batch requests there will be multiple names. + pub fn operation_names(&self) -> Vec> { + self.0.operation_names() + } } diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs index fefa44c..5910c1a 100644 --- a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs +++ b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs @@ -1,24 +1,21 @@ -use juniper::GraphQLTypeAsync; -use juniper_rocket::{GraphQLResponse}; -use multer::{Multipart, bytes::Bytes, Field}; +use multer::{Multipart, bytes::Bytes}; use rocket::{ data::{self, FromData, ToByteUnit}, form::{Error}, http::{ContentType, Status}, outcome::Outcome::{Failure, Forward, Success}, - Data, Request, fs::FileName, Either, + Data, Request }; -use std::{fs::File, env, num::NonZeroU64, path::PathBuf, collections::HashMap}; -use std::io::prelude::*; +use std::{env, path::PathBuf, collections::HashMap}; use std::{sync::Arc}; use juniper::{ http::{self, GraphQLBatchRequest}, - DefaultScalarValue, GraphQLSubscriptionType, - RootNode, ScalarValue, + DefaultScalarValue, + ScalarValue, }; -use serde::Deserialize; -use crate::graphql_upload_operations_request::GraphQLUploadOperationsRequest; + +use crate::graphql_upload_operations_request::GraphQLOperationsRequest; use crate::temp_file::TempFile; @@ -38,12 +35,86 @@ enum ProcessorType { } +/// A Wrapper that handles HTTP GraphQL requests for [`Rocket`](https://rocket.rs/) +/// with multipart/form-data support that follows the Apollo's +/// [`unofficial specification`](https://github.com/jaydenseric/graphql-multipart-request-spec.). +/// +/// # Main concept +/// +/// The current struct is nothing more than a wrapper +/// that will return two fields : +/// - `operations` : Contains the graphQL Request to be executed. +/// This object is nothing more than a replica +/// of the original [`GraphQLRequest`](https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/src/lib.rs#L64) +/// object. +/// - `files` : An optional HashMap that contains the buffered files data. +/// +/// Note that the wrapper also replicates original juniper_rocket [`GraphQLRequest`] +/// parsing behavior for `application/json` and `application/graphql` requests. +/// +/// ## How to use +/// +/// You can load the GraphQLUploadWrapper the same way +/// you load the GraphQLRequest as both are data guards. +/// The main difference will be that instead, you'll call the +/// execution of the query through the `operations` property +/// of the wrapper. +/// +/// Below is basic example : +/// +/// ``` +/// #[rocket::post("/upload", data = "")] +/// pub async fn upload<'r>( +/// request: GraphQLUploadWrapper, +/// schema: &State, +/// ) -> GraphQLResponse { +/// request.operations.execute(&*schema, &Context).await +/// } +/// ``` +/// +/// ## Fetching the uploaded files +/// +/// In order to fetch the uploaded files +/// You'll need to implement your own context object +/// That will pass the buffered files to your execution methods. +/// +/// Example : +/// ``` +/// struct Ctx{ +/// files: Option> +/// }; +/// impl juniper::Context for Ctx {} +/// ``` +/// +/// You'll then be able to inject the buffered files to your +/// operations like this : +/// ``` +/// struct Ctx{ files: Option> }; +/// impl juniper::Context for Ctx {} +/// +/// #[rocket::post("/upload", data = "")] +/// pub async fn upload<'r>( +/// request: GraphQLUploadWrapper, +/// schema: &State, +/// ) -> GraphQLResponse { +/// request.operations.execute(&*schema, &Ctx{ files: request.files }).await +/// } +/// ``` +/// +/// ## Notes about processed files +/// +/// The Wrapper does nothing special with the uploaded files aside +/// allocating them in heap memory through the Hashmap which means +/// they won't be stored anywhere, not even in a temporary folder, +/// unless you decide to. +/// +/// See [`TempFile`] for more available data and information around uploaded files. #[derive(Debug, PartialEq)] pub struct GraphQLUploadWrapper where S: ScalarValue, { - pub operations: GraphQLUploadOperationsRequest, + pub operations: GraphQLOperationsRequest, pub files: Option>, } @@ -83,9 +154,8 @@ where ))) } - /** - Body reader for multipart/form-data content type. - */ + + /// Body reader for multipart/form-data content type. async fn from_multipart_body<'r>( data: Data<'r>, content_type: &ContentType, @@ -101,7 +171,7 @@ where // Create a multipart object based on multer let mut multipart = Multipart::new(stream, boundary); - let mut filesMap: HashMap = HashMap::new(); + let mut files_map: HashMap = HashMap::new(); // Iterate on the form fields, which can be // either text content or binary. @@ -113,11 +183,10 @@ where let name = field_name; - let path = format!("{}",env::temp_dir().display()); match entry.content_type().as_ref() { - Some(mimetype) => { + Some(_) => { let file_name = match entry.file_name() { Some(filename) => Arc::<&str>::from(filename).to_string(), @@ -126,7 +195,7 @@ where let content = match entry.bytes().await { Ok(d) => d, - Err(e) => { + Err(_) => { Bytes::new() } }; @@ -138,21 +207,21 @@ where content: content }; - filesMap.insert(name, tmpfile); + files_map.insert(name, tmpfile); }, None => { - let r = entry.text().await; + let content_data = entry.text().await; // If field name is operations which should be the graphQL Query // according to spec - match r { + match content_data { Ok(result) => { if name.as_str() == "operations" { query = result; } } - Err(_) => {} + Err(_) => continue }; } } @@ -160,14 +229,13 @@ where // Default parser match serde_json::from_str(&query) { - Ok(req) => Ok((req,Some(filesMap))), + Ok(req) => Ok((req,Some(files_map))), Err(_) => Err(rocket::form::Error::validation("The provided request could not be parsed.")), } } - /** - Returns an enum value for a specific processors based on request Content-type - */ + + /// Returns an enum value for a specific processors based on request Content-type fn get_processor_type(content_type: &ContentType) -> ProcessorType { let top = content_type.top().as_str(); let sub = content_type.sub().as_str(); @@ -180,9 +248,8 @@ where } } - /** - Extracts the boundary for a multipart/form-data request - */ + + /// Extracts the boundary for a multipart/form-data request pub fn get_boundary(content_type: &ContentType) -> Result<&str, MultipartFormParsingError> { match content_type.params().find(|&(k, _)| k == "boundary") { Some(s) => Ok(s.1), @@ -213,9 +280,8 @@ where Box::pin(async move { match Self::from_json_body(data) { Ok(result) => Success(GraphQLUploadWrapper{ - operations: GraphQLUploadOperationsRequest { - gql_request: result, - }, + operations: GraphQLOperationsRequest(result) + , files: None }), Err(error) => Failure((Status::BadRequest, format!("{}", error))), @@ -228,9 +294,7 @@ where Box::pin(async move { match Self::from_graphql_body(data) { Ok(result) => Success(GraphQLUploadWrapper{ - operations: GraphQLUploadOperationsRequest { - gql_request: result, - }, + operations: GraphQLOperationsRequest(result), files: None }), Err(error) => Failure((Status::BadRequest, format!("{}", error))), @@ -242,9 +306,7 @@ where Box::pin(async move { match Self::from_multipart_body(data, content_type).await { Ok(result) => Success(GraphQLUploadWrapper{ - operations: GraphQLUploadOperationsRequest { - gql_request: result.0, - }, + operations: GraphQLOperationsRequest(result.0), files: result.1 }), Err(error) => Failure((Status::BadRequest, format!("{}", error))), diff --git a/juniper_rocket_multipart_handler/src/temp_file.rs b/juniper_rocket_multipart_handler/src/temp_file.rs index 0f9ba2a..c9e469b 100644 --- a/juniper_rocket_multipart_handler/src/temp_file.rs +++ b/juniper_rocket_multipart_handler/src/temp_file.rs @@ -2,6 +2,19 @@ use std::{path::PathBuf, fs::File, io::Write}; use multer::bytes::Bytes; +/// A buffered file from file upload. +/// +/// Please, **do note** that this struct +/// is different from original [`TempFile`] provided +/// by original's rocket implementation as it does not +/// intend to rely on a specific lifetime specification +/// other than the graphQL request processing lifetime. +/// +/// Current [`TempFile`] struct provides with a `persist_file` +/// method that will write the file on the filesystem based on the +/// `local_path` of the struct. +/// +/// default `local_path` provided is based on [`env::temp_dir()`](https://doc.rust-lang.org/nightly/std/env/fn.temp_dir.html) value #[derive(Debug, PartialEq)] pub struct TempFile{ pub local_path: PathBuf, @@ -10,27 +23,41 @@ pub struct TempFile{ pub content: Bytes } -impl TempFile{ +impl TempFile { + + /// Gets the local path where file should be persisted pub fn get_local_path(&self) -> &PathBuf { &self.local_path } + /// Sets the local path to be used for persisting + pub fn set_local_path(&mut self,path: PathBuf) -> &PathBuf { + self.local_path = path; + &self.local_path + } + + /// Gets the file name pub fn get_name(&self) -> &str { &self.name } + /// Gets the file size pub fn get_size(&self) -> &Option { &self.size } + /// Gets the file content pub fn get_content(&self) -> &Bytes { &self.content } + /// Persists the file to the local_path property pub fn persist_file(&self) -> &PathBuf { + let full_path = format!("{}/{}",&self.local_path.to_str().unwrap(),&self.name); let mut file = File::create(full_path).unwrap(); - file.write_all(&self.content); + file.write_all(&self.content).unwrap(); &self.local_path + } } From 3f3c1571ff817a1a25fdc004b88562ec8358b272 Mon Sep 17 00:00:00 2001 From: Asone Date: Tue, 29 Mar 2022 12:02:14 +0200 Subject: [PATCH 08/12] update: format juniper_rocket_multipart_handler lib code --- .../src/graphql_upload_operations_request.rs | 17 ++-- .../src/graphql_upload_wrapper.rs | 88 ++++++++----------- juniper_rocket_multipart_handler/src/lib.rs | 4 +- .../src/temp_file.rs | 23 +++-- 4 files changed, 57 insertions(+), 75 deletions(-) diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs b/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs index 67cc6b2..83de319 100644 --- a/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs +++ b/juniper_rocket_multipart_handler/src/graphql_upload_operations_request.rs @@ -1,25 +1,20 @@ -use juniper::{GraphQLTypeAsync, GraphQLType}; -use juniper_rocket::{GraphQLResponse}; -use rocket::http::Status; use juniper::{ - http::GraphQLBatchRequest, - DefaultScalarValue, GraphQLSubscriptionType, - RootNode, ScalarValue, + http::GraphQLBatchRequest, DefaultScalarValue, GraphQLSubscriptionType, RootNode, ScalarValue, }; +use juniper::{GraphQLType, GraphQLTypeAsync}; +use juniper_rocket::GraphQLResponse; +use rocket::http::Status; /// A GraphQL operations request. -/// +/// /// This struct replicates the [`GraphQLRequest`](https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/src/lib.rs#L64) original behavior. /// It is provided and used with the upload wrapper as the original struct /// does not provide any constructor and the tuple constructor is private. #[derive(Debug, PartialEq)] -pub struct GraphQLOperationsRequest( -pub GraphQLBatchRequest -) +pub struct GraphQLOperationsRequest(pub GraphQLBatchRequest) where S: ScalarValue; - impl GraphQLOperationsRequest where S: ScalarValue, diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs index 5910c1a..3fe89b5 100644 --- a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs +++ b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs @@ -1,18 +1,17 @@ -use multer::{Multipart, bytes::Bytes}; +use multer::{bytes::Bytes, Multipart}; use rocket::{ data::{self, FromData, ToByteUnit}, - form::{Error}, + form::Error, http::{ContentType, Status}, outcome::Outcome::{Failure, Forward, Success}, - Data, Request + Data, Request, }; -use std::{env, path::PathBuf, collections::HashMap}; -use std::{sync::Arc}; +use std::sync::Arc; +use std::{collections::HashMap, env, path::PathBuf}; use juniper::{ http::{self, GraphQLBatchRequest}, - DefaultScalarValue, - ScalarValue, + DefaultScalarValue, ScalarValue, }; use crate::graphql_upload_operations_request::GraphQLOperationsRequest; @@ -24,7 +23,7 @@ const BODY_LIMIT: u64 = 1024 * 100; #[derive(Debug)] pub enum MultipartFormParsingError { - BoundaryParsingError + BoundaryParsingError, } enum ProcessorType { @@ -34,17 +33,16 @@ enum ProcessorType { UKNOWN, } - /// A Wrapper that handles HTTP GraphQL requests for [`Rocket`](https://rocket.rs/) /// with multipart/form-data support that follows the Apollo's -/// [`unofficial specification`](https://github.com/jaydenseric/graphql-multipart-request-spec.). +/// [`unofficial specification`](https://github.com/jaydenseric/graphql-multipart-request-spec.). /// /// # Main concept /// /// The current struct is nothing more than a wrapper -/// that will return two fields : +/// that will return two fields : /// - `operations` : Contains the graphQL Request to be executed. -/// This object is nothing more than a replica +/// This object is nothing more than a replica /// of the original [`GraphQLRequest`](https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/src/lib.rs#L64) /// object. /// - `files` : An optional HashMap that contains the buffered files data. @@ -56,11 +54,11 @@ enum ProcessorType { /// /// You can load the GraphQLUploadWrapper the same way /// you load the GraphQLRequest as both are data guards. -/// The main difference will be that instead, you'll call the +/// The main difference will be that instead, you'll call the /// execution of the query through the `operations` property -/// of the wrapper. +/// of the wrapper. /// -/// Below is basic example : +/// Below is basic example : /// /// ``` /// #[rocket::post("/upload", data = "")] @@ -77,8 +75,8 @@ enum ProcessorType { /// In order to fetch the uploaded files /// You'll need to implement your own context object /// That will pass the buffered files to your execution methods. -/// -/// Example : +/// +/// Example : /// ``` /// struct Ctx{ /// files: Option> @@ -86,8 +84,8 @@ enum ProcessorType { /// impl juniper::Context for Ctx {} /// ``` /// -/// You'll then be able to inject the buffered files to your -/// operations like this : +/// You'll then be able to inject the buffered files to your +/// operations like this : /// ``` /// struct Ctx{ files: Option> }; /// impl juniper::Context for Ctx {} @@ -105,9 +103,9 @@ enum ProcessorType { /// /// The Wrapper does nothing special with the uploaded files aside /// allocating them in heap memory through the Hashmap which means -/// they won't be stored anywhere, not even in a temporary folder, -/// unless you decide to. -/// +/// they won't be stored anywhere, not even in a temporary folder, +/// unless you decide to. +/// /// See [`TempFile`] for more available data and information around uploaded files. #[derive(Debug, PartialEq)] pub struct GraphQLUploadWrapper @@ -122,7 +120,6 @@ impl GraphQLUploadWrapper where S: ScalarValue, { - pub async fn get_files(&self) -> &Option> { &self.files } @@ -154,12 +151,11 @@ where ))) } - /// Body reader for multipart/form-data content type. async fn from_multipart_body<'r>( data: Data<'r>, content_type: &ContentType, - ) -> Result<(GraphQLBatchRequest,Option>), Error<'r>> { + ) -> Result<(GraphQLBatchRequest, Option>), Error<'r>> { // Builds a void query for development let mut query: String = String::new(); let boundary = Self::get_boundary(content_type).unwrap(); @@ -170,9 +166,9 @@ where // Create a multipart object based on multer let mut multipart = Multipart::new(stream, boundary); - + let mut files_map: HashMap = HashMap::new(); - + // Iterate on the form fields, which can be // either text content or binary. while let Some(entry) = multipart.next_field().await.unwrap() { @@ -183,11 +179,10 @@ where let name = field_name; - let path = format!("{}",env::temp_dir().display()); + let path = format!("{}", env::temp_dir().display()); match entry.content_type().as_ref() { Some(_) => { - let file_name = match entry.file_name() { Some(filename) => Arc::<&str>::from(filename).to_string(), None => continue, @@ -195,21 +190,18 @@ where let content = match entry.bytes().await { Ok(d) => d, - Err(_) => { - Bytes::new() - } + Err(_) => Bytes::new(), }; let tmpfile = TempFile { name: file_name, size: Some(content.len()), local_path: PathBuf::from(&path), - content: content + content: content, }; files_map.insert(name, tmpfile); - - }, + } None => { let content_data = entry.text().await; @@ -221,7 +213,7 @@ where query = result; } } - Err(_) => continue + Err(_) => continue, }; } } @@ -229,12 +221,13 @@ where // Default parser match serde_json::from_str(&query) { - Ok(req) => Ok((req,Some(files_map))), - Err(_) => Err(rocket::form::Error::validation("The provided request could not be parsed.")), + Ok(req) => Ok((req, Some(files_map))), + Err(_) => Err(rocket::form::Error::validation( + "The provided request could not be parsed.", + )), } } - /// Returns an enum value for a specific processors based on request Content-type fn get_processor_type(content_type: &ContentType) -> ProcessorType { let top = content_type.top().as_str(); @@ -248,7 +241,6 @@ where } } - /// Extracts the boundary for a multipart/form-data request pub fn get_boundary(content_type: &ContentType) -> Result<&str, MultipartFormParsingError> { match content_type.params().find(|&(k, _)| k == "boundary") { @@ -256,7 +248,6 @@ where None => Err(MultipartFormParsingError::BoundaryParsingError), } } - } #[rocket::async_trait] @@ -279,10 +270,9 @@ where ProcessorType::JSON => { Box::pin(async move { match Self::from_json_body(data) { - Ok(result) => Success(GraphQLUploadWrapper{ - operations: GraphQLOperationsRequest(result) - , - files: None + Ok(result) => Success(GraphQLUploadWrapper { + operations: GraphQLOperationsRequest(result), + files: None, }), Err(error) => Failure((Status::BadRequest, format!("{}", error))), } @@ -293,9 +283,9 @@ where ProcessorType::GRAPHQL => { Box::pin(async move { match Self::from_graphql_body(data) { - Ok(result) => Success(GraphQLUploadWrapper{ + Ok(result) => Success(GraphQLUploadWrapper { operations: GraphQLOperationsRequest(result), - files: None + files: None, }), Err(error) => Failure((Status::BadRequest, format!("{}", error))), } @@ -305,9 +295,9 @@ where ProcessorType::MULTIPART => { Box::pin(async move { match Self::from_multipart_body(data, content_type).await { - Ok(result) => Success(GraphQLUploadWrapper{ + Ok(result) => Success(GraphQLUploadWrapper { operations: GraphQLOperationsRequest(result.0), - files: result.1 + files: result.1, }), Err(error) => Failure((Status::BadRequest, format!("{}", error))), } diff --git a/juniper_rocket_multipart_handler/src/lib.rs b/juniper_rocket_multipart_handler/src/lib.rs index 779a86f..e0ad8a4 100644 --- a/juniper_rocket_multipart_handler/src/lib.rs +++ b/juniper_rocket_multipart_handler/src/lib.rs @@ -1,3 +1,3 @@ -pub mod graphql_upload_wrapper; pub mod graphql_upload_operations_request; -pub mod temp_file; \ No newline at end of file +pub mod graphql_upload_wrapper; +pub mod temp_file; diff --git a/juniper_rocket_multipart_handler/src/temp_file.rs b/juniper_rocket_multipart_handler/src/temp_file.rs index c9e469b..6090812 100644 --- a/juniper_rocket_multipart_handler/src/temp_file.rs +++ b/juniper_rocket_multipart_handler/src/temp_file.rs @@ -1,37 +1,36 @@ -use std::{path::PathBuf, fs::File, io::Write}; +use std::{fs::File, io::Write, path::PathBuf}; use multer::bytes::Bytes; /// A buffered file from file upload. -/// +/// /// Please, **do note** that this struct /// is different from original [`TempFile`] provided /// by original's rocket implementation as it does not -/// intend to rely on a specific lifetime specification -/// other than the graphQL request processing lifetime. +/// intend to rely on a specific lifetime specification +/// other than the graphQL request processing lifetime. /// /// Current [`TempFile`] struct provides with a `persist_file` -/// method that will write the file on the filesystem based on the +/// method that will write the file on the filesystem based on the /// `local_path` of the struct. /// /// default `local_path` provided is based on [`env::temp_dir()`](https://doc.rust-lang.org/nightly/std/env/fn.temp_dir.html) value #[derive(Debug, PartialEq)] -pub struct TempFile{ +pub struct TempFile { pub local_path: PathBuf, pub name: String, pub size: Option, - pub content: Bytes + pub content: Bytes, } impl TempFile { - /// Gets the local path where file should be persisted pub fn get_local_path(&self) -> &PathBuf { &self.local_path } /// Sets the local path to be used for persisting - pub fn set_local_path(&mut self,path: PathBuf) -> &PathBuf { + pub fn set_local_path(&mut self, path: PathBuf) -> &PathBuf { self.local_path = path; &self.local_path } @@ -51,13 +50,11 @@ impl TempFile { &self.content } - /// Persists the file to the local_path property + /// Persists the file to the local_path property pub fn persist_file(&self) -> &PathBuf { - - let full_path = format!("{}/{}",&self.local_path.to_str().unwrap(),&self.name); + let full_path = format!("{}/{}", &self.local_path.to_str().unwrap(), &self.name); let mut file = File::create(full_path).unwrap(); file.write_all(&self.content).unwrap(); &self.local_path - } } From 4b0be8aeef4c1f52474c93bb42eb49a987a4d1c5 Mon Sep 17 00:00:00 2001 From: Asone Date: Tue, 29 Mar 2022 12:03:58 +0200 Subject: [PATCH 09/12] update: use juniper_rocket_multipart_handler in project --- Cargo.lock | 617 +++++++++++++---------- Cargo.toml | 13 +- src/app.rs | 27 +- src/catchers/payment_required.rs | 3 +- src/graphql/context.rs | 11 +- src/graphql/mod.rs | 1 - src/graphql/multipart/mod.rs | 1 - src/graphql/multipart/upload_request.bak | 262 ---------- src/graphql/multipart/upload_request.rs | 260 ---------- src/graphql/paywall_context.rs | 2 +- src/graphql/query.rs | 1 - src/lnd/invoice.rs | 9 +- src/main.rs | 10 +- src/requests/header.rs | 6 +- 14 files changed, 408 insertions(+), 815 deletions(-) delete mode 100644 src/graphql/multipart/mod.rs delete mode 100644 src/graphql/multipart/upload_request.bak delete mode 100644 src/graphql/multipart/upload_request.rs diff --git a/Cargo.lock b/Cargo.lock index 9508b17..58b362a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,20 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "ascii" @@ -16,9 +25,9 @@ checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" [[package]] name = "async-stream" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" dependencies = [ "async-stream-impl", "futures-core", @@ -26,9 +35,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", "quote", @@ -37,9 +46,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -48,9 +57,9 @@ dependencies = [ [[package]] name = "atomic" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281" +checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" dependencies = [ "autocfg", ] @@ -68,9 +77,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base-x" @@ -121,9 +130,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bson" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903a4f4c7aa97921f1703acac1fd524e9e082b3228edd34dde07758c0c92c672" +checksum = "de0aa578035b938855a710ba58d43cfb4d435f3619f99236fb35922a574d6cb1" dependencies = [ "base64", "chrono", @@ -138,9 +147,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.1" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byteorder" @@ -156,9 +165,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.70" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" @@ -175,7 +184,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time 0.1.44", "winapi", ] @@ -194,9 +203,9 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" [[package]] name = "convert_case" @@ -217,14 +226,14 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.16" +version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.3.3", + "rustc_version 0.4.0", "syn", ] @@ -289,11 +298,11 @@ dependencies = [ [[package]] name = "diesel-derive-enum" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70806b70be328e646f243680a3fc93b3cfdd6db373faa5110660a5dd5af243bc" +checksum = "6c8910921b014e2af16298f006de12aa08af894b71f0f49a486ab6d74b17bbed" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", "quote", "syn", @@ -340,13 +349,22 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ "cfg-if", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "figment" version = "0.10.6" @@ -385,9 +403,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -400,9 +418,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -410,9 +428,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-enum" @@ -427,9 +445,9 @@ dependencies = [ [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -438,18 +456,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -457,23 +473,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -483,8 +498,6 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -514,13 +527,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -541,9 +554,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b" dependencies = [ "bytes", "fnv", @@ -554,7 +567,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.6.9", "tracing", ] @@ -573,6 +586,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -590,13 +609,13 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa 0.4.8", + "itoa", ] [[package]] @@ -618,15 +637,15 @@ checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.17" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ "bytes", "futures-channel", @@ -637,7 +656,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.1", + "itoa", "pin-project-lite", "socket2", "tokio", @@ -671,9 +690,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", @@ -682,15 +701,15 @@ dependencies = [ [[package]] name = "inlinable_string" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3094308123a0e9fd59659ce45e22de9f53fc1d2ac6e1feb9fef988e4f76cad77" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" [[package]] name = "instant" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] @@ -724,12 +743,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.1" @@ -738,18 +751,18 @@ checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] [[package]] name = "juniper" -version = "0.15.7" +version = "0.15.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637ffa8a8d8a05aed3331449e311f145864adcd82442d82e54d0522decb7cecf" +checksum = "21ac55c9084d08a7e315d78e2b15b7cc220f5eb67413c6ebf6967ee5de3b69fc" dependencies = [ "async-trait", "bson", @@ -769,9 +782,9 @@ dependencies = [ [[package]] name = "juniper_codegen" -version = "0.15.7" +version = "0.15.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a040e09482a45e77dd2dafa0d9d2651d17faf0ac674da0c93eabc3075ee24997" +checksum = "aee97671061ad50301ba077d054d295e01d31a1868fbd07902db651f987e71db" dependencies = [ "proc-macro-error", "proc-macro2", @@ -791,6 +804,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "juniper_rocket_multipart_handler" +version = "0.0.1" +dependencies = [ + "juniper", + "juniper_rocket", + "multer", + "rocket", + "serde", + "serde_json", + "tokio-util 0.7.1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -799,15 +825,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.103" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] name = "lightning" -version = "0.0.104" +version = "0.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0113e6b5a55b7ead30fb0a992b787e69a0551fa15b7eed93c99490eb018ab793" +checksum = "a10e7439623b293d000fc875627704210d8d0ff5b7badbb689f2a3d51afc618f" dependencies = [ "bitcoin", "secp256k1", @@ -815,9 +841,9 @@ dependencies = [ [[package]] name = "lightning-invoice" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2531e38818b3872b9acbcc3ff499f93962d2ff23ee0d05fc386ef70b993a38a0" +checksum = "ce661ad7182c2b258d1506e264095d2b4c71436a91b72834d7fb87b7e92e06c0" dependencies = [ "bech32", "bitcoin_hashes", @@ -847,6 +873,7 @@ dependencies = [ "itconfig", "juniper", "juniper_rocket", + "juniper_rocket_multipart_handler", "lazy_static", "lightning-invoice", "multer", @@ -855,7 +882,7 @@ dependencies = [ "rocket_sync_db_pools", "serde", "serde_json", - "tokio-util", + "tokio-util 0.7.1", "tonic", "tonic_lnd", "uuid", @@ -863,33 +890,44 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if", ] [[package]] name = "loom" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2111607c723d7857e0d8299d5ce7a0bf4b844d3e44f8de136b13da513eaf8fc4" +checksum = "edc5c7d328e32cc4954e8e01193d7f0ef5ab257b5090b70a964e099a36034309" dependencies = [ "cfg-if", "generator", "scoped-tls", "serde", "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", ] [[package]] @@ -933,14 +971,15 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.7.13" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", "miow", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] @@ -969,7 +1008,7 @@ dependencies = [ "mime", "spin 0.9.2", "tokio", - "tokio-util", + "tokio-util 0.6.9", "version_check", ] @@ -981,9 +1020,9 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] @@ -1009,9 +1048,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -1019,9 +1058,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "parking_lot" @@ -1077,15 +1116,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - [[package]] name = "petgraph" version = "0.5.1" @@ -1098,18 +1128,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -1118,9 +1148,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -1130,9 +1160,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "pq-sys" @@ -1173,17 +1203,11 @@ version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - [[package]] name = "proc-macro2" -version = "1.0.29" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] @@ -1228,7 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603" dependencies = [ "bytes", - "heck", + "heck 0.3.3", "itertools", "log", "multimap", @@ -1277,9 +1301,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" dependencies = [ "proc-macro2", ] @@ -1305,19 +1329,18 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc 0.2.0", + "rand_hc", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.3", - "rand_hc 0.3.1", ] [[package]] @@ -1355,7 +1378,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.6", ] [[package]] @@ -1367,20 +1390,11 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0" dependencies = [ "bitflags", ] @@ -1405,6 +1419,30 @@ dependencies = [ "syn", ] +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1451,7 +1489,7 @@ dependencies = [ "num_cpus", "parking_lot", "pin-project-lite", - "rand 0.8.4", + "rand 0.8.5", "ref-cast", "rocket_codegen", "rocket_http", @@ -1461,7 +1499,7 @@ dependencies = [ "time 0.2.27", "tokio", "tokio-stream", - "tokio-util", + "tokio-util 0.6.9", "ubyte", "version_check", "yansi", @@ -1469,14 +1507,14 @@ dependencies = [ [[package]] name = "rocket-multipart-form-data" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bec4978d352a6bd60b23b9e518096082f194abfbb03d0fdc189b5440dbb2798" +checksum = "6487715aab0e12c302267ae2821bb261b9558dd7e52e7e64e4eff15da9c5d6f4" dependencies = [ "mime", "multer", "rocket", - "tokio-util", + "tokio-util 0.7.1", ] [[package]] @@ -1558,11 +1596,11 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 0.11.0", + "semver 1.0.7", ] [[package]] @@ -1589,15 +1627,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "scheduled-thread-pool" @@ -1641,9 +1679,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "827cb7cce42533829c792fc51b82fbf18b125b45a702ef2c8be77fce65463a7b" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" dependencies = [ "cc", ] @@ -1654,17 +1692,14 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0", + "semver-parser", ] [[package]] name = "semver" -version = "0.11.0" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser 0.10.2", -] +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" [[package]] name = "semver-parser" @@ -1672,15 +1707,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "serde" version = "1.0.136" @@ -1703,21 +1729,39 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ "indexmap", - "itoa 0.4.8", + "itoa", "ryu", "serde", ] [[package]] name = "sha1" -version = "0.6.0" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] [[package]] name = "signal-hook-registry" @@ -1730,30 +1774,30 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "smartstring" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b" +checksum = "e714dff2b33f2321fdcd475b71cec79781a692d846f37f415fb395a1d2bcd48e" dependencies = [ "static_assertions", ] [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -1855,9 +1899,9 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "syn" -version = "1.0.77" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f" dependencies = [ "proc-macro2", "quote", @@ -1866,13 +1910,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", + "fastrand", "libc", - "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", @@ -1880,31 +1924,41 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -1948,9 +2002,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -1963,11 +2017,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.12.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -1976,6 +2029,7 @@ dependencies = [ "once_cell", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] @@ -1992,9 +2046,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -2014,9 +2068,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite", @@ -2025,9 +2079,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", @@ -2037,6 +2091,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.5.8" @@ -2070,7 +2137,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-stream", - "tokio-util", + "tokio-util 0.6.9", "tower", "tower-layer", "tower-service", @@ -2108,19 +2175,19 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60422bc7fefa2f3ec70359b8ff1caff59d785877eb70595904605bcc412470f" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" dependencies = [ "futures-core", "futures-util", "indexmap", "pin-project", - "rand 0.8.4", + "pin-project-lite", + "rand 0.8.5", "slab", "tokio", - "tokio-stream", - "tokio-util", + "tokio-util 0.7.1", "tower-layer", "tower-service", "tracing", @@ -2140,9 +2207,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.28" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ "cfg-if", "log", @@ -2153,9 +2220,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" dependencies = [ "proc-macro2", "quote", @@ -2164,11 +2231,12 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" dependencies = [ "lazy_static", + "valuable", ] [[package]] @@ -2181,6 +2249,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" +dependencies = [ + "ansi_term", + "lazy_static", + "matchers", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -2196,12 +2293,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "uncased" version = "0.9.6" @@ -2214,9 +2305,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -2229,9 +2320,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-xid" @@ -2272,10 +2363,16 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.6", "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2284,9 +2381,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "void" @@ -2312,15 +2409,21 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2328,9 +2431,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -2343,9 +2446,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2353,9 +2456,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -2366,15 +2469,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", @@ -2392,9 +2495,9 @@ dependencies = [ [[package]] name = "which" -version = "4.2.2" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" dependencies = [ "either", "lazy_static", @@ -2425,6 +2528,6 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "yansi" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index 33d2280..177a132 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["juniper_rocket_multipart_handler"] + [package] name = "lnapi" version = "0.1.1" @@ -22,13 +25,19 @@ tonic = "0.6.2" hex = "0.4.3" serde = "1.0.136" serde_json = "1.0.68" -lightning-invoice = "0.12.0" +lightning-invoice = "0.13.0" diesel_migrations = "1.4.0" rocket-multipart-form-data = "0.10.0" uuid = { version = "0.8.2", features = ["serde", "v4"] } multer = "2.0.2" + [dependencies.tokio-util] -version = "0.6.6" +version = "0.7.1" + +[dependencies.juniper_rocket_multipart_handler] +path = "./juniper_rocket_multipart_handler" + [dependencies.rocket_sync_db_pools] version = "0.1.0-rc.1" features = ["diesel_postgres_pool"] + diff --git a/src/app.rs b/src/app.rs index f0702aa..6b2bbee 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,28 +1,20 @@ -use std::collections::HashMap; - use crate::{ graphql::{context::GQLContext, mutation::Mutation, query::Query}, lnd::client::LndClient, }; -use rocket::{ - data::ToByteUnit, form::Form, fs::TempFile, http::ContentType, response::content, Data, State, -}; -use rocket_multipart_form_data::{ - FileField, MultipartFormData, MultipartFormDataField, MultipartFormDataOptions, -}; +use juniper_rocket_multipart_handler::graphql_upload_wrapper::GraphQLUploadWrapper; +use rocket::{ response::content, State }; pub type Schema = RootNode<'static, Query, Mutation, EmptySubscription>; use crate::db::PostgresConn; use crate::requests::header::PaymentRequestHeader; use juniper::{EmptySubscription, RootNode}; use juniper_rocket::GraphQLResponse; -use serde_json::Value; #[rocket::get("/")] pub fn graphiql() -> content::Html { juniper_rocket::graphiql_source("/graphql", None) } - /* This is a void handler that will return a 200 empty response for browsers that intends to check pre-flight for CORS rules. @@ -101,21 +93,22 @@ pub async fn payable_post_graphql_handler( #[rocket::post("/upload", data = "")] pub async fn upload<'r>( - request: crate::graphql::multipart::upload_request::GraphQLUploadRequest, + request: GraphQLUploadWrapper, schema: &State, db: PostgresConn, lnd: LndClient, ) -> GraphQLResponse { - let files = request.files.clone(); - - request + let result = request + .operations .execute( &*schema, &GQLContext { pool: db, - lnd: lnd, - files: files, + lnd, + files: request.files, }, ) - .await + .await; + + result } diff --git a/src/catchers/payment_required.rs b/src/catchers/payment_required.rs index ef59a3f..fb177a4 100644 --- a/src/catchers/payment_required.rs +++ b/src/catchers/payment_required.rs @@ -3,7 +3,6 @@ use crate::db::PostgresConn; use crate::lnd::client::LndClient; use rocket::response::content::Json; use rocket::response::status; -use rocket::serde::Serialize; use rocket::{catch, http::Status, Request}; /** @@ -28,7 +27,7 @@ pub async fn payment_required<'r>( ApiPayment::create_from_client(lnd_client, db, None).await }) .await; - match (payment_request_result) { + match payment_request_result { Ok(payment_request) => { let json_state = format!(r#"{{"payment": {:?}}}"#, payment_request.request.as_str()); diff --git a/src/graphql/context.rs b/src/graphql/context.rs index 38b930c..2cd82b6 100644 --- a/src/graphql/context.rs +++ b/src/graphql/context.rs @@ -1,7 +1,10 @@ +use std::collections::HashMap; + use crate::{db::PostgresConn, lnd::client::LndClient}; use derive_more::Deref; -use tonic::{codegen::InterceptedService, transport::Channel}; +use juniper_rocket_multipart_handler::temp_file::TempFile; +use tonic::codegen::InterceptedService; use tonic_lnd::{rpc::lightning_client::LightningClient, MacaroonInterceptor}; /* @@ -17,7 +20,7 @@ pub struct GQLContext { #[deref] pub pool: PostgresConn, pub lnd: LndClient, - pub files: Option>, + pub files: Option>, } impl juniper::Context for GQLContext {} @@ -41,4 +44,8 @@ impl GQLContext { pub fn get_db_connection(&self) -> &PostgresConn { return &self.pool; } + + pub fn get_files(&self) -> &Option> { + return &self.files; + } } diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index cccdfbc..a774c0c 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,5 +1,4 @@ pub mod context; -pub mod multipart; pub mod mutation; pub mod paywall_context; pub mod query; diff --git a/src/graphql/multipart/mod.rs b/src/graphql/multipart/mod.rs deleted file mode 100644 index dedde36..0000000 --- a/src/graphql/multipart/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod upload_request; diff --git a/src/graphql/multipart/upload_request.bak b/src/graphql/multipart/upload_request.bak deleted file mode 100644 index 893bb81..0000000 --- a/src/graphql/multipart/upload_request.bak +++ /dev/null @@ -1,262 +0,0 @@ -//! Utilities for building HTTP endpoints in a library-agnostic manner - -// pub mod playground; - -use juniper::http::GraphQLResponse; -use serde::{ - de, - ser::{self, SerializeMap}, - Deserialize, Serialize, -}; - -use juniper::{ - // ast::InputValue, - executor::{ExecutionError, ValuesStream}, - // value::{DefaultScalarValue, ScalarValue}, - FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode, - Value, Variables, ScalarValue, DefaultScalarValue, InputValue, http::{GraphQLBatchResponse, GraphQLResponse}, -}; - -/// The expected structure of the decoded JSON document for either POST or GET requests. -/// -/// For POST, you can use Serde to deserialize the incoming JSON data directly -/// into this struct - it derives Deserialize for exactly this reason. -/// -/// For GET, you will need to parse the query string and extract "query", -/// "operationName", and "variables" manually. -#[derive(Deserialize, Clone, Serialize, PartialEq, Debug)] -pub struct GraphQLUploadRequest -where - S: ScalarValue, -{ - query: String, - #[serde(rename = "operationName")] - operation_name: Option, - #[serde(bound(deserialize = "InputValue: Deserialize<'de> + Serialize"))] - variables: Option>, -} - -impl GraphQLUploadRequest -where - S: ScalarValue, -{ - /// Returns the `operation_name` associated with this request. - pub fn operation_name(&self) -> Option<&str> { - self.operation_name.as_deref() - } - - fn variables(&self) -> Variables { - self.variables - .as_ref() - .and_then(|iv| { - iv.to_object_value().map(|o| { - o.into_iter() - .map(|(k, v)| (k.to_owned(), v.clone())) - .collect() - }) - }) - .unwrap_or_default() - } - - /// Construct a new GraphQL request from parts - pub fn new( - query: String, - operation_name: Option, - variables: Option>, - ) -> Self { - GraphQLUploadRequest { - query, - operation_name, - variables, - } - } - - /// Execute a GraphQL request synchronously using the specified schema and context - /// - /// This is a simple wrapper around the `execute_sync` function exposed at the - /// top level of this crate. - pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode, - context: &QueryT::Context, - ) -> GraphQLResponse - where - S: ScalarValue, - QueryT: GraphQLType, - MutationT: GraphQLType, - SubscriptionT: GraphQLType, - { - GraphQLResponse(juniper::execute_sync( - &self.query, - self.operation_name(), - root_node, - &self.variables(), - context, - )) - } - - /// Execute a GraphQL request using the specified schema and context - /// - /// This is a simple wrapper around the `execute` function exposed at the - /// top level of this crate. - pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>, - context: &'a QueryT::Context, - ) -> GraphQLResponse - where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, - S: ScalarValue + Send + Sync, - { - let op = self.operation_name(); - let vars = &self.variables(); - let res = juniper::execute(&self.query, op, root_node, vars, context).await; - GraphQLResponse(res) - } -} - -/// Resolve a GraphQL subscription into `Value` using the -/// specified schema and context. -/// This is a wrapper around the `resolve_into_stream` function exposed at the top -/// level of this crate. -pub async fn resolve_into_stream<'req, 'rn, 'ctx, 'a, QueryT, MutationT, SubscriptionT, S>( - req: &'req GraphQLUploadRequest, - root_node: &'rn RootNode<'a, QueryT, MutationT, SubscriptionT, S>, - context: &'ctx QueryT::Context, -) -> Result<(Value>, Vec>), GraphQLError<'a>> -where - 'req: 'a, - 'rn: 'a, - 'ctx: 'a, - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, - S: ScalarValue + Send + Sync, -{ - let op = req.operation_name(); - let vars = req.variables(); - - juniper::resolve_into_stream(&req.query, op, root_node, &vars, context).await -} - -/// Simple wrapper around the result from executing a GraphQL query -/// -/// This struct implements Serialize, so you can simply serialize this -/// to JSON and send it over the wire. Use the `is_ok` method to determine -/// whether to send a 200 or 400 HTTP status code. - - -/// Simple wrapper around GraphQLUploadRequest to allow the handling of Batch requests. -#[derive(Debug, Deserialize, PartialEq)] -#[serde(untagged)] -#[serde(bound = "InputValue: Deserialize<'de>")] -pub enum GraphQLBatchRequest -where - S: ScalarValue, -{ - /// A single operation request. - Single(GraphQLUploadRequest), - - /// A batch operation request. - /// - /// Empty batch is considered as invalid value, so cannot be deserialized. - #[serde(deserialize_with = "deserialize_non_empty_vec")] - Batch(Vec>), -} - -fn deserialize_non_empty_vec<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: de::Deserializer<'de>, - T: Deserialize<'de>, -{ - use de::Error as _; - - let v = Vec::::deserialize(deserializer)?; - if v.is_empty() { - Err(D::Error::invalid_length(0, &"a positive integer")) - } else { - Ok(v) - } -} - -impl GraphQLBatchRequest -where - S: ScalarValue, -{ - /// Execute a GraphQL batch request synchronously using the specified schema and context - /// - /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLUploadRequest. - pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode, - context: &QueryT::Context, - ) -> GraphQLBatchResponse<'a, S> - where - QueryT: GraphQLType, - MutationT: GraphQLType, - SubscriptionT: GraphQLType, - { - match *self { - Self::Single(ref req) => { - GraphQLBatchResponse::Single(req.execute_sync(root_node, context)) - } - Self::Batch(ref reqs) => GraphQLBatchResponse::Batch( - reqs.iter() - .map(|req| req.execute_sync(root_node, context)) - .collect(), - ), - } - } - - /// Executes a GraphQL request using the specified schema and context - /// - /// This is a simple wrapper around the `execute` function exposed in - /// GraphQLUploadRequest - pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>, - context: &'a QueryT::Context, - ) -> GraphQLBatchResponse<'a, S> - where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, - S: Send + Sync, - { - match self { - Self::Single(req) => { - let resp = req.execute(root_node, context).await; - GraphQLBatchResponse::Single(resp) - } - Self::Batch(reqs) => { - let resps = futures::future::join_all( - reqs.iter().map(|req| req.execute(root_node, context)), - ) - .await; - GraphQLBatchResponse::Batch(resps) - } - } - } - - /// The operation names of the request. - pub fn operation_names(&self) -> Vec> { - match self { - Self::Single(req) => vec![req.operation_name()], - Self::Batch(reqs) => reqs.iter().map(|req| req.operation_name()).collect(), - } - } -} diff --git a/src/graphql/multipart/upload_request.rs b/src/graphql/multipart/upload_request.rs deleted file mode 100644 index 1ccf791..0000000 --- a/src/graphql/multipart/upload_request.rs +++ /dev/null @@ -1,260 +0,0 @@ -/* -# GraphQL Upload Request handler for rocket_juniper -# -*/ - -use juniper::GraphQLTypeAsync; -use juniper_rocket::{GraphQLResponse}; -use multer::Multipart; -use rocket::{ - data::{self, FromData, ToByteUnit}, - form::{Error}, - http::{ContentType, Status}, - outcome::Outcome::{Failure, Forward, Success}, - Data, Request, -}; -use std::fs::File; -use std::io::prelude::*; -use std::{sync::Arc}; - -use juniper::{ - http::{self, GraphQLBatchRequest}, - DefaultScalarValue, GraphQLSubscriptionType, - RootNode, ScalarValue, -}; -use serde::Deserialize; - -#[derive(Debug)] -pub enum MultipartFormParsingError { - BoundaryParsingError -} - -enum ProcessorType { - JSON, - GRAPHQL, - MULTIPART, - UKNOWN, -} - -// This shall be deferable to env -const BODY_LIMIT: u64 = 1024 * 100; - -#[derive(Debug, PartialEq, Deserialize)] -pub struct GraphQLUploadRequest -where - S: ScalarValue, -{ - pub gql_request: GraphQLBatchRequest, - pub files: Option>, -} - -impl GraphQLUploadRequest -where - S: ScalarValue, -{ - /** - Body reader for application/json content type. - This method replicates the original handler from juniper_rocket - */ - fn from_json_body<'r>(data: Data<'r>) -> Result<(GraphQLBatchRequest,Option>), serde_json::Error> { - let body = String::new(); - let mut _reader = data.open(BODY_LIMIT.bytes()); - - match serde_json::from_str(&body) { - Ok(req) => Ok(req), - Err(e) => Err(e), - } - } - - /** - Body reader for application/graphql content type. - This method replicates the original handler from juniper_rocket - */ - fn from_graphql_body<'r>(data: Data<'r>) -> Result, Error> { - let body = String::new(); - let mut _reader = data.open(BODY_LIMIT.bytes()); - - Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - body, None, None, - ))) - } - - /** - Body reader for multipart/form-data content type. - */ - async fn from_multipart_body<'r>( - data: Data<'r>, - content_type: &ContentType, - ) -> Result<(GraphQLBatchRequest,Option>), Error<'r>> { - // Builds a void query for development - let mut query: String = String::new(); - let boundary = Self::get_boundary(content_type).unwrap(); - - // Create and read a datastream from the request body - let reader = data.open(BODY_LIMIT.bytes()); - let stream = tokio_util::io::ReaderStream::new(reader); - - // Create a multipart object based on multer - let mut multipart = Multipart::new(stream, boundary); - let mut files = Vec::::new(); - - // Iterate on the form fields, which can be - // either text content or binary. - while let Some(entry) = multipart.next_field().await.unwrap() { - let field_name = match entry.name() { - Some(name) => Arc::<&str>::from(name), - None => continue, - }; - - let name = field_name.to_string(); - - // Check if there is some mimetype which should exist when a field is a binary file - if let Some(_mime_type) = entry.content_type().as_ref() { - - let file_name = match entry.file_name() { - Some(filename) => filename, - None => continue, - }; - - let path = format!("./tmp/{}", file_name); - let content = entry.bytes().await.unwrap(); - - let mut file = File::create(&path).unwrap(); - file.write_all(&content); - files.push(path); - - } else { - // No mimetype so we expect this to not be a file but rather a json content - let r = entry.text().await; - - // If field name is operations which should be the graphQL Query - // according to spec - match r { - Ok(result) => { - if name.as_str() == "operations" { - query = result; - } - } - Err(_) => {} - }; - } - } - - // Default parser - match serde_json::from_str(&query) { - Ok(req) => Ok((req,Some(files))), - Err(_) => Err(rocket::form::Error::validation("The provided request could not be parsed.")), - } - } - - /** - Returns an enum value for a specific processors based on request Content-type - */ - fn get_processor_type(content_type: &ContentType) -> ProcessorType { - let top = content_type.top().as_str(); - let sub = content_type.sub().as_str(); - - match (top, sub) { - ("application", "json") => ProcessorType::JSON, - ("application", "graphql") => ProcessorType::GRAPHQL, - ("multipart", "form-data") => ProcessorType::MULTIPART, - _ => ProcessorType::UKNOWN, - } - } - - /** - Extracts the boundary for a multipart/form-data request - */ - pub fn get_boundary(content_type: &ContentType) -> Result<&str, MultipartFormParsingError> { - match content_type.params().find(|&(k, _)| k == "boundary") { - Some(s) => Ok(s.1), - None => Err(MultipartFormParsingError::BoundaryParsingError), - } - } - - /// Asynchronously execute an incoming GraphQL query. - pub async fn execute( - &self, - root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>, - context: &CtxT, - ) -> GraphQLResponse - where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, - CtxT: Sync, - S: Send + Sync, - { - let response = self.gql_request.execute(root_node, context).await; - let status = if response.is_ok() { - Status::Ok - } else { - Status::BadRequest - }; - let json = serde_json::to_string(&response).unwrap(); - - GraphQLResponse(status, json) - } -} - -#[rocket::async_trait] -impl<'r, S> FromData<'r> for GraphQLUploadRequest -where - S: ScalarValue, -{ - type Error = String; - - async fn from_data( - req: &'r Request<'_>, - data: Data<'r>, - ) -> data::Outcome<'r, Self, Self::Error> { - // Get content-type of HTTP request - let content_type = req.content_type().unwrap(); - - // Split content-type value as a tuple of str - - match Self::get_processor_type(content_type) { - ProcessorType::JSON => { - Box::pin(async move { - match Self::from_json_body(data) { - Ok(result) => Success(GraphQLUploadRequest { - gql_request: result.0, - files: result.1, - }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), - } - // Success(Self {}) - }) - .await - } - ProcessorType::GRAPHQL => { - Box::pin(async move { - match Self::from_graphql_body(data) { - Ok(result) => Success(GraphQLUploadRequest { - gql_request: result, - files: None, - }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), - } - }) - .await - } - ProcessorType::MULTIPART => { - Box::pin(async move { - match Self::from_multipart_body(data, content_type).await { - Ok(result) => Success(GraphQLUploadRequest { - gql_request: result.0, - files: result.1 - }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), - } - }) - .await - } - ProcessorType::UKNOWN => Box::pin(async move { Forward(data) }).await, - } - } -} diff --git a/src/graphql/paywall_context.rs b/src/graphql/paywall_context.rs index 0fa268e..40dc8eb 100644 --- a/src/graphql/paywall_context.rs +++ b/src/graphql/paywall_context.rs @@ -1,7 +1,7 @@ use crate::{db::PostgresConn, lnd::client::LndClient}; use derive_more::Deref; -use tonic::{codegen::InterceptedService, transport::Channel}; +use tonic::codegen::InterceptedService; use tonic_lnd::{rpc::lightning_client::LightningClient, MacaroonInterceptor}; /* diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 0bee28e..6dc141b 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -9,7 +9,6 @@ use crate::{db::models::Post, graphql::context::GQLContext}; use juniper::{FieldError, Value}; use tonic_lnd::rpc::invoice::InvoiceState; use uuid::Uuid; - pub struct Query; #[juniper::graphql_object(context = GQLContext)] diff --git a/src/lnd/invoice.rs b/src/lnd/invoice.rs index 4c90cdc..fd0bfde 100644 --- a/src/lnd/invoice.rs +++ b/src/lnd/invoice.rs @@ -1,12 +1,11 @@ use crate::db::models::Post; use chrono::{Duration, NaiveDateTime, Utc}; +use std::env; use tonic::codegen::InterceptedService; -use tonic::transport::Channel; use tonic::{Code, Status}; use tonic_lnd::rpc::lightning_client::LightningClient; use tonic_lnd::rpc::{Invoice, PaymentHash}; use tonic_lnd::MacaroonInterceptor; -use std::env; use lightning_invoice::*; extern crate dotenv; @@ -19,9 +18,9 @@ pub struct InvoiceParams { impl InvoiceParams { pub fn new(value: Option, memo: Option, expiry: Option) -> Self { - let default_value = env::var("DEFAULT_INVOICE_VALUE").unwrap_or("100".to_string()); - let default_expiry = env::var("DEFAULT_INVOICE_EXPIRY").unwrap_or("API Payment".to_string()); + let default_expiry = + env::var("DEFAULT_INVOICE_EXPIRY").unwrap_or("API Payment".to_string()); let default_memo = env::var("DEFAULT_INVOICE_MEMO").unwrap_or("600".to_string()); Self { @@ -87,7 +86,7 @@ impl InvoiceUtils { Some(post.price as i64), // Memo content should be handle with an env var pattern Some(format!("buy {} : {}", post.uuid, post.title).to_string()), - None + None, ); // Request invoice generation to the LN Server InvoiceUtils::generate_invoice(lnd_client, params).await diff --git a/src/main.rs b/src/main.rs index 27642a0..37756c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,10 @@ extern crate diesel; extern crate diesel_derive_enum; extern crate dotenv; +extern crate juniper_rocket_multipart_handler; extern crate tokio_util; extern crate tonic; + mod app; mod catchers; mod cors; @@ -22,7 +24,10 @@ mod requests; use catchers::payment_required::payment_required; use dotenv::dotenv; use juniper::EmptySubscription; -use rocket::Rocket; +use rocket::{ + figment::Figment, + Rocket, +}; use crate::{ app::Schema, @@ -49,7 +54,10 @@ itconfig::config! { async fn main() { dotenv().ok(); + let figment = Figment::from(rocket::Config::default()); + // .merge(("limits", Limits::new().limit("json", 16.mebibytes()))); Rocket::build() + // .configure(figment) .register("/", catchers![payment_required]) .attach(PostgresConn::fairing()) .manage(Schema::new( diff --git a/src/requests/header.rs b/src/requests/header.rs index 4aeae17..1c8182d 100644 --- a/src/requests/header.rs +++ b/src/requests/header.rs @@ -1,8 +1,8 @@ use diesel::result::Error; use rocket::{ http::Status, - request::{self, local_cache, FromRequest, Outcome}, - Request, State, + request::{FromRequest, Outcome}, + Request }; use tonic_lnd::rpc::invoice::InvoiceState; @@ -36,7 +36,7 @@ impl<'r> FromRequest<'r> for PaymentRequestHeader { Some(payment_request) => { let api_payment = conn.find_api_payment(payment_request.to_string()).await; match api_payment { - Some(payment) => { + Some(_) => { outcome_from_payment_request(request, payment_request).await } None => Outcome::Failure((Status::PaymentRequired, None)), From 859ffa68ede844f22f4b83d03a8ff0d85d3a7bb9 Mon Sep 17 00:00:00 2001 From: Asone Date: Wed, 20 Apr 2022 20:40:10 +0200 Subject: [PATCH 10/12] feature: file persistance and aplication/json requests handling --- .../src/graphql_upload_wrapper.rs | 33 ++++++++---- .../src/temp_file.rs | 51 ++++++++++++++++--- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs index 3fe89b5..0e98e26 100644 --- a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs +++ b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs @@ -4,6 +4,7 @@ use rocket::{ form::Error, http::{ContentType, Status}, outcome::Outcome::{Failure, Forward, Success}, + route::Outcome, Data, Request, }; use std::sync::Arc; @@ -18,6 +19,7 @@ use crate::graphql_upload_operations_request::GraphQLOperationsRequest; use crate::temp_file::TempFile; +pub struct FilesMap {} // This shall be deferable to env const BODY_LIMIT: u64 = 1024 * 100; @@ -128,13 +130,18 @@ where Body reader for application/json content type. This method replicates the original handler from juniper_rocket */ - fn from_json_body<'r>(data: Data<'r>) -> Result, serde_json::Error> { - let body = String::new(); - let mut _reader = data.open(BODY_LIMIT.bytes()); - - match serde_json::from_str(&body) { - Ok(req) => Ok(req), - Err(e) => Err(e), + async fn from_json_body<'r>(data: Data<'r>) -> Result, Outcome<'r>> { + use rocket::tokio::io::AsyncReadExt as _; + + let mut reader = data.open(BODY_LIMIT.bytes()); + let mut body = String::new(); + let reader_result = reader.read_to_string(&mut body).await; + match reader_result { + Ok(_) => match serde_json::from_str(&body) { + Ok(req) => Ok(req), + Err(e) => Err(Failure(Status::BadRequest)), + }, + Err(e) => Err(Failure(Status::BadRequest)), } } @@ -158,6 +165,7 @@ where ) -> Result<(GraphQLBatchRequest, Option>), Error<'r>> { // Builds a void query for development let mut query: String = String::new(); + let mut map: String = String::new(); let boundary = Self::get_boundary(content_type).unwrap(); // Create and read a datastream from the request body @@ -193,7 +201,7 @@ where Err(_) => Bytes::new(), }; - let tmpfile = TempFile { + let mut tmpfile = TempFile { name: file_name, size: Some(content.len()), local_path: PathBuf::from(&path), @@ -211,7 +219,12 @@ where Ok(result) => { if name.as_str() == "operations" { query = result; + continue; } + + // if name.as_str() == "map" { + // map = result; + // } } Err(_) => continue, }; @@ -269,12 +282,12 @@ where match Self::get_processor_type(content_type) { ProcessorType::JSON => { Box::pin(async move { - match Self::from_json_body(data) { + match Self::from_json_body(data).await { Ok(result) => Success(GraphQLUploadWrapper { operations: GraphQLOperationsRequest(result), files: None, }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), + Err(e) => Failure((Status::BadRequest, format!("{}", ""))), } // Success(Self {}) }) diff --git a/juniper_rocket_multipart_handler/src/temp_file.rs b/juniper_rocket_multipart_handler/src/temp_file.rs index 6090812..2641629 100644 --- a/juniper_rocket_multipart_handler/src/temp_file.rs +++ b/juniper_rocket_multipart_handler/src/temp_file.rs @@ -1,6 +1,7 @@ -use std::{fs::File, io::Write, path::PathBuf}; - use multer::bytes::Bytes; +use std::fs; +use std::io::Error; +use std::{fs::File, io::Write, path::PathBuf}; /// A buffered file from file upload. /// @@ -15,7 +16,7 @@ use multer::bytes::Bytes; /// `local_path` of the struct. /// /// default `local_path` provided is based on [`env::temp_dir()`](https://doc.rust-lang.org/nightly/std/env/fn.temp_dir.html) value -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct TempFile { pub local_path: PathBuf, pub name: String, @@ -50,11 +51,47 @@ impl TempFile { &self.content } + fn path_checker(path: &str) -> Result<(), Error> { + fs::create_dir_all(path) + } + /// Persists the file to the local_path property - pub fn persist_file(&self) -> &PathBuf { + pub fn persist_file(&self) -> Result<&PathBuf, Error> { let full_path = format!("{}/{}", &self.local_path.to_str().unwrap(), &self.name); - let mut file = File::create(full_path).unwrap(); - file.write_all(&self.content).unwrap(); - &self.local_path + let file = File::create(full_path); + + match file { + Ok(mut file) => { + let result = file.write_all(&self.content); + match result { + Ok(_) => Ok(&self.local_path), + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + } + + /// persists file to a given location + pub fn persist_to(&self, path: &str) -> Result { + match Self::path_checker(path) { + Ok(_) => { + let full_path = format!("{}/{}", path, &self.name); + let path_buf = PathBuf::from(&full_path); + let file = File::create(&full_path); + + match file { + Ok(mut file) => { + let result = file.write_all(&self.content); + match result { + Ok(_) => Ok(path_buf), + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } } } From 3b813ff505e68a15cecc90f7c68e70726e254117 Mon Sep 17 00:00:00 2001 From: Asone Date: Wed, 20 Apr 2022 22:25:23 +0200 Subject: [PATCH 11/12] feature: add rocket based body size for multipart. Add graphql native requests processing --- .../src/graphql_upload_wrapper.rs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs index 0e98e26..d23d7de 100644 --- a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs +++ b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs @@ -1,6 +1,6 @@ use multer::{bytes::Bytes, Multipart}; use rocket::{ - data::{self, FromData, ToByteUnit}, + data::{self, ByteUnit, FromData, ToByteUnit}, form::Error, http::{ContentType, Status}, outcome::Outcome::{Failure, Forward, Success}, @@ -19,7 +19,6 @@ use crate::graphql_upload_operations_request::GraphQLOperationsRequest; use crate::temp_file::TempFile; -pub struct FilesMap {} // This shall be deferable to env const BODY_LIMIT: u64 = 1024 * 100; @@ -122,14 +121,13 @@ impl GraphQLUploadWrapper where S: ScalarValue, { + // Retrieves files pub async fn get_files(&self) -> &Option> { &self.files } - /** - Body reader for application/json content type. - This method replicates the original handler from juniper_rocket - */ + // Body reader for application/json content type. + // This method replicates the original handler from juniper_rocket async fn from_json_body<'r>(data: Data<'r>) -> Result, Outcome<'r>> { use rocket::tokio::io::AsyncReadExt as _; @@ -145,23 +143,27 @@ where } } - /** - Body reader for application/graphql content type. - This method replicates the original handler from juniper_rocket - */ - fn from_graphql_body<'r>(data: Data<'r>) -> Result, Error> { - let body = String::new(); - let mut _reader = data.open(BODY_LIMIT.bytes()); - - Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - body, None, None, - ))) + // Body reader for application/graphql content type. + // This method replicates the original handler from juniper_rocket + async fn from_graphql_body<'r>(data: Data<'r>) -> Result, Outcome<'r>> { + use rocket::tokio::io::AsyncReadExt as _; + + let mut reader = data.open(BODY_LIMIT.bytes()); + let mut body = String::new(); + let reader_result = reader.read_to_string(&mut body).await; + match reader_result { + Ok(_) => Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + body, None, None, + ))), + Err(e) => Err(Failure(Status::BadRequest)), + } } /// Body reader for multipart/form-data content type. async fn from_multipart_body<'r>( data: Data<'r>, content_type: &ContentType, + file_limit: ByteUnit, ) -> Result<(GraphQLBatchRequest, Option>), Error<'r>> { // Builds a void query for development let mut query: String = String::new(); @@ -169,7 +171,7 @@ where let boundary = Self::get_boundary(content_type).unwrap(); // Create and read a datastream from the request body - let reader = data.open(BODY_LIMIT.bytes()); + let reader = data.open(file_limit); let stream = tokio_util::io::ReaderStream::new(reader); // Create a multipart object based on multer @@ -263,13 +265,14 @@ where } } +struct Config {} + #[rocket::async_trait] impl<'r, S> FromData<'r> for GraphQLUploadWrapper where S: ScalarValue, { type Error = String; - async fn from_data( req: &'r Request<'_>, data: Data<'r>, @@ -287,7 +290,7 @@ where operations: GraphQLOperationsRequest(result), files: None, }), - Err(e) => Failure((Status::BadRequest, format!("{}", ""))), + Err(error) => Failure((Status::BadRequest, format!("{}", error))), } // Success(Self {}) }) @@ -295,7 +298,7 @@ where } ProcessorType::GRAPHQL => { Box::pin(async move { - match Self::from_graphql_body(data) { + match Self::from_graphql_body(data).await { Ok(result) => Success(GraphQLUploadWrapper { operations: GraphQLOperationsRequest(result), files: None, @@ -307,7 +310,13 @@ where } ProcessorType::MULTIPART => { Box::pin(async move { - match Self::from_multipart_body(data, content_type).await { + match Self::from_multipart_body( + data, + content_type, + req.limits().get("data-form").unwrap(), + ) + .await + { Ok(result) => Success(GraphQLUploadWrapper { operations: GraphQLOperationsRequest(result.0), files: result.1, From 18933c3d32af9e19dc74eab5d056f2838f9b09a8 Mon Sep 17 00:00:00 2001 From: Asone Date: Sat, 6 Aug 2022 08:41:12 +0200 Subject: [PATCH 12/12] feat(file-upload): Many updates and improvements: - Restructured migrations with improvement on payment model - Added an igniter to allow running migrations on ignite - query to request invoices for a media - splitted queries and mutations to separated files - Added SSL configuration load based on environment - Moved part of doc --- .env.dist | 11 +- .gitignore | 3 +- Cargo.lock | 755 +++++++++++------- Cargo.toml | 21 +- README.md | 317 +------- docs/configuration.md | 41 + docs/installation.md | 264 ++++++ docs/paywall.md | 39 + juniper_rocket_multipart_handler/Cargo.toml | 20 - .../src/graphql_upload_wrapper.rs | 332 -------- .../src/temp_file.rs | 97 --- .../2022-04-16-153823_create_media/down.sql | 2 + .../2022-04-16-153823_create_media/up.sql | 11 + .../down.sql | 3 + .../up.sql | 11 + rocket.toml.dist | 14 +- src/app.rs | 76 +- src/catchers/payment_required.rs | 6 +- src/cors.rs | 60 +- src/db/igniter.rs | 32 + src/db/mod.rs | 1 + src/db/models/media.rs | 73 ++ src/db/models/media_payment.rs | 65 ++ src/db/models/mod.rs | 2 + src/db/models/payment.rs | 3 + src/db/schema.rs | 35 +- src/graphql/context.rs | 9 +- src/graphql/mod.rs | 1 + src/graphql/mutation.rs | 68 +- src/graphql/paywall_context.rs | 3 +- src/graphql/queries/get_files_list.rs | 14 + src/graphql/queries/get_media.rs | 30 + src/graphql/queries/get_post.rs | 84 ++ src/graphql/queries/get_posts_list.rs | 15 + src/graphql/queries/mod.rs | 7 + .../queries/request_invoice_for_media.rs | 215 +++++ .../queries/request_invoice_for_post.rs | 38 + src/graphql/queries/utils.rs | 45 ++ src/graphql/query.rs | 198 ++--- src/graphql/types/input/file.rs | 11 + src/graphql/types/input/mod.rs | 1 + src/graphql/types/output/invoices.rs | 76 ++ src/graphql/types/output/media.rs | 167 ++++ src/graphql/types/output/mod.rs | 2 + src/graphql/types/output/payment.rs | 47 +- src/guards/paymentrequestheader.rs | 2 +- src/guards/userguard.rs | 10 +- src/lnd/invoice.rs | 71 +- src/main.rs | 46 +- src/routes/auth.rs | 35 + src/routes/file.rs | 234 ++++++ src/routes/graphql.rs | 0 src/routes/mod.rs | 3 + src/routes/utils.rs | 6 + 54 files changed, 2393 insertions(+), 1339 deletions(-) create mode 100644 docs/configuration.md create mode 100644 docs/installation.md create mode 100644 docs/paywall.md delete mode 100644 juniper_rocket_multipart_handler/Cargo.toml delete mode 100644 juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs delete mode 100644 juniper_rocket_multipart_handler/src/temp_file.rs create mode 100644 migrations/2022-04-16-153823_create_media/down.sql create mode 100644 migrations/2022-04-16-153823_create_media/up.sql create mode 100644 migrations/2022-04-21-063037_create_media_payment/down.sql create mode 100644 migrations/2022-04-21-063037_create_media_payment/up.sql create mode 100644 src/db/igniter.rs create mode 100644 src/db/models/media.rs create mode 100644 src/db/models/media_payment.rs create mode 100644 src/graphql/queries/get_files_list.rs create mode 100644 src/graphql/queries/get_media.rs create mode 100644 src/graphql/queries/get_post.rs create mode 100644 src/graphql/queries/get_posts_list.rs create mode 100644 src/graphql/queries/mod.rs create mode 100644 src/graphql/queries/request_invoice_for_media.rs create mode 100644 src/graphql/queries/request_invoice_for_post.rs create mode 100644 src/graphql/queries/utils.rs create mode 100644 src/graphql/types/input/file.rs create mode 100644 src/graphql/types/output/invoices.rs create mode 100644 src/graphql/types/output/media.rs create mode 100644 src/routes/auth.rs create mode 100644 src/routes/file.rs create mode 100644 src/routes/graphql.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/utils.rs diff --git a/.env.dist b/.env.dist index 853f0d8..937af9b 100644 --- a/.env.dist +++ b/.env.dist @@ -1,5 +1,8 @@ # PGSQL DATABASE_URL=postgres://user:password@host/db_name +DATABASE_RUN_MIGRATIONS_ON_IGNITE=true +DB_PASSWORD=db_password +DB_USER=db_user # LND SERVER CONFIGURATION LND_ADDRESS="https://umbrel.local:10009" @@ -13,4 +16,10 @@ DEFAULT_INVOICE_EXPIRY=3000 # JWT CONFIGURATION JWT_TOKEN_DURATION=1000 -JWT_TOKEN_SECRET=secret \ No newline at end of file +JWT_TOKEN_SECRET=secret + +#CORS POLICY +CORS_ORIGIN_POLICY="*" +CORS_METHOD_POLICY="POST, GET, PATCH, OPTIONS" +CORS_HEADERS_POLICY="*" +CORS_CREDENTIALS_POLICY="true" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5871fc9..d17408f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ _dump target .env src/lnd/config/ -rocket.toml \ No newline at end of file +rocket.toml +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c3690a5..951022e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher 0.3.0", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -22,9 +57,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "ascii" @@ -90,12 +125,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base-x" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" - [[package]] name = "base64" version = "0.13.0" @@ -104,9 +133,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bcrypt" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe4fef31efb0f76133ae8e3576a88e58edb7cfc5584c81c758c349ba46b43fc" +checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641" dependencies = [ "base64", "blowfish", @@ -149,6 +178,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -156,7 +194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" dependencies = [ "byteorder", - "cipher", + "cipher 0.4.3", ] [[package]] @@ -173,7 +211,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -200,6 +238,17 @@ version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +[[package]] +name = "cfb" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "466bc70ba95e7ad3322ce4c69a2bc22635a0f9535c1ffb7174bc28c286764517" +dependencies = [ + "byteorder", + "fnv", + "uuid 1.1.0", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -220,6 +269,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.3" @@ -243,12 +301,6 @@ dependencies = [ "unreachable", ] -[[package]] -name = "const_fn" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" - [[package]] name = "convert_case" version = "0.4.0" @@ -257,15 +309,31 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac", "percent-encoding", - "time 0.2.27", + "rand 0.8.5", + "sha2", + "subtle", + "time 0.3.9", "version_check", ] +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -276,6 +344,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -285,7 +362,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn", ] @@ -345,7 +422,7 @@ dependencies = [ "diesel_derives", "pq-sys", "r2d2", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -382,10 +459,15 @@ dependencies = [ ] [[package]] -name = "discard" -version = "1.0.4" +name = "digest" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] [[package]] name = "dotenv" @@ -598,6 +680,16 @@ dependencies = [ "wasi 0.10.0+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.0" @@ -629,7 +721,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.1", + "tokio-util 0.7.2", "tracing", ] @@ -669,11 +761,29 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ "bytes", "fnv", @@ -693,9 +803,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] name = "httpdate" @@ -761,6 +871,15 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e035cede526e0b21d5adffc9fa0eb4ef5d6026fe9c5b0bfe8084b9472b587a55" +dependencies = [ + "cfb", +] + [[package]] name = "inlinable_string" version = "0.1.15" @@ -816,9 +935,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "js-sys" @@ -862,7 +981,7 @@ dependencies = [ "smartstring", "static_assertions", "url", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -879,9 +998,9 @@ dependencies = [ [[package]] name = "juniper_rocket" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf70a2ac6acfdd7b6e24d42b8ad8b99fbb60eb12d0c08b388d829e99c0ee08f9" +checksum = "a431e6f03bc31bd74498a837e87ddf635deef3c1a2026e59680dec2552c84c28" dependencies = [ "futures", "juniper", @@ -891,7 +1010,9 @@ dependencies = [ [[package]] name = "juniper_rocket_multipart_handler" -version = "0.0.1" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4028f0919613020b6308b9c161ac4c93ae70b9581f2d7aa49ada9d37b59763af" dependencies = [ "juniper", "juniper_rocket", @@ -899,7 +1020,8 @@ dependencies = [ "rocket", "serde", "serde_json", - "tokio-util 0.7.1", + "tokio-util 0.7.2", + "uuid 1.1.0", ] [[package]] @@ -910,9 +1032,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "lightning" @@ -956,6 +1078,7 @@ dependencies = [ "dotenv", "futures", "hex", + "infer", "itconfig", "jsonwebtoken", "juniper", @@ -970,10 +1093,10 @@ dependencies = [ "rocket_sync_db_pools", "serde", "serde_json", - "tokio-util 0.7.1", + "tokio-util 0.7.2", "tonic", "tonic_lnd", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -988,18 +1111,18 @@ dependencies = [ [[package]] name = "log" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "loom" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5c7d328e32cc4954e8e01193d7f0ef5ab257b5090b70a964e099a36034309" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if", "generator", @@ -1027,9 +1150,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "migrations_internals" @@ -1060,25 +1183,14 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ "libc", "log", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "windows-sys", ] [[package]] @@ -1095,9 +1207,9 @@ dependencies = [ "log", "memchr", "mime", - "spin 0.9.2", + "spin 0.9.3", "tokio", - "tokio-util 0.6.9", + "tokio-util 0.6.10", "version_check", ] @@ -1107,15 +1219,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", -] - [[package]] name = "num-bigint" version = "0.4.3" @@ -1129,9 +1232,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -1139,9 +1242,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] @@ -1158,18 +1261,24 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "once_cell" -version = "1.10.0" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca" + +[[package]] +name = "opaque-debug" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parking_lot" @@ -1179,7 +1288,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", ] [[package]] @@ -1196,6 +1315,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "pear" version = "0.2.3" @@ -1266,9 +1398,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -1276,6 +1408,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1315,19 +1459,13 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1442,7 +1580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" dependencies = [ "log", - "parking_lot", + "parking_lot 0.11.2", "scheduled-thread-pool", ] @@ -1528,18 +1666,18 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300f2a835d808734ee295d45007adacb9ebb29dd3ae2424acfa17930cae541da" +checksum = "685d58625b6c2b83e4cc88a27c4bf65adb7b6b16dbdc413e515c9405b47432ab" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c38e3aecd2b21cb3959637b883bb3714bc7e43f0268b9a29d3743ee3e55cdd2" +checksum = "a043824e29c94169374ac5183ac0ed43f5724dc4556b19568007486bd840fa1f" dependencies = [ "proc-macro2", "quote", @@ -1598,9 +1736,9 @@ dependencies = [ [[package]] name = "rocket" -version = "0.5.0-rc.1" +version = "0.5.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a71c18c42a0eb15bf3816831caf0dad11e7966f2a41aaf486a701979c4dd1f2" +checksum = "98ead083fce4a405feb349cf09abdf64471c6077f14e0ce59364aa90d4b99317" dependencies = [ "async-stream", "async-trait", @@ -1616,7 +1754,7 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot", + "parking_lot 0.12.0", "pin-project-lite", "rand 0.8.5", "ref-cast", @@ -1625,10 +1763,10 @@ dependencies = [ "serde", "state", "tempfile", - "time 0.2.27", + "time 0.3.9", "tokio", "tokio-stream", - "tokio-util 0.6.9", + "tokio-util 0.7.2", "ubyte", "version_check", "yansi", @@ -1636,21 +1774,21 @@ dependencies = [ [[package]] name = "rocket-multipart-form-data" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6487715aab0e12c302267ae2821bb261b9558dd7e52e7e64e4eff15da9c5d6f4" +checksum = "3a4b5ad1a62bfa8a250204c41cf02c94cf9ef135a680669cdb39dbbeb8b0132e" dependencies = [ "mime", "multer", "rocket", - "tokio-util 0.7.1", + "tokio-util 0.7.2", ] [[package]] name = "rocket_codegen" -version = "0.5.0-rc.1" +version = "0.5.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f5fa462f7eb958bba8710c17c5d774bbbd59809fa76fb1957af7e545aea8bb" +checksum = "d6aeb6bb9c61e9cd2c00d70ea267bf36f76a4cc615e5908b349c2f9d93999b47" dependencies = [ "devise", "glob", @@ -1664,37 +1802,39 @@ dependencies = [ [[package]] name = "rocket_http" -version = "0.5.0-rc.1" +version = "0.5.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c8b7d512d2fcac2316ebe590cde67573844b99e6cc9ee0f53375fa16e25ebd" +checksum = "2ded65d127954de3c12471630bf4b81a2792f065984461e65b91d0fdaafc17a2" dependencies = [ "cookie", "either", + "futures", "http", "hyper", "indexmap", "log", "memchr", - "mime", - "parking_lot", "pear", "percent-encoding", "pin-project-lite", "ref-cast", + "rustls 0.20.6", + "rustls-pemfile 1.0.0", "serde", "smallvec", "stable-pattern", "state", - "time 0.2.27", + "time 0.3.9", "tokio", + "tokio-rustls 0.23.4", "uncased", ] [[package]] name = "rocket_sync_db_pools" -version = "0.1.0-rc.1" +version = "0.1.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cfdfebd552d075c368e641c88a5cd6ce1c58c5c710548aeb777abb48830f4b" +checksum = "5fa48b6ab25013e9812f1b0c592741900b3a2a83c0936292e0565c0ac842f558" dependencies = [ "diesel", "r2d2", @@ -1706,30 +1846,21 @@ dependencies = [ [[package]] name = "rocket_sync_db_pools_codegen" -version = "0.1.0-rc.1" +version = "0.1.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267808c094db5366e1d8925aaf9f2ce05ff9b3bd92cb18c7040a1fe219c2e25" +checksum = "280ef2d232923e69cb93da156972eb5476a7cce5ba44843f6608f46a4abf7aab" dependencies = [ "devise", "quote", ] -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.7", + "semver", ] [[package]] @@ -1741,8 +1872,20 @@ dependencies = [ "base64", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -1754,6 +1897,15 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.6" @@ -1762,9 +1914,9 @@ checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "scheduled-thread-pool" @@ -1772,7 +1924,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" dependencies = [ - "parking_lot", + "parking_lot 0.11.2", ] [[package]] @@ -1797,6 +1949,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.20.3" @@ -1817,39 +1979,24 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" - -[[package]] -name = "semver-parser" -version = "0.7.0" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -1858,9 +2005,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "indexmap", "itoa", @@ -1869,20 +2016,16 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.6.1" +name = "sha2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ - "sha1_smol", + "cfg-if", + "cpufeatures", + "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sharded-slab" version = "0.1.4" @@ -1952,9 +2095,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" [[package]] name = "stable-pattern" @@ -1965,20 +2108,11 @@ dependencies = [ "memchr", ] -[[package]] -name = "standback" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -dependencies = [ - "version_check", -] - [[package]] name = "state" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" dependencies = [ "loom", ] @@ -1990,63 +2124,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" +name = "subtle" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.91" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2065,18 +2156,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", @@ -2103,21 +2194,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "time" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros 0.1.1", - "version_check", - "winapi", -] - [[package]] name = "time" version = "0.3.9" @@ -2128,17 +2204,7 @@ dependencies = [ "libc", "num_threads", "quickcheck", - "time-macros 0.2.4", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", + "time-macros", ] [[package]] @@ -2147,24 +2213,11 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" -[[package]] -name = "time-macros-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", -] - [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -2177,9 +2230,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" dependencies = [ "bytes", "libc", @@ -2221,9 +2274,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.6", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -2239,9 +2303,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", @@ -2253,9 +2317,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ "bytes", "futures-core", @@ -2267,9 +2331,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -2296,9 +2360,9 @@ dependencies = [ "prost 0.9.0", "prost-derive 0.9.0", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tokio-stream", - "tokio-util 0.6.9", + "tokio-util 0.6.10", "tower", "tower-layer", "tower-service", @@ -2326,12 +2390,12 @@ checksum = "e133e6e4c0162f45dea89ba7635abee93632ca7d08bdba29eefff2dc5e00aed7" dependencies = [ "hex", "prost 0.9.0", - "rustls", - "rustls-pemfile", + "rustls 0.19.1", + "rustls-pemfile 0.2.1", "tokio", "tonic", "tonic-build", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -2348,7 +2412,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.1", + "tokio-util 0.7.2", "tower-layer", "tower-service", "tracing", @@ -2368,9 +2432,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if", "log", @@ -2381,9 +2445,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", "quote", @@ -2392,9 +2456,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dfce9f3241b150f36e8e54bb561a742d5daa1a47b5dd9a5ce369fd4a4db2210" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", "valuable", @@ -2412,9 +2476,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" dependencies = [ "lazy_static", "log", @@ -2453,18 +2517,18 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ubyte" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42756bb9e708855de2f8a98195643dff31a97f0485d90d8467b39dc24be9e8fe" +checksum = "a58e29f263341a29bb79e14ad7fda5f63b1c7e48929bad4c685d7876b1d04e94" dependencies = [ "serde", ] [[package]] name = "uncased" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" +checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" dependencies = [ "serde", "version_check", @@ -2472,9 +2536,15 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "unicode-normalization" @@ -2493,9 +2563,19 @@ checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-xid" -version = "0.2.2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "universal-hash" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] [[package]] name = "unreachable" @@ -2534,6 +2614,16 @@ dependencies = [ "serde", ] +[[package]] +name = "uuid" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93bbc61e655a4833cf400d0d15bf3649313422fa7572886ad6dab16d79886365" +dependencies = [ + "getrandom 0.2.6", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2660,6 +2750,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "which" version = "4.2.5" @@ -2693,6 +2793,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "yansi" version = "0.5.1" @@ -2701,6 +2844,6 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zeroize" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317" +checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" diff --git a/Cargo.toml b/Cargo.toml index 5f3696a..2bb6b63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,3 @@ -[workspace] -members = ["juniper_rocket_multipart_handler"] - [package] name = "lnapi" version = "0.1.1" @@ -10,9 +7,9 @@ edition = "2018" [dependencies] futures = "0.3.1" -rocket = "0.5.0-rc.1" -juniper = "0.15.7" -juniper_rocket = "0.8.0" +rocket = { version = "0.5.0-rc.2", features = ["tls"] } +juniper = "0.15.9" +juniper_rocket = "0.8.2" diesel = { version = "1.4.7", features = ["postgres","r2d2","chrono","uuidv07"] } dotenv = "0.15.0" chrono = { version = "0.4.19", features = ["serde"] } @@ -27,22 +24,20 @@ serde = "1.0.136" serde_json = "1.0.68" lightning-invoice = "0.14.0" diesel_migrations = "1.4.0" -rocket-multipart-form-data = "0.10.0" +rocket-multipart-form-data = "0.10.3" uuid = { version = "0.8.2", features = ["serde", "v4"] } multer = "2.0.2" jsonwebtoken = "8.0.1" regex = "1.5.5" -bcrypt = "0.12" +bcrypt = "0.13.0" +juniper_rocket_multipart_handler = "0.1.0" +infer = "0.8.1" [dependencies.tokio-util] version = "0.7.1" -[dependencies.juniper_rocket_multipart_handler] -path = "./juniper_rocket_multipart_handler" - [dependencies.rocket_sync_db_pools] -version = "0.1.0-rc.1" +version = "0.1.0-rc.2" features = ["diesel_postgres_pool"] - diff --git a/README.md b/README.md index de7afd4..73168bc 100644 --- a/README.md +++ b/README.md @@ -2,316 +2,23 @@ ![graphQLN](https://github.com/Asone/graphQLN/actions/workflows/rust.yml/badge.svg) -This project is a demo of a [graphQL](https://graphql.org/) API with a built-in bitcoin [lightning network](https://en.wikipedia.org/wiki/Lightning_Network) paywall mechanism, built with [Rustlang](https://www.rust-lang.org/). +GraphQLN is a proof-of-concept of a [graphQL](https://graphql.org/) API with a built-in bitcoin [lightning network](https://en.wikipedia.org/wiki/Lightning_Network) paywall mechanism, built with [Rustlang](https://www.rust-lang.org/). +## Status -## Install project - -1. Clone the project : - -``` -git clone -``` - -2. go to the folder and install dependencies : -``` -cargo install --path . -``` - -## Set-up the project - -In order to be able to launch the project you need to set-up a few configurations. Once all set-up, you shall be able to launch the server with - -``` -cargo run -``` - -You'll find then a [graphiQL](https://github.com/graphql/graphiql) interface on [http://localhost:8000](http://localhost:8000) -### Configure Database connection - -The current project uses [postgres]() as database engine. -To set-up the connection copy the `rocket.toml.dist` file as `rocket.toml` and fill the connection URL as mentioned. - -``` -main_db = { url = "postgres://:@/"} -``` - -### Configure diesel options - -You need to create a `diesel.toml` file in the root folder to specify to diesel its configuration. You can use the `diesel.toml.dist` as a simple example of the configuration file. - -### Configure LND connection - -The current project uses LND server to handle Lightning network. - -In a `.env` file in the root folder of the project, You'll need to provide : - -- `LND_ADDRESS` : The address to reach the LND server -- `LND_CERTFILE_PATH` : the ssl certification file of your LND server -- `LND_MACAROON_PATH` : The macaroon that will allow the rocket server to connect to your LND server. - -**Note that the current project requires a macaroon with at least invoice write/read access.** - -You can use the `.env.dist` file as a template for that. - -``` -LND_ADDRESS="https://umbrel.local:10009" -LND_CERTFILE_PATH="path/to/the/lnd.cert" -LND_MACAROON_PATH="path/to/the/invoice.macaroon" -``` - -## Available requests - -### Mutation - -There is currently a single mutation available through the API, that allows you to create a post. Note that there is no restriction (like user guard) to create posts. - -Request : - -````graphql -mutation { - createPost(post: { - title: "Ad lorem ipsum", - excerpt: "This is a short description of the post", - content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed laoreet suscipit ullamcorper. Etiam sit amet justo dapibus, elementum magna sit amet, faucibus risus. Nullam at augue in quam tristique posuere. Nullam congue dignissim odio non sagittis. Sed in libero erat. Maecenas dictum blandit purus. Suspendisse eget sem suscipit, auctor risus in, ornare orci. Curabitur id facilisis nisl, vitae interdum libero. Aenean commodo nulla sit amet arcu consectetur, non tristique purus elementum. Sed ex sem, blandit eleifend fringilla ac, sagittis auctor ipsum.", - published: true, - price: 100 - }){ - title - excerpt - price - } -} -```` - -will return: - -````json -{ - "data": { - "createPost": { - "title": "Ad lorem ipsum", - "excerpt": "This is a short description of the post", - "price": 100 - } - } -} -```` -### Queries - -#### **Get posts list** - -Request : -```graphql -{ - getPostsList{ - uuid - title - excerpt - price - } -} -``` - -will return something like : - -```json -{ - "data": { - "getPostsList": [ - { - "uuid": "9f3711b4-f733-4911-9863-0c4ee575ca10", - "title": "ad lorem ipsum", - "excerpt": "alea jacta est", - "price": 100 - }, - { - "uuid": "e07677d1-4a45-422e-ac9b-a3a39cd91f0c", - "title": "Ad lorem ipsum", - "excerpt": "This is a short description of the post", - "price": 100 - } - ] - } -} -``` -#### **Get a single post** - -This is the query where the paywall and most of the LN Network interaction is applied. - -You'll find the code block that handles the paywall [here](https://github.com/Asone/graphqln/blob/master/src/graphql/query.rs#L40) - -The request takes an object with two fields : -- The post uuid -- The payment request that should allow the server to identify the access to the content has been paid. This field is optional, and if not provided, the api will respond with an error providing an invoice. - - -For example, providing the request like this : - -```graphql -{ - getPost(post:{ - uuid: "9f3711b4-f733-4911-9863-0c4ee575ca10" - }){ - uuid - title - excerpt - content - price - } -} -``` - -You'll get a response similar to this : - -```json -{ - "data": null, - "errors": [ - { - "message": "Payable post. Payment not found.. Use provided payment request.", - "locations": [ - { - "line": 2, - "column": 3 - } - ], - "path": [ - "getPost" - ], - "extensions": { - "payment_request": "lnbc1u1pshhszcpp5e3wpuwldl92zumajqs58k69stru6g9rc43nw0v7uy5rnk8vl7f0sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5ggd3dps9r27dcmxtmj463uct653n2agqttmjhm3qw6wgkfzaqw9s9qyyssq8zga2evqh8lt7kv40269puz3xehezxqvauhz4he0zvyke0x642q33jy85za4qtwa5p24x0vh5ve2p5ztqw64mlpsuwj5ml3ke8rl67spzzhwhv", - "r_hash": "cc5c1e3bedf9542e6fb204287b68b058f9a41478ac66e7b3dc25073b1d9ff25f" - } - } - ] -} -``` - -Note the [extensions](https://github.com/graphql/graphql-spec/blame/main/spec/Section%207%20--%20Response.md#L201-L204) sub-object which are part of the graphQL spec. -It provides a `payment_request` which will allow user to pay to access the content. - -The user can try to refetch the content providing the same `payment_request` within the request. - -```graphql -{ - getPost(post:{ - uuid: "9f3711b4-f733-4911-9863-0c4ee575ca10", - paymentRequest: "lnbc1u1pshhszcpp5e3wpuwldl92zumajqs58k69stru6g9rc43nw0v7uy5rnk8vl7f0sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5ggd3dps9r27dcmxtmj463uct653n2agqttmjhm3qw6wgkfzaqw9s9qyyssq8zga2evqh8lt7kv40269puz3xehezxqvauhz4he0zvyke0x642q33jy85za4qtwa5p24x0vh5ve2p5ztqw64mlpsuwj5ml3ke8rl67spzzhwhv" - }){ - uuid - title - excerpt - content - price - } -} -``` - -When provided, the server will check the invoice state for the provided `payment_request` and its local association with the requested post to ensure the user can access the content. - -A few cases can happen. - -If user requests with another payment request that is not the one provided, it will get a similar response to this : - -```json -{ - "data": null, - "errors": [ - { - "message": "No recorded payment request related to the requested post found with the payment requested provided. Proceed to payment with the provided request payment", - "locations": [ - { - "line": 2, - "column": 3 - } - ], - "path": [ - "getPost" - ], - "extensions": { - "payment_request": "lnbc1u1pshh3vypp5zyzf88glqv7gkuvqn95j97nzlt32xk8c9tu8t8dzywyax0vdw72sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5275n2cjqjgly9trkmucfux3krxw9z5na7wjmtklvua0j8tsw0pts9qyyssqw5kcm6r0zp4d5uu0p9zq5ehx9zen4svt63tvj20pa5kwfevv3p7x863f7mz4spa4w6p326jkegjq3gwtf8jzzr72nukyn8aw2s2gayqppmrhmq", - "r_hash": "1104939d1f033c8b7180996922fa62fae2a358f82af8759da22389d33d8d7795" - } - } - ] -} -``` - -Note how the server provides automatically a new invoice so the front-end can provide it to the user. - -If user requests the data without paying but providing the payment request he will get something similar to this : - -```json -{ - "data": null, - "errors": [ - { - "message": "Awaiting for payment to be done.", - "locations": [ - { - "line": 2, - "column": 3 - } - ], - "path": [ - "getPost" - ] - } - ] -} -``` - -If invoice expired, user will get something similar to this : - - -```json -{ - "data": null, - "errors": [ - { - "message": "Payment expired or canceled. Proceed to payment with the provided request payment", - "locations": [ - { - "line": 2, - "column": 3 - } - ], - "path": [ - "getPost" - ], - "extensions": { - "payment_request": "lnbc1u1pshh3vypp5zyzf88glqv7gkuvqn95j97nzlt32xk8c9tu8t8dzywyax0vdw72sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5275n2cjqjgly9trkmucfux3krxw9z5na7wjmtklvua0j8tsw0pts9qyyssqw5kcm6r0zp4d5uu0p9zq5ehx9zen4svt63tvj20pa5kwfevv3p7x863f7mz4spa4w6p326jkegjq3gwtf8jzzr72nukyn8aw2s2gayqppmrhmq", - "r_hash": "1104939d1f033c8b7180996922fa62fae2a358f82af8759da22389d33d8d7795" - } - } - ] -} -``` - -Note how the server regenerates automatically a new invoice to be provided to the user. - -Finally, once the user has paid the invoice, he will get the content with a response similar to this : - -```json -{ - "data": { - "getPost": { - "uuid": "9f3711b4-f733-4911-9863-0c4ee575ca10", - "title": "ad lorem ipsum", - "excerpt": "alea jacta est", - "content": "ad lorem ipsum dolor sit amet fluctuat nec mergitur rosa rosae rosam", - "price": 100 - } - } -} -``` - -Note that the user can reuse the payment request as many times as he wants as -we do store the association invoice - content and server will check to the LND instance that the invoice is settled. +The project is still under development and lacks tests. +## Features +- User authentication protected mutations +- API paywall over Lightning +- Data query paywall over lightning +## Documentation +An extended documentation is provided in the `docs` folder to help you understand how to install, configure and run the server : +- [Installation](./docs/installation.md) +- [Configuration](./docs/configuration.md) +- [Paywall](./docs/paywall.md) ## Main dependencies The project reliess on many dependencies to build and distribute the API. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..c648e95 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,41 @@ +## Set-up the project + +In order to be able to launch the project you need to set-up a few configurations. Once all set-up, you shall be able to launch the server with + +``` +cargo run +``` + +You'll find then a [graphiQL](https://github.com/graphql/graphiql) interface on [http://localhost:8000](http://localhost:8000) +### Configure Database connection + +The current project uses [postgres]() as database engine. +To set-up the connection copy the `rocket.toml.dist` file as `rocket.toml` and fill the connection URL as mentioned. + +``` +main_db = { url = "postgres://:@/"} +``` + +### Configure diesel options + +You need to create a `diesel.toml` file in the root folder to specify to diesel its configuration. You can use the `diesel.toml.dist` as a simple example of the configuration file. + +### Configure LND connection + +The current project uses LND server to handle Lightning network. + +In a `.env` file in the root folder of the project, You'll need to provide : + +- `LND_ADDRESS` : The address to reach the LND server +- `LND_CERTFILE_PATH` : the ssl certification file of your LND server +- `LND_MACAROON_PATH` : The macaroon that will allow the rocket server to connect to your LND server. + +**Note that the current project requires a macaroon with at least invoice write/read access.** + +You can use the `.env.dist` file as a template for that. + +``` +LND_ADDRESS="https://umbrel.local:10009" +LND_CERTFILE_PATH="path/to/the/lnd.cert" +LND_MACAROON_PATH="path/to/the/invoice.macaroon" +``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..a97597e --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,264 @@ + +## Install project + +1. Clone the project : + +``` +git clone +``` + +2. go to the folder and install dependencies : +``` +cargo install --path . +``` + +## Available requests + +### Mutation + +There is currently a single mutation available through the API, that allows you to create a post. Note that there is no restriction (like user guard) to create posts. + +Request : + +````graphql +mutation { + createPost(post: { + title: "Ad lorem ipsum", + excerpt: "This is a short description of the post", + content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed laoreet suscipit ullamcorper. Etiam sit amet justo dapibus, elementum magna sit amet, faucibus risus. Nullam at augue in quam tristique posuere. Nullam congue dignissim odio non sagittis. Sed in libero erat. Maecenas dictum blandit purus. Suspendisse eget sem suscipit, auctor risus in, ornare orci. Curabitur id facilisis nisl, vitae interdum libero. Aenean commodo nulla sit amet arcu consectetur, non tristique purus elementum. Sed ex sem, blandit eleifend fringilla ac, sagittis auctor ipsum.", + published: true, + price: 100 + }){ + title + excerpt + price + } +} +```` + +will return: + +````json +{ + "data": { + "createPost": { + "title": "Ad lorem ipsum", + "excerpt": "This is a short description of the post", + "price": 100 + } + } +} +```` +### Queries + +#### **Get posts list** + +Request : +```graphql +{ + getPostsList{ + uuid + title + excerpt + price + } +} +``` + +will return something like : + +```json +{ + "data": { + "getPostsList": [ + { + "uuid": "9f3711b4-f733-4911-9863-0c4ee575ca10", + "title": "ad lorem ipsum", + "excerpt": "alea jacta est", + "price": 100 + }, + { + "uuid": "e07677d1-4a45-422e-ac9b-a3a39cd91f0c", + "title": "Ad lorem ipsum", + "excerpt": "This is a short description of the post", + "price": 100 + } + ] + } +} +``` +#### **Get a single post** + +This is the query where the paywall and most of the LN Network interaction is applied. + +You'll find the code block that handles the paywall [here](https://github.com/Asone/graphqln/blob/master/src/graphql/query.rs#L40) + +The request takes an object with two fields : +- The post uuid +- The payment request that should allow the server to identify the access to the content has been paid. This field is optional, and if not provided, the api will respond with an error providing an invoice. + + +For example, providing the request like this : + +```graphql +{ + getPost(post:{ + uuid: "9f3711b4-f733-4911-9863-0c4ee575ca10" + }){ + uuid + title + excerpt + content + price + } +} +``` + +You'll get a response similar to this : + +```json +{ + "data": null, + "errors": [ + { + "message": "Payable post. Payment not found.. Use provided payment request.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "getPost" + ], + "extensions": { + "payment_request": "lnbc1u1pshhszcpp5e3wpuwldl92zumajqs58k69stru6g9rc43nw0v7uy5rnk8vl7f0sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5ggd3dps9r27dcmxtmj463uct653n2agqttmjhm3qw6wgkfzaqw9s9qyyssq8zga2evqh8lt7kv40269puz3xehezxqvauhz4he0zvyke0x642q33jy85za4qtwa5p24x0vh5ve2p5ztqw64mlpsuwj5ml3ke8rl67spzzhwhv", + "r_hash": "cc5c1e3bedf9542e6fb204287b68b058f9a41478ac66e7b3dc25073b1d9ff25f" + } + } + ] +} +``` + +Note the [extensions](https://github.com/graphql/graphql-spec/blame/main/spec/Section%207%20--%20Response.md#L201-L204) sub-object which are part of the graphQL spec. +It provides a `payment_request` which will allow user to pay to access the content. + +The user can try to refetch the content providing the same `payment_request` within the request. + +```graphql +{ + getPost(post:{ + uuid: "9f3711b4-f733-4911-9863-0c4ee575ca10", + paymentRequest: "lnbc1u1pshhszcpp5e3wpuwldl92zumajqs58k69stru6g9rc43nw0v7uy5rnk8vl7f0sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5ggd3dps9r27dcmxtmj463uct653n2agqttmjhm3qw6wgkfzaqw9s9qyyssq8zga2evqh8lt7kv40269puz3xehezxqvauhz4he0zvyke0x642q33jy85za4qtwa5p24x0vh5ve2p5ztqw64mlpsuwj5ml3ke8rl67spzzhwhv" + }){ + uuid + title + excerpt + content + price + } +} +``` + +When provided, the server will check the invoice state for the provided `payment_request` and its local association with the requested post to ensure the user can access the content. + +A few cases can happen. + +If user requests with another payment request that is not the one provided, it will get a similar response to this : + +```json +{ + "data": null, + "errors": [ + { + "message": "No recorded payment request related to the requested post found with the payment requested provided. Proceed to payment with the provided request payment", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "getPost" + ], + "extensions": { + "payment_request": "lnbc1u1pshh3vypp5zyzf88glqv7gkuvqn95j97nzlt32xk8c9tu8t8dzywyax0vdw72sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5275n2cjqjgly9trkmucfux3krxw9z5na7wjmtklvua0j8tsw0pts9qyyssqw5kcm6r0zp4d5uu0p9zq5ehx9zen4svt63tvj20pa5kwfevv3p7x863f7mz4spa4w6p326jkegjq3gwtf8jzzr72nukyn8aw2s2gayqppmrhmq", + "r_hash": "1104939d1f033c8b7180996922fa62fae2a358f82af8759da22389d33d8d7795" + } + } + ] +} +``` + +Note how the server provides automatically a new invoice so the front-end can provide it to the user. + +If user requests the data without paying but providing the payment request he will get something similar to this : + +```json +{ + "data": null, + "errors": [ + { + "message": "Awaiting for payment to be done.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "getPost" + ] + } + ] +} +``` + +If invoice expired, user will get something similar to this : + + +```json +{ + "data": null, + "errors": [ + { + "message": "Payment expired or canceled. Proceed to payment with the provided request payment", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "getPost" + ], + "extensions": { + "payment_request": "lnbc1u1pshh3vypp5zyzf88glqv7gkuvqn95j97nzlt32xk8c9tu8t8dzywyax0vdw72sdpagaexzurg29xyugzp2pyjqur0wd6zqcn40ysr5grpvssxcmmjv4kjq6tswd6k6cqzpgxqyz5vqsp5275n2cjqjgly9trkmucfux3krxw9z5na7wjmtklvua0j8tsw0pts9qyyssqw5kcm6r0zp4d5uu0p9zq5ehx9zen4svt63tvj20pa5kwfevv3p7x863f7mz4spa4w6p326jkegjq3gwtf8jzzr72nukyn8aw2s2gayqppmrhmq", + "r_hash": "1104939d1f033c8b7180996922fa62fae2a358f82af8759da22389d33d8d7795" + } + } + ] +} +``` + +Note how the server regenerates automatically a new invoice to be provided to the user. + +Finally, once the user has paid the invoice, he will get the content with a response similar to this : + +```json +{ + "data": { + "getPost": { + "uuid": "9f3711b4-f733-4911-9863-0c4ee575ca10", + "title": "ad lorem ipsum", + "excerpt": "alea jacta est", + "content": "ad lorem ipsum dolor sit amet fluctuat nec mergitur rosa rosae rosam", + "price": 100 + } + } +} +``` + +Note that the user can reuse the payment request as many times as he wants as +we do store the association invoice - content and server will check to the LND instance that the invoice is settled. + diff --git a/docs/paywall.md b/docs/paywall.md new file mode 100644 index 0000000..eeffd46 --- /dev/null +++ b/docs/paywall.md @@ -0,0 +1,39 @@ +# Paywall + +## Principles + +The paywall acts like a bridge beetween a payment network - Lightning network - and the +access rights to a file or a content. + +The bridge can be implemented on different levels. + + +## Globally protected API + +The most basic paywall mechanism is to protect a whole API behind a proof of access. + +It is not implemented on this project. + +## Route protected API + +In this case the client must provide a token - the payment request - to access the endpoint of the API. + +The client uses the headers to provide the token. + +if no token or an invalid one is provided, the client should be redirected to an HTTP 402 response, providing an invoice - the payment request - to the client. + +When the payment request is provided by the client, the server reads the invoice settlement state to provide access to the content. + +## GraphQL query protected API + +On certain queries of a graphQL the client must provide a payment_request value to access the response of the API. + +The client calls the query which will provide the invoice - the payment request - to the client the the graphQL errors property. + +When the payment request is provided as query's input, the server matches resource payment registry and reads the invoice settlement matching it. + +if no token is provided, or an invalid one, +the client will be provided an invoice - the payment request - + + +## GraphQL field protected API \ No newline at end of file diff --git a/juniper_rocket_multipart_handler/Cargo.toml b/juniper_rocket_multipart_handler/Cargo.toml deleted file mode 100644 index 9dc25f3..0000000 --- a/juniper_rocket_multipart_handler/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "juniper_rocket_multipart_handler" -version = "0.0.1" -description = "A juniper multipart handler for Rocket web server" -authors = ["Nelson Herbin "] -edition = "2018" -license = "CC0-1.0" -repository = "https://github.com/Asone/graphQLN" -homepage = "https://github.com/Asone/graphQLN" - -[dependencies] -multer = "2.0.2" -rocket = "0.5.0-rc.1" -juniper = "0.15.7" -serde = "1.0.136" -serde_json = "1.0.68" -juniper_rocket = "0.8.0" - -[dependencies.tokio-util] -version = "0.7.1" \ No newline at end of file diff --git a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs b/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs deleted file mode 100644 index d23d7de..0000000 --- a/juniper_rocket_multipart_handler/src/graphql_upload_wrapper.rs +++ /dev/null @@ -1,332 +0,0 @@ -use multer::{bytes::Bytes, Multipart}; -use rocket::{ - data::{self, ByteUnit, FromData, ToByteUnit}, - form::Error, - http::{ContentType, Status}, - outcome::Outcome::{Failure, Forward, Success}, - route::Outcome, - Data, Request, -}; -use std::sync::Arc; -use std::{collections::HashMap, env, path::PathBuf}; - -use juniper::{ - http::{self, GraphQLBatchRequest}, - DefaultScalarValue, ScalarValue, -}; - -use crate::graphql_upload_operations_request::GraphQLOperationsRequest; - -use crate::temp_file::TempFile; - -// This shall be deferable to env -const BODY_LIMIT: u64 = 1024 * 100; - -#[derive(Debug)] -pub enum MultipartFormParsingError { - BoundaryParsingError, -} - -enum ProcessorType { - JSON, - GRAPHQL, - MULTIPART, - UKNOWN, -} - -/// A Wrapper that handles HTTP GraphQL requests for [`Rocket`](https://rocket.rs/) -/// with multipart/form-data support that follows the Apollo's -/// [`unofficial specification`](https://github.com/jaydenseric/graphql-multipart-request-spec.). -/// -/// # Main concept -/// -/// The current struct is nothing more than a wrapper -/// that will return two fields : -/// - `operations` : Contains the graphQL Request to be executed. -/// This object is nothing more than a replica -/// of the original [`GraphQLRequest`](https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/src/lib.rs#L64) -/// object. -/// - `files` : An optional HashMap that contains the buffered files data. -/// -/// Note that the wrapper also replicates original juniper_rocket [`GraphQLRequest`] -/// parsing behavior for `application/json` and `application/graphql` requests. -/// -/// ## How to use -/// -/// You can load the GraphQLUploadWrapper the same way -/// you load the GraphQLRequest as both are data guards. -/// The main difference will be that instead, you'll call the -/// execution of the query through the `operations` property -/// of the wrapper. -/// -/// Below is basic example : -/// -/// ``` -/// #[rocket::post("/upload", data = "")] -/// pub async fn upload<'r>( -/// request: GraphQLUploadWrapper, -/// schema: &State, -/// ) -> GraphQLResponse { -/// request.operations.execute(&*schema, &Context).await -/// } -/// ``` -/// -/// ## Fetching the uploaded files -/// -/// In order to fetch the uploaded files -/// You'll need to implement your own context object -/// That will pass the buffered files to your execution methods. -/// -/// Example : -/// ``` -/// struct Ctx{ -/// files: Option> -/// }; -/// impl juniper::Context for Ctx {} -/// ``` -/// -/// You'll then be able to inject the buffered files to your -/// operations like this : -/// ``` -/// struct Ctx{ files: Option> }; -/// impl juniper::Context for Ctx {} -/// -/// #[rocket::post("/upload", data = "")] -/// pub async fn upload<'r>( -/// request: GraphQLUploadWrapper, -/// schema: &State, -/// ) -> GraphQLResponse { -/// request.operations.execute(&*schema, &Ctx{ files: request.files }).await -/// } -/// ``` -/// -/// ## Notes about processed files -/// -/// The Wrapper does nothing special with the uploaded files aside -/// allocating them in heap memory through the Hashmap which means -/// they won't be stored anywhere, not even in a temporary folder, -/// unless you decide to. -/// -/// See [`TempFile`] for more available data and information around uploaded files. -#[derive(Debug, PartialEq)] -pub struct GraphQLUploadWrapper -where - S: ScalarValue, -{ - pub operations: GraphQLOperationsRequest, - pub files: Option>, -} - -impl GraphQLUploadWrapper -where - S: ScalarValue, -{ - // Retrieves files - pub async fn get_files(&self) -> &Option> { - &self.files - } - - // Body reader for application/json content type. - // This method replicates the original handler from juniper_rocket - async fn from_json_body<'r>(data: Data<'r>) -> Result, Outcome<'r>> { - use rocket::tokio::io::AsyncReadExt as _; - - let mut reader = data.open(BODY_LIMIT.bytes()); - let mut body = String::new(); - let reader_result = reader.read_to_string(&mut body).await; - match reader_result { - Ok(_) => match serde_json::from_str(&body) { - Ok(req) => Ok(req), - Err(e) => Err(Failure(Status::BadRequest)), - }, - Err(e) => Err(Failure(Status::BadRequest)), - } - } - - // Body reader for application/graphql content type. - // This method replicates the original handler from juniper_rocket - async fn from_graphql_body<'r>(data: Data<'r>) -> Result, Outcome<'r>> { - use rocket::tokio::io::AsyncReadExt as _; - - let mut reader = data.open(BODY_LIMIT.bytes()); - let mut body = String::new(); - let reader_result = reader.read_to_string(&mut body).await; - match reader_result { - Ok(_) => Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( - body, None, None, - ))), - Err(e) => Err(Failure(Status::BadRequest)), - } - } - - /// Body reader for multipart/form-data content type. - async fn from_multipart_body<'r>( - data: Data<'r>, - content_type: &ContentType, - file_limit: ByteUnit, - ) -> Result<(GraphQLBatchRequest, Option>), Error<'r>> { - // Builds a void query for development - let mut query: String = String::new(); - let mut map: String = String::new(); - let boundary = Self::get_boundary(content_type).unwrap(); - - // Create and read a datastream from the request body - let reader = data.open(file_limit); - let stream = tokio_util::io::ReaderStream::new(reader); - - // Create a multipart object based on multer - let mut multipart = Multipart::new(stream, boundary); - - let mut files_map: HashMap = HashMap::new(); - - // Iterate on the form fields, which can be - // either text content or binary. - while let Some(entry) = multipart.next_field().await.unwrap() { - let field_name = match entry.name() { - Some(name) => Arc::<&str>::from(name).to_string(), - None => continue, - }; - - let name = field_name; - - let path = format!("{}", env::temp_dir().display()); - - match entry.content_type().as_ref() { - Some(_) => { - let file_name = match entry.file_name() { - Some(filename) => Arc::<&str>::from(filename).to_string(), - None => continue, - }; - - let content = match entry.bytes().await { - Ok(d) => d, - Err(_) => Bytes::new(), - }; - - let mut tmpfile = TempFile { - name: file_name, - size: Some(content.len()), - local_path: PathBuf::from(&path), - content: content, - }; - - files_map.insert(name, tmpfile); - } - None => { - let content_data = entry.text().await; - - // If field name is operations which should be the graphQL Query - // according to spec - match content_data { - Ok(result) => { - if name.as_str() == "operations" { - query = result; - continue; - } - - // if name.as_str() == "map" { - // map = result; - // } - } - Err(_) => continue, - }; - } - } - } - - // Default parser - match serde_json::from_str(&query) { - Ok(req) => Ok((req, Some(files_map))), - Err(_) => Err(rocket::form::Error::validation( - "The provided request could not be parsed.", - )), - } - } - - /// Returns an enum value for a specific processors based on request Content-type - fn get_processor_type(content_type: &ContentType) -> ProcessorType { - let top = content_type.top().as_str(); - let sub = content_type.sub().as_str(); - - match (top, sub) { - ("application", "json") => ProcessorType::JSON, - ("application", "graphql") => ProcessorType::GRAPHQL, - ("multipart", "form-data") => ProcessorType::MULTIPART, - _ => ProcessorType::UKNOWN, - } - } - - /// Extracts the boundary for a multipart/form-data request - pub fn get_boundary(content_type: &ContentType) -> Result<&str, MultipartFormParsingError> { - match content_type.params().find(|&(k, _)| k == "boundary") { - Some(s) => Ok(s.1), - None => Err(MultipartFormParsingError::BoundaryParsingError), - } - } -} - -struct Config {} - -#[rocket::async_trait] -impl<'r, S> FromData<'r> for GraphQLUploadWrapper -where - S: ScalarValue, -{ - type Error = String; - async fn from_data( - req: &'r Request<'_>, - data: Data<'r>, - ) -> data::Outcome<'r, Self, Self::Error> { - // Get content-type of HTTP request - let content_type = req.content_type().unwrap(); - - // Split content-type value as a tuple of str - - match Self::get_processor_type(content_type) { - ProcessorType::JSON => { - Box::pin(async move { - match Self::from_json_body(data).await { - Ok(result) => Success(GraphQLUploadWrapper { - operations: GraphQLOperationsRequest(result), - files: None, - }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), - } - // Success(Self {}) - }) - .await - } - ProcessorType::GRAPHQL => { - Box::pin(async move { - match Self::from_graphql_body(data).await { - Ok(result) => Success(GraphQLUploadWrapper { - operations: GraphQLOperationsRequest(result), - files: None, - }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), - } - }) - .await - } - ProcessorType::MULTIPART => { - Box::pin(async move { - match Self::from_multipart_body( - data, - content_type, - req.limits().get("data-form").unwrap(), - ) - .await - { - Ok(result) => Success(GraphQLUploadWrapper { - operations: GraphQLOperationsRequest(result.0), - files: result.1, - }), - Err(error) => Failure((Status::BadRequest, format!("{}", error))), - } - }) - .await - } - ProcessorType::UKNOWN => Box::pin(async move { Forward(data) }).await, - } - } -} diff --git a/juniper_rocket_multipart_handler/src/temp_file.rs b/juniper_rocket_multipart_handler/src/temp_file.rs deleted file mode 100644 index 2641629..0000000 --- a/juniper_rocket_multipart_handler/src/temp_file.rs +++ /dev/null @@ -1,97 +0,0 @@ -use multer::bytes::Bytes; -use std::fs; -use std::io::Error; -use std::{fs::File, io::Write, path::PathBuf}; - -/// A buffered file from file upload. -/// -/// Please, **do note** that this struct -/// is different from original [`TempFile`] provided -/// by original's rocket implementation as it does not -/// intend to rely on a specific lifetime specification -/// other than the graphQL request processing lifetime. -/// -/// Current [`TempFile`] struct provides with a `persist_file` -/// method that will write the file on the filesystem based on the -/// `local_path` of the struct. -/// -/// default `local_path` provided is based on [`env::temp_dir()`](https://doc.rust-lang.org/nightly/std/env/fn.temp_dir.html) value -#[derive(Debug, PartialEq, Clone)] -pub struct TempFile { - pub local_path: PathBuf, - pub name: String, - pub size: Option, - pub content: Bytes, -} - -impl TempFile { - /// Gets the local path where file should be persisted - pub fn get_local_path(&self) -> &PathBuf { - &self.local_path - } - - /// Sets the local path to be used for persisting - pub fn set_local_path(&mut self, path: PathBuf) -> &PathBuf { - self.local_path = path; - &self.local_path - } - - /// Gets the file name - pub fn get_name(&self) -> &str { - &self.name - } - - /// Gets the file size - pub fn get_size(&self) -> &Option { - &self.size - } - - /// Gets the file content - pub fn get_content(&self) -> &Bytes { - &self.content - } - - fn path_checker(path: &str) -> Result<(), Error> { - fs::create_dir_all(path) - } - - /// Persists the file to the local_path property - pub fn persist_file(&self) -> Result<&PathBuf, Error> { - let full_path = format!("{}/{}", &self.local_path.to_str().unwrap(), &self.name); - let file = File::create(full_path); - - match file { - Ok(mut file) => { - let result = file.write_all(&self.content); - match result { - Ok(_) => Ok(&self.local_path), - Err(error) => Err(error), - } - } - Err(error) => Err(error), - } - } - - /// persists file to a given location - pub fn persist_to(&self, path: &str) -> Result { - match Self::path_checker(path) { - Ok(_) => { - let full_path = format!("{}/{}", path, &self.name); - let path_buf = PathBuf::from(&full_path); - let file = File::create(&full_path); - - match file { - Ok(mut file) => { - let result = file.write_all(&self.content); - match result { - Ok(_) => Ok(path_buf), - Err(error) => Err(error), - } - } - Err(error) => Err(error), - } - } - Err(error) => Err(error), - } - } -} diff --git a/migrations/2022-04-16-153823_create_media/down.sql b/migrations/2022-04-16-153823_create_media/down.sql new file mode 100644 index 0000000..8333bd2 --- /dev/null +++ b/migrations/2022-04-16-153823_create_media/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE "media"; \ No newline at end of file diff --git a/migrations/2022-04-16-153823_create_media/up.sql b/migrations/2022-04-16-153823_create_media/up.sql new file mode 100644 index 0000000..3acd7e4 --- /dev/null +++ b/migrations/2022-04-16-153823_create_media/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here-- Your SQL goes here +CREATE TABLE IF NOT EXISTS "media" ( + "uuid" uuid UNIQUE PRIMARY KEY NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "absolute_path" TEXT NOT NULL, + "price" INT NOT NULL DEFAULT 0, + "published" boolean NOT NULL DEFAULT false, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/2022-04-21-063037_create_media_payment/down.sql b/migrations/2022-04-21-063037_create_media_payment/down.sql new file mode 100644 index 0000000..4eccb48 --- /dev/null +++ b/migrations/2022-04-21-063037_create_media_payment/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE "media_payment"; \ No newline at end of file diff --git a/migrations/2022-04-21-063037_create_media_payment/up.sql b/migrations/2022-04-21-063037_create_media_payment/up.sql new file mode 100644 index 0000000..273eede --- /dev/null +++ b/migrations/2022-04-21-063037_create_media_payment/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS "media_payment" ( + "uuid" uuid UNIQUE NOT NULL, + "request" text UNIQUE NOT NULL, + "state" text, + "hash" TEXT UNIQUE NOT NULL, + "media_uuid" uuid references media(uuid) NOT NULL, + "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY( uuid ) +); \ No newline at end of file diff --git a/rocket.toml.dist b/rocket.toml.dist index 637b049..41410ba 100644 --- a/rocket.toml.dist +++ b/rocket.toml.dist @@ -1,3 +1,15 @@ [global.databases] -main_db = { url = "postgres://:@/"} \ No newline at end of file +main_db = { url = "postgres://:@/"} + + [default.limits] +data-form = "32 MiB" + +# Uncomment if you want to use SSL +#[default.tls] +#certs = "ssl/cert.pem" +#key = "ssl/cert-key.pem" + +[release] +log_level="normal" +secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 90ff02e..1afe4b3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,110 +2,54 @@ /// Todo : Split routes in different files /// use crate::{ - db::models::user_token::UserToken, - forms::login_user::LoginUser, graphql::{context::GQLContext, mutation::Mutation, query::Query}, guards::userguard::UserGuard, lnd::client::LndClient, }; use juniper_rocket_multipart_handler::graphql_upload_wrapper::GraphQLUploadWrapper; -use rocket::{ response::content, State }; +use rocket::State; pub type Schema = RootNode<'static, Query, Mutation, EmptySubscription>; - -use rocket::{ - form::{Form, Strict}, - http::{Cookie, Status} -}; use crate::db::PostgresConn; use crate::guards::paymentrequestheader::PaymentRequestHeader; use juniper::{EmptySubscription, RootNode}; use juniper_rocket::GraphQLResponse; -use rocket::http::CookieJar; -#[rocket::get("/")] -pub fn graphiql() -> content::Html { - juniper_rocket::graphiql_source("/graphql", None) -} /* This is a void handler that will return a 200 empty response for browsers that intends to check pre-flight for CORS rules. */ #[rocket::options("/graphql")] -pub async fn options_handler() {} +pub async fn graphql_options_handler() {} -/** - Calls the GraphQL API from a HTTP GET Request. - It does nothing special but a paywall mechanism through - a payment_request param could be implemented later. -*/ -#[rocket::get("/graphql?")] -pub async fn get_graphql_handler( - request: juniper_rocket::GraphQLRequest, - schema: &State, - db: PostgresConn, - user_guard: UserGuard, - lnd: LndClient, -) -> GraphQLResponse { - request - .execute( - &*schema, - &GQLContext { - pool: db, - lnd: lnd, - files: None, - user: user_guard.0, - }, - ) - .await -} +#[rocket::options("/auth")] +pub async fn auth_options_handler() {} /** Calls the API with a query specific paywall protected mechanism. */ #[rocket::post("/graphql", data = "")] pub async fn post_graphql_handler( - request: juniper_rocket::GraphQLRequest, + request: GraphQLUploadWrapper, schema: &State, db: PostgresConn, user_guard: UserGuard, lnd: LndClient, ) -> GraphQLResponse { request + .operations .execute( &*schema, &GQLContext { pool: db, lnd: lnd, - files: None, + files: request.files, user: user_guard.0, + server_config: None, }, ) .await } -/// Authentication route -#[rocket::post("/auth", data = "")] -pub async fn login( - db: PostgresConn, - cookies: &CookieJar<'_>, - user_form: Form>, -) -> rocket::http::Status { - let user = user_form.into_inner().into_inner(); - - let session = user.login(db).await; - - match session { - Ok(user_session) => { - let token = UserToken::generate_token(user_session).unwrap(); - let cookie = Cookie::build("session", token).finish(); - - cookies.add(cookie); - Status::Ok - } - Err(_) => Status::ExpectationFailed, - } -} - /// Calls the API through an API-scoped paywall #[rocket::post("/payable", data = "")] pub async fn payable_post_graphql_handler( @@ -124,6 +68,7 @@ pub async fn payable_post_graphql_handler( lnd: lnd, files: None, user: user_guard.0, + server_config: None, }, ) .await @@ -145,7 +90,8 @@ pub async fn upload<'r>( pool: db, lnd, files: request.files, - user: user_guard.0 + user: user_guard.0, + server_config: None, }, ) .await; diff --git a/src/catchers/payment_required.rs b/src/catchers/payment_required.rs index fb177a4..a075cc6 100644 --- a/src/catchers/payment_required.rs +++ b/src/catchers/payment_required.rs @@ -1,7 +1,7 @@ use crate::db::models::api_payment::ApiPayment; use crate::db::PostgresConn; use crate::lnd::client::LndClient; -use rocket::response::content::Json; +use rocket::response::content::RawJson; use rocket::response::status; use rocket::{catch, http::Status, Request}; @@ -13,7 +13,7 @@ use rocket::{catch, http::Status, Request}; pub async fn payment_required<'r>( _: Status, request: &'r Request<'_>, -) -> Result>, Status> { +) -> Result>, Status> { let pool = request.guard::().await.succeeded(); let lnd_client_result = request.guard::().await.succeeded(); @@ -31,7 +31,7 @@ pub async fn payment_required<'r>( Ok(payment_request) => { let json_state = format!(r#"{{"payment": {:?}}}"#, payment_request.request.as_str()); - Ok(status::Custom(Status::PaymentRequired, Json(json_state))) + Ok(status::Custom(Status::PaymentRequired, RawJson(json_state))) } Err(_) => Err(Status::InternalServerError), } diff --git a/src/cors.rs b/src/cors.rs index 9029844..66d148d 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -1,13 +1,27 @@ use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::Header; use rocket::{Request, Response}; +use std::env; +/// Represents a Cors Config object +/// that will be used to build Cors Policy on +/// Server runtime +struct CorsConfig { + allow_origin: String, + allow_methods: String, + allow_headers: String, + allow_credentials: String, +} + +/// +/// Allows us to modify CORS default parameters +/// pub struct Cors; -/** - Allows us to modify CORS default parameters -*/ #[rocket::async_trait] +/// +/// Implements the Cors Policy +/// impl Fairing for Cors { fn info(&self) -> Info { Info { @@ -16,13 +30,45 @@ impl Fairing for Cors { } } + // Fairing provides the cors policy on request response async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) { - response.set_header(Header::new("Access-Control-Allow-Origin", "*")); + // Gets the config + let config = Cors::get_config(); + + response.set_header(Header::new( + "Access-Control-Allow-Origin", + config.allow_origin, + )); response.set_header(Header::new( "Access-Control-Allow-Methods", - "POST, GET, PATCH, OPTIONS", + config.allow_methods, + )); + response.set_header(Header::new( + "Access-Control-Allow-Headers", + config.allow_headers, )); - response.set_header(Header::new("Access-Control-Allow-Headers", "*")); - response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); + response.set_header(Header::new( + "Access-Control-Allow-Credentials", + config.allow_credentials, + )); + } +} + +impl Cors { + // Creates a config with environment variables if set and default values for + // values not set in env variables + fn get_config() -> CorsConfig { + let origin_policy = env::var("CORS_ORIGIN_POLICY").unwrap_or("*".to_string()); + let allow_methods = + env::var("CORS_METHOD_POLICY").unwrap_or("POST, GET, PATCH, OPTIONS".to_string()); + let allow_headers = env::var("CORS_HEADERS_POLICY").unwrap_or("*".to_string()); + let allow_credentials = env::var("CORS_CREDENTIALS_POLICY").unwrap_or("false".to_string()); + + return CorsConfig { + allow_origin: origin_policy.to_owned(), + allow_methods: allow_methods.to_owned(), + allow_headers: allow_headers.to_owned(), + allow_credentials: allow_credentials.to_owned(), + }; } } diff --git a/src/db/igniter.rs b/src/db/igniter.rs new file mode 100644 index 0000000..30a610f --- /dev/null +++ b/src/db/igniter.rs @@ -0,0 +1,32 @@ +use std::env; + +use super::PostgresConn; +use rocket::{Build, Rocket}; +embed_migrations!(); +/// This method should be called on ignite of the server - juste before server launch -. +/// It calls the migrations scripts to populate the database with tables and default data. +/// Note that it is up to the SQL scripts in migrations to ensure that it does not override +/// previous existing tables or data when executed. +pub async fn run_db_migrations(rocket: Rocket) -> Result, Rocket> { + let conn = PostgresConn::get_one(&rocket) + .await + .expect("Database connection"); + + + + let flag = env::var("DATABASE_RUN_MIGRATIONS_ON_IGNITE").unwrap_or("false".to_string()).parse::(); + + // Skip + if flag.is_err() || !flag.unwrap() { + return Ok(rocket); + } + + conn.run(|conn| match embedded_migrations::run(&*conn) { + Ok(()) => Ok(rocket), + Err(e) => { + error!("Failed to run database migrations: {:?}", e); + Err(rocket) + } + }) + .await +} diff --git a/src/db/mod.rs b/src/db/mod.rs index b8e1d7e..21905c8 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,6 +2,7 @@ use rocket_sync_db_pools::{database, diesel}; use self::models::api_payment::ApiPayment; +pub mod igniter; pub mod models; pub mod schema; diff --git a/src/db/models/media.rs b/src/db/models/media.rs new file mode 100644 index 0000000..8f44a4d --- /dev/null +++ b/src/db/models/media.rs @@ -0,0 +1,73 @@ +pub use crate::db::schema::media; + +use crate::graphql::types::input::file::FileInput; +use chrono::NaiveDateTime; +use diesel; +use diesel::prelude::*; +use diesel::PgConnection; +use std::path::PathBuf; + +#[derive(Identifiable, Queryable, PartialEq, Debug)] +#[primary_key(uuid)] +#[table_name = "media"] +pub struct Media { + pub uuid: uuid::Uuid, + pub title: String, + pub description: Option, + pub absolute_path: String, + pub price: i32, + pub published: bool, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +#[derive(Debug, Insertable)] +#[table_name = "media"] +pub struct NewMedia { + pub uuid: uuid::Uuid, + pub title: String, + pub description: Option, + pub absolute_path: String, + pub published: bool, + pub price: i32, +} + +impl From<(&PathBuf, FileInput)> for NewMedia { + fn from(file_data: (&PathBuf, FileInput)) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + title: file_data.1.title, + description: file_data.1.description, + absolute_path: file_data.0.to_string_lossy().to_owned().to_string(), + price: file_data.1.price, + published: file_data.1.published, + } + } +} + +impl Media { + pub fn create(new_media: NewMedia, connection: &PgConnection) -> QueryResult { + use crate::db::schema::media::dsl::*; + + diesel::insert_into::(media) + .values(&new_media) + .get_result(connection) + } + + pub fn find_all_published(connection: &PgConnection) -> Vec { + use crate::db::schema::media::dsl::*; + media.filter(published.eq(true)).load(connection).unwrap() + } + + pub fn find_one_by_uuid( + media_uuid: uuid::Uuid, + connection: &PgConnection, + ) -> QueryResult> { + use crate::db::schema::media::dsl::*; + + media + .filter(uuid.eq(media_uuid)) + .first::(connection) + .optional() + } +} diff --git a/src/db/models/media_payment.rs b/src/db/models/media_payment.rs new file mode 100644 index 0000000..5bdfc84 --- /dev/null +++ b/src/db/models/media_payment.rs @@ -0,0 +1,65 @@ +pub use crate::db::schema::media_payment; +use crate::lnd::invoice::LndInvoice; +use chrono::NaiveDateTime; +use diesel; +use diesel::prelude::*; +use diesel::PgConnection; +use uuid::Uuid; + +#[derive(Queryable, PartialEq, Associations, Debug)] +#[table_name = "media_payment"] +#[belongs_to(parent = Media, foreign_key = "media_uuid")] +pub struct MediaPayment { + pub uuid: Uuid, + pub request: String, + pub state: Option, + pub hash: String, + pub media_uuid: Uuid, + pub expires_at: NaiveDateTime, +} + +#[derive(Debug, Insertable)] +#[table_name = "media_payment"] +pub struct NewMediaPayment { + uuid: Uuid, + hash: String, + request: String, + media_uuid: Uuid, + expires_at: NaiveDateTime, +} + +impl From<(LndInvoice, uuid::Uuid)> for NewMediaPayment { + fn from(data: (LndInvoice, uuid::Uuid)) -> Self { + Self { + uuid: Uuid::new_v4(), + hash: data.0.r_hash, + request: data.0.payment_request, + media_uuid: data.1.to_owned(), + expires_at: data.0.expires_at, + } + } +} + +impl MediaPayment { + pub fn find_one_by_request( + payment_request: String, + connection: &PgConnection, + ) -> QueryResult> { + use crate::db::schema::media_payment::dsl::*; + media_payment + .filter(request.eq(payment_request)) + .first::(connection) + .optional() + } + + pub fn create( + new_payment: NewMediaPayment, + connection: &PgConnection, + ) -> QueryResult { + use crate::db::schema::media_payment::dsl::*; + + diesel::insert_into::(media_payment) + .values(&new_payment) + .get_result(connection) + } +} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 7553841..31e7baa 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -1,4 +1,6 @@ pub mod api_payment; +pub mod media; +pub mod media_payment; pub mod payment; pub mod post; pub mod session; diff --git a/src/db/models/payment.rs b/src/db/models/payment.rs index b882e1f..73dd941 100644 --- a/src/db/models/payment.rs +++ b/src/db/models/payment.rs @@ -1,4 +1,5 @@ pub use crate::db::schema::payment; +use crate::graphql::types::output::payment::PaymentType; use crate::lnd::invoice::LndInvoice; use chrono::NaiveDateTime; use diesel; @@ -26,6 +27,7 @@ pub struct NewPayment { request: String, post_uuid: Uuid, expires_at: NaiveDateTime, + state: Option, } impl From<(LndInvoice, uuid::Uuid)> for NewPayment { @@ -36,6 +38,7 @@ impl From<(LndInvoice, uuid::Uuid)> for NewPayment { request: data.0.payment_request, post_uuid: data.1, expires_at: data.0.expires_at, + state: Some(PaymentType::state_from_invoice_state(data.0.state)), } } } diff --git a/src/db/schema.rs b/src/db/schema.rs index 279d700..a4e6615 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -8,6 +8,30 @@ table! { } } +table! { + media (uuid) { + uuid -> Uuid, + title -> Text, + description -> Nullable, + absolute_path -> Text, + price -> Int4, + published -> Bool, + created_at -> Timestamptz, + updated_at -> Timestamptz, + } +} + +table! { + media_payment (uuid) { + uuid -> Uuid, + request -> Text, + state -> Nullable, + hash -> Text, + media_uuid -> Uuid, + expires_at -> Timestamptz, + } +} + table! { payment (uuid) { uuid -> Uuid, @@ -52,7 +76,16 @@ table! { } } +joinable!(media_payment -> media (media_uuid)); joinable!(payment -> post (post_uuid)); joinable!(session -> user (user_uuid)); -allow_tables_to_appear_in_same_query!(api_payment, payment, post, session, user,); +allow_tables_to_appear_in_same_query!( + api_payment, + media, + media_payment, + payment, + post, + session, + user, +); diff --git a/src/graphql/context.rs b/src/graphql/context.rs index 40ee8f9..2c138c8 100644 --- a/src/graphql/context.rs +++ b/src/graphql/context.rs @@ -1,10 +1,14 @@ use std::collections::HashMap; -use crate::{db::{PostgresConn, models::user::User}, lnd::client::LndClient}; +use crate::{ + db::{models::user::User, PostgresConn}, + lnd::client::LndClient, +}; use derive_more::Deref; use juniper_rocket_multipart_handler::temp_file::TempFile; -use tonic::codegen::InterceptedService; +// use tonic::codegen::InterceptedService; +use tonic_lnd::tonic::codegen::InterceptedService; use tonic_lnd::{rpc::lightning_client::LightningClient, MacaroonInterceptor}; /* @@ -22,6 +26,7 @@ pub struct GQLContext { pub lnd: LndClient, pub files: Option>, pub user: Option, + pub server_config: Option, } impl juniper::Context for GQLContext {} diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index a774c0c..a44c086 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,5 +1,6 @@ pub mod context; pub mod mutation; pub mod paywall_context; +pub mod queries; pub mod query; pub mod types; diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs index d2a82fe..d3d5364 100644 --- a/src/graphql/mutation.rs +++ b/src/graphql/mutation.rs @@ -1,13 +1,17 @@ use juniper::{FieldError, FieldResult}; use crate::db::models::{ + media::{Media, NewMedia}, post::{NewPost, Post}, user::User, }; use super::{ context::GQLContext, - types::{input::post::CreatePostInput, output::post::PostType}, + types::{ + input::{file::FileInput, post::CreatePostInput}, + output::{media::MediaType, post::PostType}, + }, }; pub struct Mutation; @@ -51,6 +55,68 @@ impl Mutation { } } + #[graphql(description = "Upload and stores a payable media onto the server")] + async fn upload_file<'a>( + context: &'a GQLContext, + file_input: FileInput, + ) -> FieldResult { + if Self::is_authenticated(&context.user) == false { + return Err(FieldError::new( + "You need to be authenticated to use this mutation", + graphql_value!(""), + )); + } + + let files_map = context.get_files(); + let connection = context.get_db_connection(); + + match files_map { + Some(files_map) => { + + if files_map.len() == 0 { + return Err(FieldError::new( + "Current mutation requires a single file for upload. No file provided", + graphql_value!("") + )) + } + + let file = files_map.into_iter().next(); + + match file { + Some(file) => { + let persisted_path = file.1.persist_file(); + + match persisted_path { + Ok(path) => { + let new_media = NewMedia::from((&path,file_input)); + let media = connection.run(move |c| Media::create(new_media,c)).await; + match media { + Ok(media) => Ok(MediaType::from(media)), + Err(_) => Err(FieldError::new( + "Error while persisting file. Aborting", + graphql_value!("") + )) + } + }, + Err(_) => Err(FieldError::new("Error while writing file on filesystem.", + graphql_value!("") + )) + } + + }, + None => Err(FieldError::new( + "Current mutation requires a single file for upload. No file provided", + graphql_value!("") + )) + } + }, + None => Err(FieldError::new( + "Current mutation accepts a single file for upload. Multiple files uploaded provided", + graphql_value!("") + )) + } + } + /// Changes password for current user async fn change_password<'a>(context: &'a GQLContext, password: String) -> FieldResult { if Self::is_authenticated(&context.user) == false { diff --git a/src/graphql/paywall_context.rs b/src/graphql/paywall_context.rs index 48a6bef..efa3f4f 100644 --- a/src/graphql/paywall_context.rs +++ b/src/graphql/paywall_context.rs @@ -2,7 +2,8 @@ use crate::{db::PostgresConn, lnd::client::LndClient}; use derive_more::Deref; -use tonic::codegen::InterceptedService; +// use tonic::codegen::InterceptedService; +use tonic_lnd::tonic::codegen::InterceptedService; use tonic_lnd::{rpc::lightning_client::LightningClient, MacaroonInterceptor}; /* diff --git a/src/graphql/queries/get_files_list.rs b/src/graphql/queries/get_files_list.rs new file mode 100644 index 0000000..1d80a52 --- /dev/null +++ b/src/graphql/queries/get_files_list.rs @@ -0,0 +1,14 @@ +use crate::db::models::media::Media; +use crate::graphql::context::GQLContext; +use crate::graphql::types::output::media::MediaType; +use juniper::FieldError; + +pub async fn get_files_list<'a>(context: &'a GQLContext) -> Result, FieldError> { + let connection = context.get_db_connection(); + let db_results = connection.run(move |c| Media::find_all_published(c)).await; + + Ok(db_results + .into_iter() + .map(|p| MediaType::from(p)) + .collect::>()) +} diff --git a/src/graphql/queries/get_media.rs b/src/graphql/queries/get_media.rs new file mode 100644 index 0000000..75c2e74 --- /dev/null +++ b/src/graphql/queries/get_media.rs @@ -0,0 +1,30 @@ +use crate::db::models::media::Media; +use crate::graphql::context::GQLContext; +use crate::graphql::types::output::media::MediaType; +use juniper::{FieldError, Value}; +use uuid::Uuid; + +pub async fn get_media<'a, 'b>( + context: &'a GQLContext, + uuid: Uuid, + payment_request: Option, +) -> Result { + let connection = context.get_db_connection(); + let result = connection + .run(move |c| Media::find_one_by_uuid(uuid, c)) + .await; + + match result { + Ok(result) => match result { + Some(media) => match payment_request { + Some(payment_request) => Ok(MediaType::from((media, payment_request))), + None => Ok(MediaType::from(media)), + }, + None => Err(FieldError::new( + "No media found with the provided Uuid", + Value::null(), + )), + }, + Err(_) => Err(FieldError::new("Error while fetching media", Value::null())), + } +} diff --git a/src/graphql/queries/get_post.rs b/src/graphql/queries/get_post.rs new file mode 100644 index 0000000..4d08faa --- /dev/null +++ b/src/graphql/queries/get_post.rs @@ -0,0 +1,84 @@ +use super::utils::QueryUtils; +use crate::db::models::payment::Payment; +use crate::graphql::types::input::post::PayablePostInput; +use crate::graphql::types::output::post::PostType; +use crate::lnd::invoice::InvoiceUtils; +use crate::{db::models::Post, graphql::context::GQLContext}; +use juniper::{FieldError, Value}; +use tonic_lnd::rpc::invoice::InvoiceState; + +pub async fn get_post<'a, 'b>( + context: &'a GQLContext, + post: PayablePostInput, +) -> Result { + let post_id: uuid::Uuid = post.uuid.clone(); + let connection = context.get_db_connection(); + + // Find the post in the database + let result = connection + .run(move |c| Post::find_one_by_id(post_id, c)) + .await; + + match result { + Some(r) => match r.published { + // Checks if post is published + true => match r.is_payable() { + // Checks if there should be a paywall ( price > 0 ) + true => match post.payment_request { + // If payable, ensure there's a payment_request provided + Some(payment_request) => { + // payment_request found + + // Search for payment entry based on the payment_request provided + let payment = connection + .run(move |c| Payment::find_one_by_request(payment_request.clone(), c)) + .await; + match payment { + Some(payment) => { // Payment found + + // Request LND invoice and checks the invoice state + match InvoiceUtils::get_invoice_state_from_payment_request(context.get_lnd_client(), payment.request).await { + Ok(invoice_result) => match invoice_result { + Some(invoice) => match invoice.state() { + InvoiceState::Settled => Ok(PostType::from(r)), // Payment has been done. Serves the post + InvoiceState::Open => Err(FieldError::new( + "Awaiting for payment to be done.", + graphql_value!({"state": "open"}), + )), // Payment hasn't been done yet. We shall wait for payment, so there's no need to regenerate an invoice + InvoiceState::Accepted => Err(FieldError::new( + "Payment ongoing but not settled yet", + graphql_value!({"state": "ongoing"}), + )), // Payment is on process onto the network but has not reach its receiver yet. We shall wait, so there's no need to regenerate an invoice + InvoiceState::Canceled => Err(QueryUtils::generate_invoiced_error(context,post_id,r,"Payment expired or canceled.").await), + }, + // LND Server says there's no invoice matching + None => Err(QueryUtils::generate_invoiced_error(context,post_id,r,"No invoice found for corresponding payment request. Proceed to payment with the provided request payment").await) + + }, + // Invoice is broken. Maybe we should serve a new invoice here ? + Err(_) => Err(FieldError::new( + "An error happened when trying to decode invoice", + Value::null(), + )), + } + }, + // Our DB does not contain any payment with the provided payment_request. + None => Err(QueryUtils::generate_invoiced_error(context,post_id,r,"No recorded payment request related to the requested post found with the payment requested provided.").await) + } + } + None => Err(QueryUtils::generate_invoiced_error( + context, + post_id, + r, + "Payable post. Payment not found.", + ) + .await), + }, + false => Ok(PostType::from(r)), // Post has a price of 0 (free), so we serve it without condition + }, + false => Err(FieldError::new("Post not found", Value::Null)), // Post not published + }, + // Post has not been found in DB + None => Err(FieldError::new("Post not found", Value::Null)), + } +} diff --git a/src/graphql/queries/get_posts_list.rs b/src/graphql/queries/get_posts_list.rs new file mode 100644 index 0000000..f8d0464 --- /dev/null +++ b/src/graphql/queries/get_posts_list.rs @@ -0,0 +1,15 @@ +use crate::graphql::types::output::post::PreviewPostType; +use crate::{db::models::Post, graphql::context::GQLContext}; +use juniper::FieldError; + +pub async fn get_posts_list<'a>( + context: &'a GQLContext, +) -> Result, FieldError> { + let connection = context.get_db_connection(); + let db_results = connection.run(move |c| Post::find_all_published(c)).await; + + Ok(db_results + .into_iter() + .map(|p| PreviewPostType::from(p)) + .collect::>()) +} diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs new file mode 100644 index 0000000..b7e9964 --- /dev/null +++ b/src/graphql/queries/mod.rs @@ -0,0 +1,7 @@ +pub mod get_files_list; +pub mod get_media; +pub mod get_post; +pub mod get_posts_list; +pub mod request_invoice_for_media; +pub mod request_invoice_for_post; +pub mod utils; diff --git a/src/graphql/queries/request_invoice_for_media.rs b/src/graphql/queries/request_invoice_for_media.rs new file mode 100644 index 0000000..41e2702 --- /dev/null +++ b/src/graphql/queries/request_invoice_for_media.rs @@ -0,0 +1,215 @@ +use crate::db::models::media::Media; +use crate::db::models::media_payment::MediaPayment; +use crate::db::models::media_payment::NewMediaPayment; +use crate::db::PostgresConn; +use crate::graphql::context::GQLContext; +use crate::graphql::types::output::invoices::MediaInvoice; +use crate::lnd::invoice::InvoiceParams; +use crate::lnd::invoice::InvoiceUtils; +use juniper::{FieldError, Value}; +use tonic::codegen::InterceptedService; +use tonic::transport::Channel; +use tonic_lnd::rpc::invoice::InvoiceState; +use tonic_lnd::MacaroonInterceptor; + +use tonic_lnd::rpc::lightning_client::LightningClient; + +/// Requests an invoice and/or its state for a media. +/// The request can get an optional `payment_request` +/// that if provided will have its validity checked. +pub async fn request_invoice_for_media<'a>( + context: &'a GQLContext, + uuid: uuid::Uuid, + payment_request: Option, +) -> Result { + let connection = context.get_db_connection(); + let client = context.get_lnd_client(); + + // Get media from db + let db_result = connection + .run(move |c| Media::find_one_by_uuid(uuid, c)) + .await; + + // db failure + if db_result.is_err() { + return Err(FieldError::new( + "Error while requesting database", + Value::Null, + )); + } + + // unwrap result if no db failure + let media = db_result.unwrap(); + + // Return error if there is no media found in db + if media.is_none() { + return Err(FieldError::new( + "No media found with provided uuid", + Value::Null, + )); + } + + // unwrap result to get the media. At this point we are sure + // media is some due to if statement above + let media = media.unwrap(); + + // Dispatch action based on presence of payment_request in request input + match payment_request { + Some(payment_request) => { + check_provided_payment_request(connection, client.clone(), media, payment_request).await + } + None => create_media_invoice(connection, client, media).await, + } +} + +async fn create_media_invoice( + connection: &PostgresConn, + lnd: &LightningClient>, + media: Media, +) -> Result { + let payment = generate_media_payment(connection, lnd, media).await; + match payment { + Ok(payment) => Ok(MediaInvoice::from((payment, InvoiceState::Open))), + Err(_) => Err(FieldError::new( + "Error while registering payment request.", + Value::Null, + )), + } +} + +/// Processes a check of an invoice state when payment_request input field is provided +async fn check_provided_payment_request( + connection: &PostgresConn, + lnd: LightningClient>, + media: Media, + payment_request: String, +) -> Result { + // Request db to find payment + let payment = connection + .run(move |c| MediaPayment::find_one_by_request(payment_request, c)) + .await; + + // In case of db failure + if payment.is_err() { + return Err(FieldError::new( + "Error while requesting database", + Value::Null, + )); + } + + // Unwrap result + let payment = payment.unwrap(); + + // In case there is no payment found + if payment.is_none() { + return Err(FieldError::new( + "No payment found with the provided payment_request", + Value::Null, + )); + } + + // Unwrap as we are sure at this point there is a payment + let payment = payment.unwrap(); + + // Ensure the request media is the same that is associated in the payment + if payment.media_uuid != media.uuid { + return Err(FieldError::new( + "payment_request does not match with the request media", + Value::Null, + )); + } + let payment_request = payment.request.clone(); + let invoice_result = + InvoiceUtils::get_invoice_state_from_payment_request(&lnd, payment_request).await; + + // Request LND service to get the Invoice object + + // In case of LND Service failure + if invoice_result.is_err() { + return Err(FieldError::new( + "Error while requesting lightning network registry", + Value::Null, + )); + } + + // unwrap the result as we are sure error case is handled + let invoice = invoice_result.unwrap(); + + // In case no invoice is found on LND service + if invoice.is_none() { + return Err(FieldError::new( + "No invoice found with the current payment request on the lightning network service", + Value::Null, + )); + } + + // Unwrap as we are sure at this point we have an invoice + let invoice = invoice.unwrap(); + + // Return result based on the invoice state + match invoice.state() { + InvoiceState::Accepted => { + // If invoice is in accepted state we return the current media invoice + // with the current state. + Ok(MediaInvoice::from((payment, invoice.state()))) + } + InvoiceState::Canceled => { + // If invoice has been canceled we generate a new one + let payment = generate_media_payment(connection, &lnd, media).await; + + // We catch the result and return the new media payment. + // We provide invoice state for previous invoice as + // this will help returning the replacementpayment output type + match payment { + Ok(payment) => Ok(MediaInvoice::from((payment, InvoiceState::Canceled))), + Err(error) => Err(error), + } + } + InvoiceState::Open => Ok(MediaInvoice::from((payment, invoice.state()))), + InvoiceState::Settled => Ok(MediaInvoice::from((payment, invoice.state()))), + } +} + +/// Method to generate a media payment +/// With invoice registering on LND +async fn generate_media_payment( + connection: &PostgresConn, + lnd: &LightningClient>, + media: Media, +) -> Result { + let memo = format!("Buy file \"{}\" with uuid: {}", media.title, media.uuid); + let params = InvoiceParams::new(Some(media.price as i64), Some(memo), None); + let invoice = InvoiceUtils::generate_invoice(lnd.clone(), params).await; + let payment = connection + .run(move |c| MediaPayment::create(NewMediaPayment::from((invoice, media.uuid)), c)) + .await; + + match payment { + Ok(payment) => Ok(payment), + Err(_) => Err(FieldError::new( + "Error while registering payment request.", + Value::Null, + )), + } +} + +/// Method to generate a field error +/// from a media payment +/// Shall be deleted as not used anymore +fn _field_error_from_media_payment( + media_payment: MediaPayment, + message: Option, +) -> FieldError { + let message = message.unwrap_or("Payment required".to_string()); + let request = media_payment.request.as_str(); + let expires_at = media_payment.expires_at.timestamp() as i32; + let state = media_payment.state; + FieldError::new( + message, + graphql_value!({ + "state": state, + "paymentRequest": request, + "expiresAt": expires_at + }), + ) +} diff --git a/src/graphql/queries/request_invoice_for_post.rs b/src/graphql/queries/request_invoice_for_post.rs new file mode 100644 index 0000000..5d064cf --- /dev/null +++ b/src/graphql/queries/request_invoice_for_post.rs @@ -0,0 +1,38 @@ +use crate::db::models::payment::NewPayment; +use crate::db::models::payment::Payment; +use crate::graphql::types::output::payment::PaymentType; +use crate::lnd::invoice::InvoiceUtils; +use crate::{db::models::Post, graphql::context::GQLContext}; +use juniper::{FieldError, Value}; + +pub async fn request_invoice_for_post<'a>( + context: &'a GQLContext, + post_id: uuid::Uuid, +) -> Result { + let connection = context.get_db_connection(); + let db_result = connection + .run(move |c| Post::find_one_by_id(post_id, c)) + .await; + + match db_result { + Some(post) => { + let invoice = + InvoiceUtils::generate_post_invoice(context.get_lnd_client().clone(), post).await; + let payment = connection + .run(move |c| Payment::create(NewPayment::from((invoice, post_id)), c)) + .await; + + match payment { + Ok(payment) => Ok(PaymentType::from(payment)), + Err(_) => Err(FieldError::new( + "Could not find post with provided uuid", + Value::Null, + )), + } + } + None => Err(FieldError::new( + "Could not find post with provided uuid", + Value::Null, + )), + } +} diff --git a/src/graphql/queries/utils.rs b/src/graphql/queries/utils.rs new file mode 100644 index 0000000..fcf029b --- /dev/null +++ b/src/graphql/queries/utils.rs @@ -0,0 +1,45 @@ +use crate::db::models::payment::NewPayment; +use crate::db::models::payment::Payment; +use crate::lnd::invoice::InvoiceUtils; +use crate::{db::models::Post, graphql::context::GQLContext}; +use juniper::{FieldError, Value}; +use uuid::Uuid; + +pub struct QueryUtils {} + +impl QueryUtils { + pub async fn generate_invoiced_error( + context: &GQLContext, + post_id: Uuid, + post: Post, + message: &str, + ) -> FieldError { + let connection = context.get_db_connection(); + let invoice = + InvoiceUtils::generate_post_invoice(context.get_lnd_client().clone(), post).await; + let payment = connection + .run(move |c| Payment::create(NewPayment::from((invoice, post_id)), c)) + .await; + + match payment { + Ok(payment) => { + let request = payment.request.as_str(); + let hash = payment.hash.as_str(); + + FieldError::new( + format!("{} Use provided payment request.", message), + graphql_value!({"state": "open", + "payment_request": request, + "r_hash": hash}), + ) + } + Err(_) => FieldError::new( + format!( + "{}. An error happened while trying to generate payment request", + message + ), + Value::null(), + ), + } + } +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 6dc141b..cf1c071 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,13 +1,18 @@ +use super::queries::get_files_list::get_files_list; +use super::queries::get_media::get_media; +use super::queries::get_post::get_post; +use super::queries::get_posts_list::get_posts_list; +use super::queries::request_invoice_for_media::request_invoice_for_media; +use super::queries::request_invoice_for_post::request_invoice_for_post; use super::types::input::post::PayablePostInput; +use super::types::output::media::MediaType; use super::types::output::payment::PaymentType; use super::types::output::post::PostType; use super::types::output::post::PreviewPostType; -use crate::db::models::payment::NewPayment; -use crate::db::models::payment::Payment; -use crate::lnd::invoice::InvoiceUtils; -use crate::{db::models::Post, graphql::context::GQLContext}; -use juniper::{FieldError, Value}; -use tonic_lnd::rpc::invoice::InvoiceState; +use crate::db::models::media::Media; +use crate::graphql::context::GQLContext; +use crate::graphql::types::output::invoices::MediaInvoice; +use juniper::FieldError; use uuid::Uuid; pub struct Query; @@ -20,49 +25,47 @@ impl Query { */ #[graphql(description = "Retrieves the list of posts")] async fn get_posts_list(context: &'a GQLContext) -> Result, FieldError> { - let connection = context.get_db_connection(); - let db_results = connection.run(move |c| Post::find_all_published(c)).await; + get_posts_list(context).await + } - Ok(db_results - .into_iter() - .map(|p| PreviewPostType::from(p)) - .collect::>()) + #[graphql(description = "Requests list of files")] + async fn get_files_list(context: &'a GQLContext) -> Result, FieldError> { + get_files_list(context).await + } + + #[graphql(description = " + Requests an invoice for a media. \n + If a payment_request is provided, the query will check + for the provided payment_request status and provide a new onee + if necessary. + ")] + async fn request_invoice_for_media( + context: &'a GQLContext, + uuid: uuid::Uuid, + payment_request: Option, + ) -> Result { + request_invoice_for_media(context, uuid, payment_request).await } #[graphql(description = "Requests a ln query paywall invoice for a given post")] - async fn requestInvoiceForPost( + async fn request_invoice_for_post( context: &'a GQLContext, post_id: uuid::Uuid, ) -> Result { - let connection = context.get_db_connection(); - let db_result = connection - .run(move |c| Post::find_one_by_id(post_id, c)) - .await; - - match db_result { - Some(post) => { - let invoice = - InvoiceUtils::generate_post_invoice(context.get_lnd_client().clone(), post) - .await; - let payment = connection - .run(move |c| Payment::create(NewPayment::from((invoice, post_id)), c)) - .await; - - match payment { - Ok(payment) => Ok(PaymentType::from(payment)), - Err(_) => Err(FieldError::new( - "Could not find post with provided uuid", - Value::Null, - )), - } - } - None => Err(FieldError::new( - "Could not find post with provided uuid", - Value::Null, - )), - } + request_invoice_for_post(context, post_id).await } + /* + * + */ + #[graphql(description = "Gets a specific post. The query is protected through a paywall")] + async fn get_media<'a, 'b>( + context: &'a GQLContext, + uuid: Uuid, + payment_request: Option, + ) -> Result { + get_media(context, uuid, payment_request).await + } /* Gets a post. This is the main request where paywall shall be applied. @@ -72,116 +75,17 @@ impl Query { context: &'a GQLContext, post: PayablePostInput, ) -> Result { - let post_id: uuid::Uuid = post.uuid.clone(); - let connection = context.get_db_connection(); - - // Find the post in the database - let result = connection - .run(move |c| Post::find_one_by_id(post_id, c)) - .await; - - match result { - Some(r) => match r.published { - // Checks if post is published - true => match r.is_payable() { - // Checks if there should be a paywall ( price > 0 ) - true => match post.payment_request { - // If payable, ensure there's a payment_request provided - Some(payment_request) => { - // payment_request found - - // Search for payment entry based on the payment_request provided - let payment = connection - .run(move |c| { - Payment::find_one_by_request(payment_request.clone(), c) - }) - .await; - match payment { - Some(payment) => { // Payment found - - // Request LND invoice and checks the invoice state - match InvoiceUtils::get_invoice_state_from_payment_request(context.get_lnd_client(), payment.request).await { - Ok(invoice_result) => match invoice_result { - Some(invoice) => match invoice.state() { - InvoiceState::Settled => Ok(PostType::from(r)), // Payment has been done. Serves the post - InvoiceState::Open => Err(FieldError::new( - "Awaiting for payment to be done.", - graphql_value!({"state": "open"}), - )), // Payment hasn't been done yet. We shall wait for payment, so there's no need to regenerate an invoice - InvoiceState::Accepted => Err(FieldError::new( - "Payment ongoing but not settled yet", - graphql_value!({"state": "ongoing"}), - )), // Payment is on process onto the network but has not reach its receiver yet. We shall wait, so there's no need to regenerate an invoice - InvoiceState::Canceled => Err(QueryUtils::generate_invoiced_error(context,post_id,r,"Payment expired or canceled.").await), - }, - // LND Server says there's no invoice matching - None => Err(QueryUtils::generate_invoiced_error(context,post_id,r,"No invoice found for corresponding payment request. Proceed to payment with the provided request payment").await) - - }, - // Invoice is broken. Maybe we should serve a new invoice here ? - Err(_) => Err(FieldError::new( - "An error happened when trying to decode invoice", - Value::null(), - )), - } - }, - // Our DB does not contain any payment with the provided payment_request. - None => Err(QueryUtils::generate_invoiced_error(context,post_id,r,"No recorded payment request related to the requested post found with the payment requested provided.").await) - } - } - None => Err(QueryUtils::generate_invoiced_error( - context, - post_id, - r, - "Payable post. Payment not found.", - ) - .await), - }, - false => Ok(PostType::from(r)), // Post has a price of 0 (free), so we serve it without condition - }, - false => Err(FieldError::new("Post not found", Value::Null)), // Post not published - }, - // Post has not been found in DB - None => Err(FieldError::new("Post not found", Value::Null)), - } + get_post(context, post).await } -} - -pub struct QueryUtils {} -impl QueryUtils { - pub async fn generate_invoiced_error( - context: &GQLContext, - post_id: Uuid, - post: Post, - message: &str, - ) -> FieldError { + #[graphql(description = "Gets the list of available medias")] + async fn get_medias_list(context: &'a GQLContext) -> Result, FieldError> { let connection = context.get_db_connection(); - let invoice = - InvoiceUtils::generate_post_invoice(context.get_lnd_client().clone(), post).await; - let payment = connection - .run(move |c| Payment::create(NewPayment::from((invoice, post_id)), c)) - .await; + let db_results = connection.run(move |c| Media::find_all_published(c)).await; - match payment { - Ok(payment) => { - let request = payment.request.as_str(); - let hash = payment.hash.as_str(); - - FieldError::new( - format!("{} Use provided payment request.", message), - graphql_value!({"state": "open", - "payment_request": request, - "r_hash": hash}), - ) - } - Err(_) => FieldError::new( - format!( - "{}. An error happened while trying to generate payment request", - message - ), - Value::null(), - ), - } + Ok(db_results + .into_iter() + .map(|media| MediaType::from(media)) + .collect::>()) } } diff --git a/src/graphql/types/input/file.rs b/src/graphql/types/input/file.rs new file mode 100644 index 0000000..81748b9 --- /dev/null +++ b/src/graphql/types/input/file.rs @@ -0,0 +1,11 @@ +#[derive(Clone, GraphQLInputObject)] +pub struct FileInput { + pub filename: String, + pub title: String, + pub description: Option, + pub price: i32, + pub published: bool, + // We expect this to be always `null` as per the spec + // see : https://github.com/jaydenseric/graphql-multipart-request-spec + pub file: Option, +} diff --git a/src/graphql/types/input/mod.rs b/src/graphql/types/input/mod.rs index e8b6291..34681e3 100644 --- a/src/graphql/types/input/mod.rs +++ b/src/graphql/types/input/mod.rs @@ -1 +1,2 @@ +pub mod file; pub mod post; diff --git a/src/graphql/types/output/invoices.rs b/src/graphql/types/output/invoices.rs new file mode 100644 index 0000000..ed57637 --- /dev/null +++ b/src/graphql/types/output/invoices.rs @@ -0,0 +1,76 @@ +use chrono::NaiveDateTime; +use tonic_lnd::rpc::invoice::InvoiceState; +use uuid::Uuid; + +use crate::db::models::media_payment::MediaPayment; + +#[derive(GraphQLObject)] +pub struct AvailablePayment { + #[graphql(description = "The related media uuid")] + media_uuid: Uuid, + #[graphql(description = "The paywall ln invoice payment request string")] + payment_request: String, + #[graphql(description = "The expiry time of current invoice")] + expires_at: NaiveDateTime, + #[graphql(description = "The current state of the payment request")] + state: Option, +} + +#[derive(GraphQLObject)] +pub struct ReplacementPayment { + #[graphql(description = "The related media uuid")] + media_uuid: Uuid, + #[graphql(description = "The paywall ln invoice payment request string")] + payment_request: String, + #[graphql(description = "The expiry time of current invoice")] + expires_at: NaiveDateTime, + #[graphql(description = "The current state of the payment request")] + state: Option, +} + +#[derive(GraphQLObject)] +pub struct SettledPayment { + #[graphql(description = "The related media uuid")] + media_uuid: Uuid, + #[graphql(description = "The paywall ln invoice payment request string")] + payment_request: String, + #[graphql(description = "The current state of the payment request")] + state: Option, +} + +#[derive(GraphQLUnion)] +pub enum MediaInvoice { + ReplacementPayment(ReplacementPayment), + AvailablePayment(AvailablePayment), + SettledPayment(SettledPayment), +} + +impl From<(MediaPayment, InvoiceState)> for MediaInvoice { + fn from(data: (MediaPayment, InvoiceState)) -> Self { + match data.1 { + InvoiceState::Accepted => Self::AvailablePayment(AvailablePayment { + media_uuid: data.0.media_uuid, + payment_request: data.0.request, + expires_at: data.0.expires_at, + state: Some("accepted".to_string()), + }), + InvoiceState::Open => Self::AvailablePayment(AvailablePayment { + media_uuid: data.0.media_uuid, + payment_request: data.0.request, + expires_at: data.0.expires_at, + state: Some("open".to_string()), + }), + InvoiceState::Settled => Self::SettledPayment(SettledPayment { + media_uuid: data.0.media_uuid, + payment_request: data.0.request, + state: Some("settled".to_string()), + }), + InvoiceState::Canceled => Self::ReplacementPayment(ReplacementPayment { + media_uuid: data.0.media_uuid, + payment_request: data.0.request, + expires_at: data.0.expires_at, + state: Some("open".to_string()), + }), + } + } +} diff --git a/src/graphql/types/output/media.rs b/src/graphql/types/output/media.rs new file mode 100644 index 0000000..c0f7f0c --- /dev/null +++ b/src/graphql/types/output/media.rs @@ -0,0 +1,167 @@ +use std::env; + +use crate::{ + db::models::{ + media::Media, + media_payment::{MediaPayment, NewMediaPayment}, + }, + graphql::context::GQLContext, + lnd::invoice::{InvoiceParams, InvoiceUtils}, +}; +use chrono::NaiveDateTime; +use infer::Infer; +use juniper::Value; +use juniper::{FieldError, FieldResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct MediaPreviewType { + pub uuid: uuid::Uuid, + pub description: Option, + pub price: i32, + pub published: bool, + pub created_at: NaiveDateTime, +} + +#[derive(Clone)] +pub struct MediaType { + pub uuid: uuid::Uuid, + pub title: String, + pub description: Option, + pub price: i32, + pub published: bool, + pub created_at: NaiveDateTime, + absolute_path: String, + payment_request: Option, +} + +impl From for MediaType { + fn from(item: Media) -> Self { + Self { + uuid: item.uuid, + title: item.title, + description: item.description, + price: item.price, + published: item.published, + created_at: item.created_at, + absolute_path: item.absolute_path, + payment_request: None, + } + } +} + +impl From<(Media, String)> for MediaType { + fn from(item: (Media, String)) -> Self { + let media = item.0; + + Self { + uuid: media.uuid, + title: media.title, + description: media.description, + price: media.price, + published: media.published, + created_at: media.created_at, + absolute_path: media.absolute_path, + payment_request: Some(item.1), + } + } +} + +impl MediaType { + async fn _generate_invoiced_error(&self, context: &GQLContext, message: &str) -> FieldError { + let connection = context.get_db_connection(); + let params = InvoiceParams::new(Some(self.price.into()), None, None); + let invoice = + InvoiceUtils::generate_invoice(context.get_lnd_client().clone(), params).await; + let uuid = self.uuid.clone(); + // let invoice = + // InvoiceUtils::generate_post_invoice(context.get_lnd_client().clone(), post).await; + let media_payment = connection + .run(move |c| MediaPayment::create(NewMediaPayment::from((invoice, uuid)), c)) + .await; + + match media_payment { + Ok(media_payment) => { + let request = media_payment.request.as_str(); + let hash = media_payment.hash.as_str(); + + FieldError::new( + format!("{} Use provided payment request.", message), + graphql_value!({"state": "open", + "payment_request": request, + "r_hash": hash}), + ) + } + Err(_) => FieldError::new( + format!( + "{}. An error happened while trying to generate payment request", + message + ), + Value::null(), + ), + } + } +} + +#[graphql_object( + name = "Media", + description = "Full Media output type" + context = GQLContext +)] +impl MediaType { + #[graphql(description = "The media internal id")] + fn uuid(&self) -> uuid::Uuid { + self.uuid + } + + #[graphql(description = "Media's title")] + fn title(&self) -> &String { + &self.title + } + + #[graphql(description = "Description of media")] + fn description(&self) -> Option<&String> { + self.description.as_ref() + } + + #[graphql(description = "Price of media access in satoshis. If free is 0")] + fn price(&self) -> i32 { + self.price + } + + #[graphql(description = "Publication status of media")] + fn published(&self) -> bool { + self.published + } + + #[graphql(description = "Creation date of media")] + fn created_at(&self) -> NaiveDateTime { + self.created_at + } + + #[graphql(description = "The direct URL of a media.")] + fn absolute_path(&self) -> String { + let a = env::current_dir().unwrap(); + + a.to_string_lossy().to_string() + } + + #[graphql(description = "the public URL to a media")] + fn public_url<'a>(&self, _context: &'a GQLContext) -> FieldResult { + let uri = format!("/file/{}", &self.uuid); + Ok(uri) + } + #[graphql(description = "The file type")] + fn file_type(&self) -> Option<&str> { + let info = Infer::new(); + let kind = info.get_from_path(&self.absolute_path); + + match kind { + Ok(result) => match result { + Some(t) => return Some(t.extension()), + None => return None, + }, + Err(_) => return None, + } + } +} diff --git a/src/graphql/types/output/mod.rs b/src/graphql/types/output/mod.rs index 835f9a2..79cd724 100644 --- a/src/graphql/types/output/mod.rs +++ b/src/graphql/types/output/mod.rs @@ -1,2 +1,4 @@ +pub mod invoices; +pub mod media; pub mod payment; pub mod post; diff --git a/src/graphql/types/output/payment.rs b/src/graphql/types/output/payment.rs index 22a71c5..b3603a2 100644 --- a/src/graphql/types/output/payment.rs +++ b/src/graphql/types/output/payment.rs @@ -1,5 +1,6 @@ -use crate::db::models::payment::Payment; +use crate::db::models::{media_payment::MediaPayment, payment::Payment}; use chrono::NaiveDateTime; +use tonic_lnd::rpc::invoice::InvoiceState; #[derive(GraphQLObject)] #[graphql(description = "A payment request object")] @@ -8,6 +9,8 @@ pub struct PaymentType { payment_request: String, #[graphql(description = "The expiry time of current invoice")] expires_at: NaiveDateTime, + #[graphql(description = "The current state of the payment request")] + state: Option, } impl From for PaymentType { @@ -15,6 +18,48 @@ impl From for PaymentType { Self { payment_request: item.request, expires_at: item.expires_at, + state: None, + } + } +} + +impl From for PaymentType { + fn from(item: MediaPayment) -> Self { + Self { + payment_request: item.request, + expires_at: item.expires_at, + state: None, + } + } +} + +impl From<(MediaPayment, InvoiceState)> for PaymentType { + fn from(item: (MediaPayment, InvoiceState)) -> Self { + Self { + payment_request: item.0.request, + expires_at: item.0.expires_at, + state: Some(Self::state_from_invoice_state(item.1)), + } + } +} + +impl From<(MediaPayment, &InvoiceState)> for PaymentType { + fn from(item: (MediaPayment, &InvoiceState)) -> Self { + Self { + payment_request: item.0.request, + expires_at: item.0.expires_at, + state: Some(Self::state_from_invoice_state(*item.1)), + } + } +} + +impl PaymentType { + pub fn state_from_invoice_state(invoice_state: InvoiceState) -> String { + match invoice_state { + InvoiceState::Accepted => String::from("accepted"), + InvoiceState::Canceled => String::from("canceled"), + InvoiceState::Settled => String::from("settled"), + InvoiceState::Open => String::from("open"), } } } diff --git a/src/guards/paymentrequestheader.rs b/src/guards/paymentrequestheader.rs index ef6d3f7..a13294a 100644 --- a/src/guards/paymentrequestheader.rs +++ b/src/guards/paymentrequestheader.rs @@ -2,7 +2,7 @@ use diesel::result::Error; use rocket::{ http::Status, request::{FromRequest, Outcome}, - Request + Request, }; use tonic_lnd::rpc::invoice::InvoiceState; diff --git a/src/guards/userguard.rs b/src/guards/userguard.rs index c52241a..f56028d 100644 --- a/src/guards/userguard.rs +++ b/src/guards/userguard.rs @@ -47,14 +47,14 @@ impl<'r> FromRequest<'r> for UserGuard { async fn from_request(request: &'r Request<'_>) -> Outcome { let authorization = request.headers().get_one("Authorization"); - - match authorization { - Some(authorization) => { - let formated_token = Self::format_bearer(authorization); + let session = request.cookies().get("session"); + match session { + Some(session) => { + // let formated_token = Self::format_bearer(authorization); let secret = Self::get_secret().unwrap(); let token = jsonwebtoken::decode::( - formated_token.as_str(), + session.value(), &DecodingKey::from_secret(secret.as_ref()), &Validation::new(Algorithm::HS256), ); diff --git a/src/lnd/invoice.rs b/src/lnd/invoice.rs index 9de9782..4226bc1 100644 --- a/src/lnd/invoice.rs +++ b/src/lnd/invoice.rs @@ -3,10 +3,12 @@ use crate::db::models::Post; use chrono::{Duration, NaiveDateTime, Utc}; use std::env; -use tonic::codegen::InterceptedService; +use tonic_lnd::rpc::invoice::InvoiceState; +// use tonic::codegen::InterceptedService; use tonic::{Code, Status}; use tonic_lnd::rpc::lightning_client::LightningClient; use tonic_lnd::rpc::{Invoice, PaymentHash}; +use tonic_lnd::tonic::codegen::InterceptedService; use tonic_lnd::MacaroonInterceptor; use lightning_invoice::*; @@ -21,9 +23,8 @@ pub struct InvoiceParams { impl InvoiceParams { pub fn new(value: Option, memo: Option, expiry: Option) -> Self { let default_value = env::var("DEFAULT_INVOICE_VALUE").unwrap_or("100".to_string()); - let default_expiry = - env::var("DEFAULT_INVOICE_EXPIRY").unwrap_or("API Payment".to_string()); - let default_memo = env::var("DEFAULT_INVOICE_MEMO").unwrap_or("600".to_string()); + let default_expiry = env::var("DEFAULT_INVOICE_EXPIRY").unwrap_or("600".to_string()); + let default_memo = env::var("DEFAULT_INVOICE_MEMO").unwrap_or("API Payment".to_string()); Self { value: value.unwrap_or_else(|| default_value.parse::().unwrap()), @@ -45,6 +46,7 @@ pub struct LndInvoice { pub value: i64, pub r_hash: String, pub expires_at: NaiveDateTime, + pub state: InvoiceState, } impl LndInvoice { @@ -55,12 +57,15 @@ impl LndInvoice { let expires_at = Utc::now() .checked_add_signed(Duration::seconds(invoice.expiry)) .unwrap(); + let state = invoice.state(); + Self { payment_request: invoice.payment_request, memo: invoice.memo, value: invoice.value as i64, r_hash: r_hash, expires_at: expires_at.naive_utc(), + state: state, } } } @@ -128,12 +133,58 @@ impl InvoiceUtils { LndInvoice::new(invoice, hex::encode(result.r_hash)) } - /* - Gets the invoice state from a payment request string. - It consists as a two steps method. - - First it registers an invoice - */ + // pub async fn state_invoice<'a>( + // lnd_client: &LightningClient< + // InterceptedService, + // >, + // payment_request: String) -> Result { + // let mut client = lnd_client.clone(); + + // // Parse the payment request + // let invoice = payment_request + // .as_str() + // .parse::() + // .unwrap(); + + // // Get the payment hash + // let p_hash = invoice.payment_hash().unwrap(); + + // /* + // The below instruction might seems a bit odd. + // the expected r_hash here is not the Invoice r_hash + // but rather the r_hash of the payment request which is + // denominated in the SignedRawInvoice as the payment_hash. + // */ + // let request = tonic::Request::new(PaymentHash { + // r_hash: p_hash.0.to_vec(), + // ..PaymentHash::default() + // }); + + // match client.lookup_invoice(request).await { + // Ok(response) => { + // match response.into_inner().state() { + // InvoiceState::Open => Err(Status::PaymentRequired), + // InvoiceState::Settled => Ok(Status::Ok), + // InvoiceState::Canceled => Err(Status::PaymentRequired), + // InvoiceState::Accepted => Err(Status::Accepted), + // } + // } , + // Err(status) => { + // if status.code() == Code::Unknown + // && (status.message() == "there are no existing invoices" + // || status.message() == "unable to locate invoice") + // { + // Ok(None) + // } else { + // Err(status) + // } + // } + // } + // } + // } + + // Gets the invoice state from a payment request string. + // It consists as a two steps method. pub async fn get_invoice_state_from_payment_request<'a>( lnd_client: &LightningClient< InterceptedService, diff --git a/src/main.rs b/src/main.rs index 1c4599f..1315b79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,12 +8,14 @@ extern crate rocket; #[macro_use] extern crate diesel; +#[macro_use] +extern crate diesel_migrations; + extern crate diesel_derive_enum; extern crate dotenv; extern crate juniper_rocket_multipart_handler; extern crate tokio_util; extern crate tonic; - mod app; mod catchers; mod cors; @@ -23,19 +25,20 @@ mod forms; mod graphql; mod guards; mod lnd; +mod routes; +use crate::db::PostgresConn; +use app::Schema; +use db::igniter::run_db_migrations; use dotenv::dotenv; use juniper::EmptySubscription; -use rocket::{ - figment::Figment, - Rocket, -}; +use rocket::fairing::AdHoc; +use rocket::{Build, Rocket}; +use routes::{auth::login, file::get_file, utils::graphiql}; -use crate::db::PostgresConn; -use app::Schema; use app::{ - get_graphql_handler, graphiql, login, options_handler, payable_post_graphql_handler, - post_graphql_handler, upload + auth_options_handler, graphql_options_handler, payable_post_graphql_handler, + post_graphql_handler, upload, }; use catchers::payment_required::payment_required; use cors::Cors; @@ -51,16 +54,20 @@ itconfig::config! { } #[rocket::main] -async fn main() { +async fn main() -> Result<(), rocket::Error> { dotenv().ok(); config::init(); - let figment = Figment::from(rocket::Config::default()); - // .merge(("limits", Limits::new().limit("json", 16.mebibytes()))); - Rocket::build() + let _rocket = Rocket::build() + .attach(PostgresConn::fairing()) + .attach(Cors) + .attach(AdHoc::try_on_ignite( + "Database Migrations", + run_db_migrations, + )) + .manage(Cors) // .configure(figment) .register("/", catchers![payment_required]) - .attach(PostgresConn::fairing()) .manage(Schema::new( Query, Mutation, @@ -69,18 +76,19 @@ async fn main() { .mount( "/", rocket::routes![ - options_handler, + graphql_options_handler, + auth_options_handler, graphiql, - get_graphql_handler, post_graphql_handler, payable_post_graphql_handler, upload, - login + login, + get_file ], ) - .attach(Cors) - .manage(Cors) .launch() .await .expect("server to launch"); + + Ok(()) } diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..4821887 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,35 @@ +use rocket::{ + form::{Form, Strict}, + http::{Cookie, CookieJar, SameSite, Status}, +}; + +use crate::{ + db::{models::user_token::UserToken, PostgresConn}, + forms::login_user::LoginUser, +}; + +/// Authentication route +#[rocket::post("/auth", data = "")] +pub async fn login( + db: PostgresConn, + cookies: &CookieJar<'_>, + user_form: Form>, +) -> rocket::http::Status { + let user = user_form.into_inner().into_inner(); + + let session = user.login(db).await; + + match session { + Ok(user_session) => { + let token = UserToken::generate_token(user_session).unwrap(); + let cookie = Cookie::build("session", token) + .same_site(SameSite::None) + .secure(true) + .finish(); + + cookies.add(cookie); + Status::Ok + } + Err(_) => Status::ExpectationFailed, + } +} diff --git a/src/routes/file.rs b/src/routes/file.rs new file mode 100644 index 0000000..2c91404 --- /dev/null +++ b/src/routes/file.rs @@ -0,0 +1,234 @@ +use rocket::{ + fs::NamedFile, + http::Status, + response::{content::RawJson, status}, +}; +use tonic::{codegen::InterceptedService, transport::Channel}; +use tonic_lnd::{ + rpc::{invoice::InvoiceState, lightning_client::LightningClient, Invoice}, + MacaroonInterceptor, +}; +use uuid::Uuid; + +use crate::{ + db::{ + models::{ + media::Media, + media_payment::{MediaPayment, NewMediaPayment}, + }, + PostgresConn, + }, + lnd::{ + client::LndClient, + invoice::{InvoiceParams, InvoiceUtils}, + }, +}; + +#[derive(Debug)] +pub enum FileHandlingError { + MediaNotFound, + InvoiceNotFound, + DbFailure, + LNFailure, + UuidParsingError, + PaymentRequired, +} + +/// A route to retrieve files behind the paywall. +#[rocket::get("/file/?")] +pub async fn get_file( + uuid: String, + invoice: Option, + db: PostgresConn, + lnd: LndClient, +) -> Result>>> { + // Calls the get_media to try to retrieve the requested media from database + let media = get_media(&uuid, &db).await; + + // Builds the error response if the media could not be retrieved + if media.is_err() { + return match media.unwrap_err() { + FileHandlingError::DbFailure => Err(status::Custom(Status::InternalServerError, None)), + FileHandlingError::MediaNotFound => Err(status::Custom(Status::NotFound, None)), + FileHandlingError::UuidParsingError => Err(status::Custom(Status::BadRequest, None)), + _ => Err(status::Custom(Status::ImATeapot, None)), + }; + } + + // There's no reason we could not unwrap the media as the match above should ensure to handle all the error cases. + // We can so consider the unwrap as safe. + let media = media.unwrap(); + + // If the media exists and is free we should deliver it to the user without performing any further operation + if media.price == 0 { + return Ok(NamedFile::open(media.absolute_path).await.unwrap()); + } + + // Otherwise we ensure try to retrieve an associated payment to the requested media. + // see get_media_payment for handling process + let payment = get_media_payment(invoice, &media.uuid, &db).await; + + // Payment retrieval failed + if payment.is_err() { + return match payment.unwrap_err() { + FileHandlingError::DbFailure => Err(status::Custom(Status::InternalServerError, None)), + FileHandlingError::PaymentRequired => { + let invoice = request_new_media_payment(&media, lnd, db).await; + + match invoice { + Ok(invoice) => { + let data = format!("{{ payment_request: {}}}", invoice.request); + + Err(status::Custom(Status::PaymentRequired, Some(RawJson(data)))) + } + Err(e) => match e { + FileHandlingError::DbFailure => { + Err(status::Custom(Status::InternalServerError, None)) + } + _ => Err(status::Custom(Status::ImATeapot, None)), + }, + } + } + _ => Err(status::Custom(Status::ImATeapot, None)), + }; + } + + let invoice = get_invoice(payment.unwrap(), &lnd.0).await; // .map_err(|error| return error).unwrap(); + + if invoice.is_err() { + return match invoice.unwrap_err() { + FileHandlingError::InvoiceNotFound => Err(status::Custom(Status::NotFound, None)), + FileHandlingError::LNFailure => Err(status::Custom(Status::InternalServerError, None)), + _ => Err(status::Custom(Status::ImATeapot, None)), + }; + } + + let invoice = invoice.unwrap(); + + match invoice.state() { + InvoiceState::Settled => Ok(NamedFile::open(media.absolute_path).await.unwrap()), + InvoiceState::Accepted => Err(status::Custom(Status::NotFound, None)), + InvoiceState::Canceled => { + let invoice = request_new_media_payment(&media, lnd, db).await; + match invoice { + Ok(invoice) => { + let data = format!("{{ payment_request: {}}}", invoice.request); + + Err(status::Custom(Status::PaymentRequired, Some(RawJson(data)))) + } + Err(e) => match e { + FileHandlingError::DbFailure => { + Err(status::Custom(Status::InternalServerError, None)) + } + _ => Err(status::Custom(Status::ImATeapot, None)), + }, + } + } + InvoiceState::Open => { + let data = format!("{{ payment_request: {}}}", invoice.payment_request); + + Err(status::Custom(Status::PaymentRequired, Some(RawJson(data)))) + } + } +} + +/// Generates an invoice and saves its value in databasee +async fn request_new_media_payment( + media: &Media, + lnd_client: LndClient, + db: PostgresConn, +) -> Result { + // Loads the client + let client = lnd_client.0; + + // let uuid = Uuid::parse_str(uuid.as_str()); + + // Return error if uuid parsing fails. + // if uuid.is_err() { + // return Err(FileHandlingError::UuidParsingError); + // } + + let uuid = media.uuid.to_owned(); + + // Calls utility to generate an invoice/ + // Todo : Generate + let invoice = InvoiceUtils::generate_invoice( + client, + InvoiceParams::new(Some(media.price.into()), None, None), + ) + .await; + + let media_payment = db + .run(move |c| MediaPayment::create(NewMediaPayment::from((invoice, uuid)), c)) + .await; + + match media_payment { + Ok(media_payment) => Ok(media_payment), + Err(_) => Err(FileHandlingError::DbFailure), + } +} + +// Retrieves media from database +async fn get_media(uuid: &String, db: &PostgresConn) -> Result { + let uuid = Uuid::parse_str(uuid.as_str()); + + match uuid { + Ok(uuid) => { + let media = db.run(move |c| Media::find_one_by_uuid(uuid, c)).await; + match media { + Ok(media) => match media { + Some(media) => Ok(media), + None => Err(FileHandlingError::MediaNotFound), + }, + Err(_) => Err(FileHandlingError::DbFailure), + } + } + Err(_) => Err(FileHandlingError::UuidParsingError), + } +} + +// Retrieves a media payment based on +async fn get_media_payment( + payment_request: Option, + media_uuid: &Uuid, + db: &PostgresConn, +) -> Result { + match payment_request { + // Ensure there is some payment_request provided + Some(payment_request) => { + // Retrieve recorded payment request from db + let payment = db + .run(move |c| MediaPayment::find_one_by_request(payment_request, c)) + .await; + match payment { + Ok(payment) => { + match payment { + Some(payment) => { + // Ensure the retrieved payment request matched the requested file association + match &payment.media_uuid == media_uuid { + true => Ok(payment), + false => Err(FileHandlingError::MediaNotFound), + } + } + None => Err(FileHandlingError::PaymentRequired), + } + } + Err(_) => Err(FileHandlingError::DbFailure), + } + } + None => Err(FileHandlingError::PaymentRequired), + } +} + +async fn get_invoice( + media_payment: MediaPayment, + lnd: &LightningClient>, +) -> Result { + match InvoiceUtils::get_invoice_state_from_payment_request(lnd, media_payment.request).await { + Ok(invoice) => match invoice { + Some(invoice) => Ok(invoice), + None => Err(FileHandlingError::InvoiceNotFound), + }, + Err(_) => Err(FileHandlingError::LNFailure), + } +} diff --git a/src/routes/graphql.rs b/src/routes/graphql.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..2b3fa05 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod file; +pub mod utils; diff --git a/src/routes/utils.rs b/src/routes/utils.rs new file mode 100644 index 0000000..4e98952 --- /dev/null +++ b/src/routes/utils.rs @@ -0,0 +1,6 @@ +use rocket::response::content; + +#[rocket::get("/")] +pub fn graphiql() -> content::RawHtml { + juniper_rocket::graphiql_source("/graphql", None) +}