├── .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 |
--------------------------------------------------------------------------------