├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src ├── main.rs ├── routes.rs ├── util.rs └── version.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tomphttp-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | salvo = { version= "0.41" } 10 | tokio = { version = "1", features = ["macros"] } 11 | serde = { version = "1.0" } 12 | serde_json = "1.0" 13 | reqwest = "0.11.18" 14 | once_cell = "1.17.1" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU Lesser General Public License 2 | ================================= 3 | 4 | _Version 3, 29 June 2007_ 5 | _Copyright © 2007 Free Software Foundation, Inc. <>_ 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | 11 | This version of the GNU Lesser General Public License incorporates 12 | the terms and conditions of version 3 of the GNU General Public 13 | License, supplemented by the additional permissions listed below. 14 | 15 | ### 0. Additional Definitions 16 | 17 | As used herein, “this License” refers to version 3 of the GNU Lesser 18 | General Public License, and the “GNU GPL” refers to version 3 of the GNU 19 | General Public License. 20 | 21 | “The Library” refers to a covered work governed by this License, 22 | other than an Application or a Combined Work as defined below. 23 | 24 | An “Application” is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A “Combined Work” is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the “Linked 32 | Version”. 33 | 34 | The “Minimal Corresponding Source” for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The “Corresponding Application Code” for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | ### 1. Exception to Section 3 of the GNU GPL 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | ### 2. Conveying Modified Versions 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | * **a)** under this License, provided that you make a good faith effort to 58 | ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | 62 | * **b)** under the GNU GPL, with none of the additional permissions of 63 | this License applicable to that copy. 64 | 65 | ### 3. Object Code Incorporating Material from Library Header Files 66 | 67 | The object code form of an Application may incorporate material from 68 | a header file that is part of the Library. You may convey such object 69 | code under terms of your choice, provided that, if the incorporated 70 | material is not limited to numerical parameters, data structure 71 | layouts and accessors, or small macros, inline functions and templates 72 | (ten or fewer lines in length), you do both of the following: 73 | 74 | * **a)** Give prominent notice with each copy of the object code that the 75 | Library is used in it and that the Library and its use are 76 | covered by this License. 77 | * **b)** Accompany the object code with a copy of the GNU GPL and this license 78 | document. 79 | 80 | ### 4. Combined Works 81 | 82 | You may convey a Combined Work under terms of your choice that, 83 | taken together, effectively do not restrict modification of the 84 | portions of the Library contained in the Combined Work and reverse 85 | engineering for debugging such modifications, if you also do each of 86 | the following: 87 | 88 | * **a)** Give prominent notice with each copy of the Combined Work that 89 | the Library is used in it and that the Library and its use are 90 | covered by this License. 91 | 92 | * **b)** Accompany the Combined Work with a copy of the GNU GPL and this license 93 | document. 94 | 95 | * **c)** For a Combined Work that displays copyright notices during 96 | execution, include the copyright notice for the Library among 97 | these notices, as well as a reference directing the user to the 98 | copies of the GNU GPL and this license document. 99 | 100 | * **d)** Do one of the following: 101 | - **0)** Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | - **1)** Use a suitable shared library mechanism for linking with the 109 | Library. A suitable mechanism is one that **(a)** uses at run time 110 | a copy of the Library already present on the user's computer 111 | system, and **(b)** will operate properly with a modified version 112 | of the Library that is interface-compatible with the Linked 113 | Version. 114 | 115 | * **e)** Provide Installation Information, but only if you would otherwise 116 | be required to provide such information under section 6 of the 117 | GNU GPL, and only to the extent that such information is 118 | necessary to install and execute a modified version of the 119 | Combined Work produced by recombining or relinking the 120 | Application with a modified version of the Linked Version. (If 121 | you use option **4d0**, the Installation Information must accompany 122 | the Minimal Corresponding Source and Corresponding Application 123 | Code. If you use option **4d1**, you must provide the Installation 124 | Information in the manner specified by section 6 of the GNU GPL 125 | for conveying Corresponding Source.) 126 | 127 | ### 5. Combined Libraries 128 | 129 | You may place library facilities that are a work based on the 130 | Library side by side in a single library together with other library 131 | facilities that are not Applications and are not covered by this 132 | License, and convey such a combined library under terms of your 133 | choice, if you do both of the following: 134 | 135 | * **a)** Accompany the combined library with a copy of the same work based 136 | on the Library, uncombined with any other library facilities, 137 | conveyed under the terms of this License. 138 | * **b)** Give prominent notice with the combined library that part of it 139 | is a work based on the Library, and explaining where to find the 140 | accompanying uncombined form of the same work. 141 | 142 | ### 6. Revised Versions of the GNU Lesser General Public License 143 | 144 | The Free Software Foundation may publish revised and/or new versions 145 | of the GNU Lesser General Public License from time to time. Such new 146 | versions will be similar in spirit to the present version, but may 147 | differ in detail to address new problems or concerns. 148 | 149 | Each version is given a distinguishing version number. If the 150 | Library as you received it specifies that a certain numbered version 151 | of the GNU Lesser General Public License “or any later version” 152 | applies to it, you have the option of following the terms and 153 | conditions either of that published version or of any later version 154 | published by the Free Software Foundation. If the Library as you 155 | received it does not specify a version number of the GNU Lesser 156 | General Public License, you may choose any version of the GNU Lesser 157 | General Public License ever published by the Free Software Foundation. 158 | 159 | If the Library as you received it specifies that a proxy can decide 160 | whether future versions of the GNU Lesser General Public License shall 161 | apply, that proxy's public statement of acceptance of any version is 162 | permanent authorization for you to choose that version for the 163 | Library. 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bare-server-rust 2 | bare-server-rust is a fully compliant Rust implementation of [TompHTTPs' Bare Server specifications](https://github.com/tomphttp/specifications/blob/master/BareServer.md). 3 | This is a server that will receive requests from a service worker (or any client) and forward a request to a specified URL. 4 | 5 | # How to use 6 | ``` 7 | git clone https://github.com/NebulaServices/bare-server-rust 8 | 9 | cd bare-server-rust 10 | 11 | cargo build && cargo run 12 | ``` 13 | ## To-do 14 | * Websocket support 15 | 16 | ## Authors 17 | * [UndefinedBHVR](https://github.com/UndefinedBHVR) 18 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # NOTE: Wrapping comments required the nightly toolchain 2 | # A saddening amount of rustfmt features are locked on nightly 3 | # despite being more or less stable. 4 | wrap_comments = true -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use salvo::prelude::*; 3 | use util::REQWEST_CLIENT; 4 | use version::VersionData; 5 | 6 | pub mod routes; 7 | pub mod util; 8 | pub mod version; 9 | 10 | #[handler] 11 | async fn versions(res: &mut Response) { 12 | res.render(Json(VersionData::default())); 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | REQWEST_CLIENT 18 | .set(Client::new()) 19 | .expect("This should never error"); 20 | 21 | let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; 22 | Server::new(acceptor).serve(routes::built_routes()).await; 23 | } 24 | -------------------------------------------------------------------------------- /src/routes.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::HeaderMap; 2 | 3 | use salvo::http::header::{HeaderName, HeaderValue}; 4 | 5 | use salvo::http::request::secure_max_size; 6 | use salvo::prelude::*; 7 | 8 | 9 | use crate::util::{join_bare_headers, split_headers, ProcessedHeaders, REQWEST_CLIENT}; 10 | use crate::version::VersionData; 11 | use std::any::TypeId; 12 | use std::collections::HashMap; 13 | use std::ops::DerefMut; 14 | use std::str::FromStr; 15 | 16 | #[handler] 17 | async fn versions(res: &mut Response) { 18 | add_cors_headers(res); 19 | res.render(Json(VersionData::default())); 20 | } 21 | 22 | #[handler] 23 | /// A function to preprocess headers from a request and inject them to the depot 24 | async fn preprocess_headers(req: &mut Request, depot: &mut Depot) { 25 | // Get a mutable reference to the headers from the request 26 | let headers: &mut HeaderMap = req.headers_mut(); 27 | // Create a new processed headers object with default values 28 | let mut processed = ProcessedHeaders::default(); 29 | 30 | // Process forwarded headers using functional methods 31 | // Get the value of x-bare-forward-headers or use an empty string as default 32 | let header_value = headers.get("x-bare-forward-headers").map_or("", |h| { 33 | h.to_str().expect("Should map to string successfully") 34 | }); 35 | // Split the value by comma and space and collect it into a vector 36 | let forwarded_heads: Vec = header_value.split(", ").map(|s| s.to_owned()).collect(); 37 | // Filter out the invalid headers and append the valid ones to the processed 38 | // headers 39 | forwarded_heads 40 | .iter() 41 | .filter(|head| { 42 | match head.as_str() { 43 | // If headers are invalid, we don't need them. 44 | "connection" | "transfer-encoding" | "host" | "origin" | "referer" | "" => false, 45 | _ => true, 46 | } 47 | }) 48 | .for_each(|head| { 49 | println!("Current head: {head}"); 50 | let he = &HeaderName::from_str(head).expect("Should not fail here"); 51 | processed.append(he, headers.get(he).expect("Header should exist").clone()); 52 | }); 53 | 54 | // Get the value of x-bare-headers or use the joined bare headers as default 55 | let bare_headers = headers 56 | .get("x-bare-headers") 57 | .map_or_else(|| join_bare_headers(headers).unwrap(), |h| h.to_owned()); 58 | 59 | // Process bare headers if they exist 60 | if !bare_headers.is_empty() { 61 | // Deserialize the bare headers into a hashmap of strings 62 | let data: HashMap = 63 | serde_json::from_str(bare_headers.to_str().expect("Should be valid string")) 64 | .expect("Should not fail to Str:Str deserialize"); 65 | // Append the hashmap entries to the processed headers 66 | data.iter().for_each(|(head, value)| { 67 | processed.append( 68 | HeaderName::from_str(head).unwrap(), 69 | HeaderValue::from_str(value).unwrap(), 70 | ); 71 | }); 72 | 73 | // Pass content length header too. 74 | if let Some(content_length) = headers.get("content-length") { 75 | processed.append( 76 | HeaderName::from_str("content-length").unwrap(), 77 | content_length.to_owned(), 78 | ); 79 | } 80 | // Host key is not needed, I think? 81 | processed.remove("host"); 82 | } 83 | // Inject processed headers to the depot. 84 | depot.inject(processed); 85 | } 86 | 87 | #[handler] 88 | /// Handler for [`TOMPHttp V2`](https://github.com/tomphttp/specifications/blob/master/BareServerV2.md#send-and-receive-data-from-a-remote) requests. 89 | async fn v2_get(req: &mut Request, res: &mut Response, depot: &mut Depot) { 90 | // Get a mutable reference to the processed headers from the depot 91 | let headers: &mut ProcessedHeaders = depot 92 | .get_mut(&format!("{:?}", TypeId::of::())) 93 | .unwrap(); 94 | 95 | // Get the path from the request header or use "/" as default 96 | let path = req 97 | .header::("x-bare-path") 98 | .unwrap_or("/".to_owned()); 99 | 100 | // Construct the full URL from the request header or use default values 101 | let url = format!( 102 | "{}//{}{}", 103 | // Assume HTTPS if not specified 104 | req.header::("x-bare-protocol") 105 | .unwrap_or("https:".to_owned()), 106 | req.header::("x-bare-host") 107 | .unwrap_or("example.com".to_owned()), 108 | path 109 | ); 110 | 111 | // Make a new request using the same method and URL as the original one 112 | let response = REQWEST_CLIENT 113 | .get() 114 | .unwrap() 115 | .request(req.method().clone(), url) 116 | // Use the processed headers as the new request headers 117 | .headers(headers.deref_mut().to_owned()) 118 | // Read the payload from the original request with a maximum size limit 119 | .body( 120 | req.payload_with_max_size(secure_max_size()) 121 | .await 122 | .expect("Probably won't error?") 123 | .to_vec(), 124 | ) 125 | // Send the new request and panic if it fails 126 | .send() 127 | .await 128 | .unwrap_or_else(|x| { 129 | panic!("{x}"); 130 | }); 131 | 132 | // Set the status code of the response to match the new request's status code 133 | res.status_code(response.status()); 134 | // Set x-bare-headers to show the new request's headers 135 | res.add_header("x-bare-headers", format!("{:?}", response.headers()), true) 136 | .expect("This shouldn't fail, probably?"); 137 | // Split the headers if needed 138 | res.set_headers(split_headers(&res.headers)); 139 | // Set some of the required headers from the new request's headers 140 | if let Some(header) = response.headers().get("content-type") { 141 | res.add_header("content-type", header, true).unwrap(); 142 | } 143 | res.add_header("x-bare-status", response.status().as_str(), true) 144 | .expect("This shouldn't fail."); 145 | res.add_header( 146 | "x-bare-status-text", 147 | response.status().canonical_reason().expect("Should exist"), 148 | true, 149 | ) 150 | .expect("This shouldn't fail"); 151 | // Add cors headers to the response 152 | add_cors_headers(res); 153 | 154 | // Write the body of the response using the bytes from the new request's 155 | // response 156 | res.write_body(response.bytes().await.unwrap()) 157 | .expect("This should not fail?"); 158 | } 159 | 160 | /// Blanket fix for CORS headers while in dev. 161 | /// 162 | /// THIS IS BAD AND A SEC VULN, WILL BE FIXED LATER. 163 | fn add_cors_headers(res: &mut Response) { 164 | res.add_header("access-control-allow-origin", "*", true) 165 | .unwrap(); 166 | res.add_header("access-control-allow-headers", "*", true) 167 | .unwrap(); 168 | res.add_header("access-control-allow-methods", "*", true) 169 | .unwrap(); 170 | res.add_header("access-control-expose-headers", "*", true) 171 | .unwrap(); 172 | } 173 | 174 | /// Build our routes. 175 | pub fn built_routes() -> Router { 176 | Router::new().get(versions).push( 177 | Router::with_path("v2") 178 | .hoop(preprocess_headers) 179 | .handle(v2_get), 180 | ) 181 | } 182 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use reqwest::Client; 3 | use salvo::{ 4 | http::HeaderValue, 5 | hyper::{http::HeaderName, HeaderMap}, 6 | }; 7 | use std::{ 8 | ops::{Deref, DerefMut}, 9 | str::{self, FromStr}, 10 | }; 11 | const MAX_HEADER_VALUE: usize = 3072; 12 | pub static REQWEST_CLIENT: OnceCell = OnceCell::new(); 13 | #[derive(Default, Clone, Debug)] 14 | pub struct ProcessedHeaders(HeaderMap); 15 | 16 | impl Deref for ProcessedHeaders { 17 | type Target = HeaderMap; 18 | 19 | fn deref(&self) -> &Self::Target { 20 | &self.0 21 | } 22 | } 23 | 24 | impl DerefMut for ProcessedHeaders { 25 | fn deref_mut(&mut self) -> &mut Self::Target { 26 | &mut self.0 27 | } 28 | } 29 | 30 | /// This function splits any header value in the input HeaderMap that is longer 31 | /// than MAX_HEADER_VALUE (3072 bytes) into smaller headers with a suffix 32 | /// indicating their order and returns a new HeaderMap. 33 | /// 34 | /// For example, a header "X-BARE-HEADERS" with a 5000-byte value will be split 35 | /// into two headers: "X-BARE-HEADERS-0" and "X-BARE-HEADERS-1". 36 | /// 37 | /// The original case of the header names is preserved and other headers are not 38 | /// modified. 39 | pub fn split_headers(headers: &HeaderMap) -> HeaderMap { 40 | // Create a new empty header map for the output 41 | let mut output = HeaderMap::new(); 42 | // Iterate over each header in the input header map 43 | headers.iter().for_each(|(name, value)| { 44 | // Check if the header name is "x-bare-headers" (case-insensitive) and if the 45 | // header value length exceeds the MAX_HEADER_VALUE limit 46 | if name.as_str().to_lowercase() == "x-bare-headers" && value.len() > MAX_HEADER_VALUE { 47 | // Split the header value into chunks of MAX_HEADER_VALUE bytes 48 | value 49 | .as_bytes() 50 | .chunks(MAX_HEADER_VALUE) 51 | // Convert each chunk into a string slice 52 | .map(|buf| unsafe { str::from_utf8_unchecked(buf) }) 53 | // Enumerate each chunk with an index 54 | .enumerate() 55 | // For each chunk, create a new header name with the suffix "-{i}" where i is the 56 | // index, and append it to the output header map with the chunk as the value 57 | .for_each(|(i, value)| { 58 | output.append( 59 | HeaderName::from_str(&format!("X-BARE-HEADERS-{i}")) 60 | .expect("[Split Headers] Failed to create header name?"), 61 | HeaderValue::from_str(&format!(";{value}")) 62 | .expect("[Split Headers] Failed to split header content?"), 63 | ); 64 | }); 65 | } else { 66 | // If the header name is not "x-bare-headers" or the header value length is 67 | // within the limit, append it to the output header map as it is 68 | output.append(name, value.clone()); 69 | } 70 | }); 71 | // Return the output header map 72 | output 73 | } 74 | 75 | /// This function joins any headers in the input HeaderMap that have the prefix 76 | /// "X-BARE-HEADERS-" into a single header with the name "X-BARE-HEADERS" and 77 | /// returns a new HeaderMap. 78 | /// 79 | /// For example, two headers "X-BARE-HEADERS-0" and "X-BARE-HEADERS-1" with 80 | /// values "foo" and "bar" respectively will be joined into one header 81 | /// "X-BARE-HEADERS" with the value "foobar". 82 | /// 83 | /// The original case of the header names is preserved and other headers are not 84 | /// modified. 85 | pub fn join_bare_headers(headers: &HeaderMap) -> Result { 86 | let mut err: Option = None; 87 | // Create a new empty string for the joined header value 88 | let mut joined_value = String::new(); 89 | // Iterate over each header in the input header map 90 | headers.iter().for_each(|(name, value)| { 91 | // Check if the header name has the prefix "x-bare-headers-" (case-insensitive) 92 | if name.as_str().to_lowercase().starts_with("x-bare-headers-") { 93 | if !value 94 | .to_str() 95 | .expect("[Join Headers] Should be convertable to string") 96 | .starts_with(';') 97 | { 98 | err = Some("Header started with invalid character.".into()); 99 | } 100 | // Append the header value to the joined value string 101 | joined_value.push_str( 102 | value 103 | .to_str() 104 | .expect("[Join Headers] Failed to convert header value to string?"), 105 | ); 106 | } 107 | }); 108 | if let Some(e) = err { 109 | return Err(e); 110 | } 111 | // Create a new header value from the joined value string 112 | let joined_value = HeaderValue::from_str(&joined_value) 113 | .expect("[Join Headers] Failed to create header value?"); 114 | // Return joined values 115 | Ok(joined_value) 116 | } 117 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the data structures for the version information of the 2 | //! Nebula TOMPHTTP Server project. 3 | 4 | // Import the serde crate to enable serialization of the structs 5 | use serde::Serialize; 6 | 7 | /// A struct that represents the version data of the project 8 | #[derive(Serialize, Debug)] 9 | pub struct VersionData { 10 | /// A vector of strings that contains the supported versions of the TOMPHTTP 11 | /// specification 12 | versions: Vec, 13 | /// A string that indicates the programming language used for the project 14 | language: String, 15 | /// A MaintainerData struct that contains the information of the project 16 | /// maintainer 17 | maintainer: MaintainerData, 18 | /// A ProjectData struct that contains the general information of the 19 | /// project 20 | project: ProjectData, 21 | } 22 | 23 | // Implement the Default trait for VersionData to provide a default value 24 | impl Default for VersionData { 25 | fn default() -> Self { 26 | Self { 27 | // Currently the project supports only version 2 of the [`TOMPHTTP`](https://github.com/tomphttp/specifications/blob/master/BareServerV2.md) specification 28 | versions: vec!["v2".into()], 29 | // The project is written in Rust 30 | language: "Rust".into(), 31 | // Use the default value for MaintainerData 32 | maintainer: MaintainerData::default(), 33 | // Use the default value for ProjectData 34 | project: ProjectData::default(), 35 | } 36 | } 37 | } 38 | 39 | /// A struct that represents the maintainer data of the project 40 | #[derive(Serialize, Debug)] 41 | pub struct MaintainerData { 42 | /// A string that contains the email address of the maintainer 43 | email: String, 44 | /// A string that contains the website of the maintainer 45 | website: String, 46 | } 47 | 48 | // Implement the Default trait for MaintainerData to provide a default value 49 | impl Default for MaintainerData { 50 | fn default() -> Self { 51 | Self { 52 | email: "nebuladev@undefinedbhvr.com".into(), 53 | website: "https://github.com/NebulaServices".into(), 54 | } 55 | } 56 | } 57 | 58 | /// A struct that represents the project data of the project 59 | #[derive(Serialize, Debug)] 60 | pub struct ProjectData { 61 | /// A string that contains the name of the project 62 | name: String, 63 | /// A string that contains a brief description of the project 64 | description: String, 65 | /// A string that contains the email address of the project contact 66 | email: String, 67 | /// A string that contains the website of the project 68 | website: String, 69 | /// A string that contains the repository URL of the project 70 | repository: String, 71 | /// A string that contains the current version of the project 72 | version: String, 73 | } 74 | 75 | // Implement the Default trait for ProjectData to provide a default value 76 | impl Default for ProjectData { 77 | fn default() -> Self { 78 | Self { 79 | name: "Nebula TOMPHTTP Server".into(), 80 | description: "Clean implementation of the TOMPHttp Specification in Rust.".into(), 81 | email: "".into(), 82 | website: "https://github.com/NebulaServices/bare-server-rust".into(), 83 | repository: "https://github.com/NebulaServices/bare-server-rust".into(), 84 | version: "1.0.0".into(), 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------