well-known mime, favicon

master
Ondřej Hruška 5 years ago
parent ecaad53beb
commit 0ecfdbb7f9
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      .gitignore
  2. 177
      Cargo.lock
  3. 7
      Cargo.toml
  4. BIN
      postit.db
  5. 29
      src/config.rs
  6. BIN
      src/favicon.ico
  7. 416
      src/main.rs
  8. 742
      src/well_known_mime.rs

2
.gitignore vendored

@ -1 +1,3 @@
/target /target
.idea/
postit.json

177
Cargo.lock generated

@ -12,7 +12,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
dependencies = [ dependencies = [
"memchr", "memchr 2.3.3",
] ]
[[package]] [[package]]
@ -87,6 +87,16 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
[[package]]
name = "bincode"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf"
dependencies = [
"byteorder",
"serde",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
@ -151,7 +161,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
dependencies = [ dependencies = [
"memchr", "memchr 2.3.3",
"safemem", "safemem",
] ]
@ -323,6 +333,30 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]]
name = "flate2"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
[[package]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@ -393,6 +427,15 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "indexmap"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
dependencies = [
"autocfg 1.0.0",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.5" version = "0.4.5"
@ -461,6 +504,15 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "memchr"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.3" version = "2.3.3"
@ -488,6 +540,15 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "miniz_oxide"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5"
dependencies = [
"adler32",
]
[[package]] [[package]]
name = "multipart" name = "multipart"
version = "0.15.4" version = "0.15.4"
@ -506,6 +567,61 @@ dependencies = [
"twoway", "twoway",
] ]
[[package]]
name = "nom"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b"
dependencies = [
"memchr 1.0.2",
]
[[package]]
name = "num"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
dependencies = [
"autocfg 1.0.0",
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
dependencies = [
"autocfg 1.0.0",
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.42" version = "0.1.42"
@ -516,6 +632,29 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-iter"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00"
dependencies = [
"autocfg 1.0.0",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
dependencies = [
"autocfg 1.0.0",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.11" version = "0.2.11"
@ -614,6 +753,16 @@ dependencies = [
"sha-1", "sha-1",
] ]
[[package]]
name = "petgraph"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c127eea4a29ec6c85d153c59dc1213f33ec74cead30fe4730aecc88cc1fd92"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.7.24" version = "0.7.24"
@ -657,8 +806,14 @@ dependencies = [
name = "postit" name = "postit"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"bincode",
"chrono",
"clappconfig", "clappconfig",
"flate2",
"log 0.4.8", "log 0.4.8",
"num",
"num-derive",
"parking_lot", "parking_lot",
"rand 0.7.3", "rand 0.7.3",
"rouille", "rouille",
@ -666,6 +821,7 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"siphasher 0.3.3", "siphasher 0.3.3",
"tree_magic",
] ]
[[package]] [[package]]
@ -904,7 +1060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr 2.3.3",
"regex-syntax", "regex-syntax",
"thread_local", "thread_local",
] ]
@ -1140,13 +1296,26 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "tree_magic"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d99367ce3e553a84738f73bd626ccca541ef90ae757fdcdc4cbe728e6cb629"
dependencies = [
"fnv",
"lazy_static",
"nom",
"parking_lot",
"petgraph",
]
[[package]] [[package]]
name = "twoway" name = "twoway"
version = "0.1.8" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
dependencies = [ dependencies = [
"memchr", "memchr 2.3.3",
] ]
[[package]] [[package]]

@ -16,3 +16,10 @@ serde_derive = "1.0"
log = "0.4.8" log = "0.4.8"
siphasher = "0.3.3" siphasher = "0.3.3"
rand = "0.7.3" rand = "0.7.3"
chrono = "0.4.11"
bincode = "1.2.1"
flate2 = "1.0.14"
anyhow = "1.0.28"
tree_magic = { version = "0.2.3", default_features = false, features = ["staticmime"] }
num = "0.2.1"
num-derive = "0.3.0"

Binary file not shown.

@ -1,8 +1,8 @@
use clappconfig::{AppConfig, anyhow, clap::ArgMatches}; use clappconfig::{anyhow, clap::ArgMatches, AppConfig};
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)] #[serde(default)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub(crate) struct Config { pub(crate) struct Config {
@ -32,6 +32,15 @@ pub(crate) struct Config {
/// Max uploaded file size in bytes /// Max uploaded file size in bytes
pub(crate) max_file_size: usize, pub(crate) max_file_size: usize,
/// Enable persistence
pub(crate) persistence: bool,
/// Enable compression when persisting/loading
pub(crate) compression: bool,
/// Persistence file
pub(crate) persist_file : String,
} }
impl Default for Config { impl Default for Config {
@ -44,7 +53,10 @@ impl Default for Config {
default_expiry: Duration::from_secs(60 * 10), default_expiry: Duration::from_secs(60 * 10),
max_expiry: Duration::from_secs(60 * 10), max_expiry: Duration::from_secs(60 * 10),
expired_gc_interval: Duration::from_secs(60), expired_gc_interval: Duration::from_secs(60),
max_file_size: 1 * (1024 * 1024) // 1MB max_file_size: 1 * (1024 * 1024), // 1MB
persistence: false,
compression: true,
persist_file: "postit.db".to_string(),
} }
} }
} }
@ -66,22 +78,17 @@ impl AppConfig for Config {
} }
mod serde_duration_secs { mod serde_duration_secs {
use serde::{self, Deserialize, Serializer, Deserializer}; use serde::{self, Deserialize, Deserializer, Serializer};
use std::time::Duration; use std::time::Duration;
pub fn serialize<S>( pub fn serialize<S>(value: &Duration, se: S) -> Result<S::Ok, S::Error>
value: &Duration,
se: S,
) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
se.serialize_u64(value.as_secs()) se.serialize_u64(value.as_secs())
} }
pub fn deserialize<'de, D>( pub fn deserialize<'de, D>(de: D) -> Result<Duration, D::Error>
de: D,
) -> Result<Duration, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@ -1,53 +1,63 @@
#[macro_use] extern crate serde_derive; #[macro_use]
#[macro_use] extern crate log; extern crate serde_derive;
#[macro_use]
extern crate log;
#[macro_use]
extern crate num_derive;
use std::time::{Instant, Duration}; use crate::config::Config;
use clappconfig::{anyhow, AppConfig};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::collections::HashMap;
use clappconfig::{AppConfig, anyhow};
use std::io::Read;
use std::hash::{Hash, Hasher};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::Rng; use rand::Rng;
use rouille::{Request, Response, ResponseBody}; use rouille::{Request, Response, ResponseBody};
use std::borrow::Cow; use std::borrow::Cow;
use crate::config::Config; use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::time::Duration;
use chrono::{Utc, DateTime};
use std::fs::OpenOptions;
use std::fmt::Display;
use serde::export::Formatter;
use std::fmt;
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use serde::de::{DeserializeOwned, Visitor};
use crate::well_known_mime::Mime;
mod config; mod config;
mod well_known_mime;
fn error_with_text(code : u16, text : impl Into<String>) -> Response { const HDR_EXPIRES : &str = "X-Expires";
Response { const HDR_SECRET : &str = "X-Secret";
status_code: code,
headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
data: rouille::ResponseBody::from_string(text),
upgrade: None,
}
}
fn empty_error(code : u16) -> Response { const FAVICON : &[u8] = include_bytes!("favicon.ico");
Response {
status_code: code,
headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
data: rouille::ResponseBody::empty(),
upgrade: None,
}
}
/// Post ID (represented as a 16-digit hex string)
type PostId = u64; type PostId = u64;
/// Write token (represented as a 16-digit hex string)
type Secret = u64; type Secret = u64;
/// Hash of a data record
type DataHash = u64; type DataHash = u64;
#[derive(Debug)] /// Post stored in the repository
#[derive(Debug,Serialize,Deserialize)]
struct Post { struct Post {
mime : Cow<'static, str>, /// Content-Type
hash : DataHash, mime: Mime,
secret : Secret, /// Data hash
expires : Instant, hash: DataHash,
/// Secret key for editing or deleting
secret: Secret,
/// Expiration timestamp
#[serde(with = "serde_chrono_datetime_as_unix")]
expires: DateTime<Utc>,
} }
impl Post { impl Post {
/// Check if the post expired
pub fn is_expired(&self) -> bool { pub fn is_expired(&self) -> bool {
self.expires < Instant::now() self.expires < Utc::now()
} }
} }
@ -55,7 +65,15 @@ fn main() -> anyhow::Result<()> {
let config = Config::init("postit", "postit.json", None)?; let config = Config::init("postit", "postit.json", None)?;
let serve_at = format!("{}:{}", config.host, config.port); let serve_at = format!("{}:{}", config.host, config.port);
let store = Mutex::new(Repository::new(config)); let store = Mutex::new({
let mut store = Repository::new(config);
if store.config.persistence {
if let Err(e) = store.load() {
error!("Load failed: {}", e);
}
}
store
});
rouille::start_server(serve_at, move |req| { rouille::start_server(serve_at, move |req| {
let mut store_w = store.lock(); let mut store_w = store.lock();
@ -63,52 +81,120 @@ fn main() -> anyhow::Result<()> {
info!("{} {}", method, req.raw_url()); info!("{} {}", method, req.raw_url());
if req.url() == "/favicon.ico" {
return Response::from_data("image/vnd.microsoft.icon", FAVICON);
}
store_w.gc_expired_posts_if_needed(); store_w.gc_expired_posts_if_needed();
match method { let resp = match method {
"POST" | "PUT" => { "POST" | "PUT" => store_w.serve_post_put(req),
store_w.serve_post_put(req) "GET" | "HEAD" => store_w.serve_get_head(req),
} "DELETE" => store_w.serve_delete(req),
"GET" | "HEAD" => { _ => rouille::Response::empty_400(),
store_w.serve_get_head(req) };
}
"DELETE" => { if store_w.config.persistence {
store_w.serve_delete(req) if let Err(e) = store_w.persist_if_needed() {
error!("Store failed: {}", e);
} }
_ => {
rouille::Response::empty_400()
} }
if resp.is_error() {
warn!("Error resp: {}", resp.status_code);
} }
resp
}); });
} }
type PostsMap = HashMap<PostId, Post>; type PostsMap = HashMap<PostId, Post>;
type DataMap = HashMap<DataHash, (usize, Vec<u8>)>; type DataMap = HashMap<DataHash, (usize, Vec<u8>)>;
#[derive(Debug,Serialize,Deserialize)]
struct Repository { struct Repository {
#[serde(skip)]
config: Config, config: Config,
/// Flag that the repository needs saving
#[serde(skip)]
dirty: bool,
/// Stored posts
posts: PostsMap, posts: PostsMap,
/// (use_count, data) /// Post data - (use_count, data)
data: DataMap, data: DataMap,
/// Time of last expired posts GC /// Time of last expired posts GC
last_gc_time: Instant, #[serde(with = "serde_chrono_datetime_as_unix")]
last_gc_time: DateTime<Utc>,
} }
impl Repository { impl Repository {
/// New instance
fn new(config: Config) -> Self { fn new(config: Config) -> Self {
Repository { Repository {
config, config,
dirty: false,
posts: Default::default(), posts: Default::default(),
data: Default::default(), data: Default::default(),
last_gc_time: Instant::now(), last_gc_time: Utc::now(),
}
}
fn persist_if_needed(&mut self) -> anyhow::Result<()> {
if self.dirty {
self.persist()
} else {
Ok(())
}
} }
/// Store to a file
fn persist(&mut self) -> anyhow::Result<()> {
debug!("Persist to file: {}", self.config.persist_file);
self.dirty = false;
let file = OpenOptions::new()
.truncate(true)
.write(true)
.create(true)
.open(&self.config.persist_file)?;
if self.config.compression {
let flate = flate2::write::DeflateEncoder::new(file, flate2::Compression::best());
bincode::serialize_into(flate, self)?;
} else {
bincode::serialize_into(file, self)?;
}
Ok(())
}
/// Load from a file
fn load(&mut self) -> anyhow::Result<()> {
debug!("Load from file: {}", self.config.persist_file);
let file = OpenOptions::new()
.read(true)
.open(&self.config.persist_file)?;
let result : Repository = if self.config.compression {
let flate = flate2::read::DeflateDecoder::new(file);
bincode::deserialize_from(flate)?
} else {
bincode::deserialize_from(file)?
};
let old_config = self.config.clone();
std::mem::replace(self, result);
self.config = old_config;
self.dirty = false;
Ok(())
} }
fn serve_delete(&mut self, req : &Request) -> Response { /// Serve a DELETE request
fn serve_delete(&mut self, req: &Request) -> Response {
let post_id = match self.request_to_post_id(req, true) { let post_id = match self.request_to_post_id(req, true) {
Ok(Some(pid)) => pid, Ok(Some(pid)) => pid,
Ok(None) => return error_with_text(400, "Post ID required."), Ok(None) => return error_with_text(400, "Post ID required."),
Err(resp) => return resp Err(resp) => return resp,
}; };
self.delete_post(post_id); self.delete_post(post_id);
@ -116,18 +202,25 @@ impl Repository {
Response::text("Deleted.") Response::text("Deleted.")
} }
fn serve_post_put(&mut self, req : &Request) -> Response { /// Serve a POST or PUT request
///
/// POST inserts a new record
/// PUT updates a record
fn serve_post_put(&mut self, req: &Request) -> Response {
// Post ID is empty for POST, set for PUT
let post_id = match self.request_to_post_id(req, true) { let post_id = match self.request_to_post_id(req, true) {
Ok(pid) => { Ok(pid) => {
if req.method() == "PUT" && pid.is_none() { if req.method() == "PUT" && pid.is_none() {
warn!("PUT without ID!");
return error_with_text(400, "PUT requires a file ID!"); return error_with_text(400, "PUT requires a file ID!");
} else if req.method() == "POST" && pid.is_some() { } else if req.method() == "POST" && pid.is_some() {
warn!("POST with ID!");
return error_with_text(400, "Use PUT to update a file!"); return error_with_text(400, "Use PUT to update a file!");
} }
pid pid
}, }
Err(resp) => return resp Err(resp) => return resp,
}; };
debug!("Submit new data, post ID: {:?}", post_id); debug!("Submit new data, post ID: {:?}", post_id);
@ -136,7 +229,9 @@ impl Repository {
if let Some(body) = req.data() { if let Some(body) = req.data() {
// Read up to 1 byte past the limit to catch too large uploads. // Read up to 1 byte past the limit to catch too large uploads.
// We can't reply on the "Length" field, which is not present with chunked encoding. // We can't reply on the "Length" field, which is not present with chunked encoding.
body.take(self.config.max_file_size as u64 + 1).read_to_end(&mut data).unwrap(); body.take(self.config.max_file_size as u64 + 1)
.read_to_end(&mut data)
.unwrap();
if data.len() > self.config.max_file_size { if data.len() > self.config.max_file_size {
return empty_error(413); return empty_error(413);
} }
@ -144,38 +239,44 @@ impl Repository {
return error_with_text(400, "Empty body!"); return error_with_text(400, "Empty body!");
} }
// Convert "application/x-www-form-urlencoded" to text/plain (CURL uses this)
// NOTE: rouille does NOT parse urlencoded, we will serve the encoded format back if really used.
let mime = match req.header("Content-Type") { let mime = match req.header("Content-Type") {
None => None, None => None,
Some("application/x-www-form-urlencoded") => Some("text/plain"), Some("application/x-www-form-urlencoded") => None,
Some(v) => Some(v), Some(v) => Some(v),
}; };
let expiry = match req.header("X-Expires") { let expiry = match req.header(HDR_EXPIRES) {
Some(text) => { Some(text) => match text.parse() {
match text.parse() {
Ok(v) => { Ok(v) => {
let dur = Duration::from_secs(v); let dur = Duration::from_secs(v);
if dur > self.config.max_expiry { if dur > self.config.max_expiry {
return error_with_text(400, return error_with_text(
format!("Expiration time {} out of allowed range 0-{} s", 400,
format!(
"Expiration time {} out of allowed range 0-{} s",
v, v,
self.config.max_expiry.as_secs() self.config.max_expiry.as_secs()
)); ),
);
} }
Some(dur) Some(dur)
},
Err(_) => {
return error_with_text(400, "Malformed \"X-Expires\", use relative time in seconds.");
},
} }
Err(_) => {
return error_with_text(
400,
"Malformed \"X-Expires\", use relative time in seconds.",
);
} }
None => None },
None => None,
}; };
if let Some(id) = post_id { if let Some(id) = post_id {
// UPDATE // UPDATE
self.update(id, data, mime, expiry); self.update(id, data, mime, expiry);
Response::text("Updated.") Response::text("Updated OK.")
} else { } else {
// INSERT // INSERT
let (id, token) = self.insert(data, mime, expiry.unwrap_or(self.config.default_expiry)); let (id, token) = self.insert(data, mime, expiry.unwrap_or(self.config.default_expiry));
@ -185,11 +286,12 @@ impl Repository {
} }
} }
fn serve_get_head(&mut self, req : &Request) -> Response { /// Serve a GET or HEAD request
fn serve_get_head(&mut self, req: &Request) -> Response {
let post_id = match self.request_to_post_id(req, false) { let post_id = match self.request_to_post_id(req, false) {
Ok(Some(pid)) => pid, Ok(Some(pid)) => pid,
Ok(None) => return error_with_text(400, "Post ID required."), Ok(None) => return error_with_text(400, "Post ID required."),
Err(resp) => return resp Err(resp) => return resp,
}; };
if let Some(post) = self.posts.get(&post_id) { if let Some(post) = self.posts.get(&post_id) {
@ -205,7 +307,10 @@ impl Repository {
Response { Response {
status_code: 200, status_code: 200,
headers: vec![("Content-Type".into(), format!("{}; charset=utf8", post.mime).into())], headers: vec![(
"Content-Type".into(),
format!("{}; charset=utf8", post.mime).into(),
)],
data: if req.method() == "HEAD" { data: if req.method() == "HEAD" {
ResponseBody::empty() ResponseBody::empty()
} else { } else {
@ -220,47 +325,64 @@ impl Repository {
} }
} }
fn request_to_post_id(&self, req : &Request, check_secret : bool) -> Result<Option<PostId>, Response> { /// Extract post ID from a request.
///
/// if `check_secret` is true, ensure a `X-Secret` header contains a valid write token
/// for the post ID.
fn request_to_post_id(
&self,
req: &Request,
check_secret: bool,
) -> Result<Option<PostId>, Response> {
let url = req.url(); let url = req.url();
let stripped = url.trim_matches('/'); let stripped = url.trim_matches('/');
if stripped.is_empty() { if stripped.is_empty() {
// No ID given debug!("No ID given");
return Ok(None); return Ok(None);
} }
if stripped.len() != 16 {
warn!("Bad ID len!");
return Err(Response::empty_404());
}
let id = match u64::from_str_radix(stripped, 16) { let id = match u64::from_str_radix(stripped, 16) {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(_) => { Err(_) => {
return Err(error_with_text(400, "Bad file ID format!")); warn!("ID parsing error: {}", stripped);
}, return Err(Response::empty_404());
}
}; };
if check_secret { if check_secret {
// Check the write token // Check the write token
match self.posts.get(&id) { match self.posts.get(&id) {
None/* | Some(_p) if _p.is_expired()*/ => { None => {
warn!("ID {} does not exist!", id);
return Err(error_with_text(404, "No file with this ID!")); return Err(error_with_text(404, "No file with this ID!"));
}, },
Some(post) => { Some(post) => {
if post.is_expired() { if post.is_expired() {
warn!("Access of expired post!"); warn!("Access of expired file {}!", id);
return Err(error_with_text(404, "No file with this ID!")); return Err(error_with_text(404, "No file with this ID!"));
} }
let secret: u64 = match req.header("X-Secret").map(|v| u64::from_str_radix(v, 16)) { let secret: u64 = match req.header(HDR_SECRET).map(|v| u64::from_str_radix(v, 16)) {
Some(Ok(bytes)) => bytes, Some(Ok(bytes)) => bytes,
None => { None => {
return Err(error_with_text(400, "X-Secret required!")); warn!("Missing secret token!");
return Err(error_with_text(400, "Secret token required!"));
} }
Some(Err(e)) => { Some(Err(e)) => {
warn!("{:?}", e); warn!("Token parse error: {:?}", e);
return Err(error_with_text(400, "Bad secret format!")); return Err(error_with_text(400, "Bad secret token format!"));
}, },
}; };
if post.secret != secret { if post.secret != secret {
return Err(error_with_text(401, "Invalid secret!")); warn!("Secret token mismatch");
return Err(error_with_text(401, "Invalid secret token!"));
} }
}, },
} }
@ -270,15 +392,17 @@ impl Repository {
Ok(Some(id)) Ok(Some(id))
} }
/// Drop expired posts, if cleaning is due
fn gc_expired_posts_if_needed(&mut self) { fn gc_expired_posts_if_needed(&mut self) {
if self.last_gc_time.elapsed() > self.config.expired_gc_interval { if Utc::now().signed_duration_since(self.last_gc_time).to_std().unwrap_or_default() > self.config.expired_gc_interval {
self.gc_expired_posts(); self.gc_expired_posts();
self.last_gc_time = Instant::now(); self.last_gc_time = Utc::now();
} }
} }
/// Drop expired posts
fn gc_expired_posts(&mut self) { fn gc_expired_posts(&mut self) {
debug!("GC expired posts"); debug!("GC expired uploads");
let mut to_rm = vec![]; let mut to_rm = vec![];
for post in &self.posts { for post in &self.posts {
@ -287,6 +411,10 @@ impl Repository {
} }
} }
if !to_rm.is_empty() {
self.dirty = true;
}
for id in to_rm { for id in to_rm {
debug!("Drop post ID {:016x}", id); debug!("Drop post ID {:016x}", id);
if let Some(post) = self.posts.remove(&id) { if let Some(post) = self.posts.remove(&id) {
@ -295,30 +423,37 @@ impl Repository {
} }
} }
fn hash_data(data : &Vec<u8>) -> DataHash { /// Get hash of a byte vector (for deduplication)
fn hash_data(data: &Vec<u8>) -> DataHash {
let mut hasher = siphasher::sip::SipHasher::new(); let mut hasher = siphasher::sip::SipHasher::new();
data.hash(&mut hasher); data.hash(&mut hasher);
hasher.finish() hasher.finish()
} }
fn store_data_or_increment_rc(data_map : &mut DataMap, hash : u64, data: Vec<u8>) { /// Store a data buffer under a given hash.
/// If the buffer is already present in the repository, increment its use count.
fn store_data_or_increment_rc(data_map: &mut DataMap, hash: u64, data: Vec<u8>) {
match data_map.get_mut(&hash) { match data_map.get_mut(&hash) {
None => { None => {
debug!("Store new data hash #{:016x}", hash); debug!("Store new data hash #{:016x}", hash);
data_map.insert(hash, (1, data)); data_map.insert(hash, (1, data));
}, }
Some(entry) => { Some(entry) => {
debug!("Link new use of data hash #{:016x}", hash); debug!("Link new use of data hash #{:016x}", hash);
entry.0 += 1; // increment use counter entry.0 += 1; // increment use counter
}, }
} }
} }
fn drop_data_or_decrement_rc(data_map : &mut DataMap, hash : u64) { /// Drop a data record with the given hash, or decrement its use count if there are other uses
fn drop_data_or_decrement_rc(data_map: &mut DataMap, hash: u64) {
if let Some(old_data) = data_map.get_mut(&hash) { if let Some(old_data) = data_map.get_mut(&hash) {
if old_data.0 > 1 { if old_data.0 > 1 {
old_data.0 -= 1; old_data.0 -= 1;
debug!("Unlink use of data hash #{:016x} ({} remain)", hash, old_data.0); debug!(
"Unlink use of data hash #{:016x} ({} remain)",
hash, old_data.0
);
} else { } else {
debug!("Drop data hash #{:016x}", hash); debug!("Drop data hash #{:016x}", hash);
data_map.remove(&hash); data_map.remove(&hash);
@ -326,13 +461,26 @@ impl Repository {
} }
} }
fn insert(&mut self, data : Vec<u8>, mime : Option<&str>, expires : Duration) -> (PostId, Secret) { /// Insert a post
info!("Insert post with data of len {} bytes, mime {}, expiry {:?}", fn insert(&mut self, data: Vec<u8>, mime: Option<&str>, expires: Duration) -> (PostId, Secret) {
data.len(), mime.unwrap_or("unspecified"), info!(
expires); "Insert post with data of len {} bytes, mime {}, expiry {:?}",
data.len(),
mime.unwrap_or("unspecified"),
expires
);
let hash = Self::hash_data(&data); let hash = Self::hash_data(&data);
let mime = match mime {
None => {
Mime::from(tree_magic::from_u8(&data))
},
Some(explicit) => {
Mime::from(explicit)
},
};
Self::store_data_or_increment_rc(&mut self.data, hash, data); Self::store_data_or_increment_rc(&mut self.data, hash, data);
let post_id = loop { let post_id = loop {
@ -344,26 +492,36 @@ impl Repository {
let secret = OsRng.gen(); let secret = OsRng.gen();
debug!("Data hash = #{:016x}", hash);
debug!("Post ID = #{:016x}", post_id); debug!("Post ID = #{:016x}", post_id);
debug!("Data hash = #{:016x}, mime {}", hash, mime);
debug!("Secret = #{:016x}", secret); debug!("Secret = #{:016x}", secret);
self.posts.insert(post_id, Post { self.posts.insert(
mime: mime.map(ToString::to_string).map(Cow::Owned) post_id,
.unwrap_or(Cow::Borrowed("application/octet-stream")), Post {
mime,
hash, hash,
secret, secret,
expires: Instant::now() + expires expires: Utc::now() + chrono::Duration::from_std(expires).unwrap(), // this is safe unless mis-configured
}); },
);
self.dirty = true;
(post_id, secret) (post_id, secret)
} }
fn update(&mut self, id : PostId, data : Vec<u8>, mime : Option<&str>, expires : Option<Duration>) { /// Update a post by ID
info!("Update post id #{:016x} with data of len {} bytes, mime {}, expiry {}", fn update(&mut self, id: PostId, data: Vec<u8>, mime: Option<&str>, expires: Option<Duration>) {
id, data.len(), mime.unwrap_or("unchanged"), info!(
expires.map(|v| Cow::Owned(format!("{:?}", v))) "Update post id #{:016x} with data of len {} bytes, mime {}, expiry {}",
.unwrap_or("unchanged".into())); id,
data.len(),
mime.unwrap_or("unchanged"),
expires
.map(|v| Cow::Owned(format!("{:?}", v)))
.unwrap_or("unchanged".into())
);
let hash = Self::hash_data(&data); let hash = Self::hash_data(&data);
let post = self.posts.get_mut(&id).unwrap(); // post existence was checked before let post = self.posts.get_mut(&id).unwrap(); // post existence was checked before
@ -374,27 +532,73 @@ impl Repository {
Self::drop_data_or_decrement_rc(&mut self.data, post.hash); Self::drop_data_or_decrement_rc(&mut self.data, post.hash);
Self::store_data_or_increment_rc(&mut self.data, hash, data); Self::store_data_or_increment_rc(&mut self.data, hash, data);
post.hash = hash; post.hash = hash;
self.dirty = true;
} else { } else {
debug!("Data hash = #{:016x} (no change)", hash); debug!("Data hash = #{:016x} (no change)", hash);
} }
if let Some(mime) = mime { if let Some(mime) = mime {
if &post.mime != mime { let new_mime = Mime::from(mime);
if post.mime != new_mime {
debug!("Content type changed to {}", mime); debug!("Content type changed to {}", mime);
post.mime = Cow::Owned(mime.to_string()); post.mime = new_mime;
self.dirty = true;
} }
} }
if let Some(exp) = expires { if let Some(exp) = expires {
debug!("Expiration changed to {:?} from now", exp); debug!("Expiration changed to {:?} from now", exp);
post.expires = Instant::now() + exp; post.expires = Utc::now() + chrono::Duration::from_std(exp).unwrap(); // this is safe unless mis-configured;
self.dirty = true;
} }
} }
fn delete_post(&mut self, id : PostId) { /// Delete a post by ID
fn delete_post(&mut self, id: PostId) {
info!("Delete post id #{:016x}", id); info!("Delete post id #{:016x}", id);
let post = self.posts.remove(&id).unwrap(); // post existence was checked before let post = self.posts.remove(&id).unwrap(); // post existence was checked before
Self::drop_data_or_decrement_rc(&mut self.data, post.hash); Self::drop_data_or_decrement_rc(&mut self.data, post.hash);
self.dirty = true;
}
}
/// Serialize chrono unix timestamp as seconds
mod serde_chrono_datetime_as_unix {
use serde::{self, Deserialize, Deserializer, Serializer};
use chrono::{DateTime, Utc, NaiveDateTime};
pub fn serialize<S>(value: &DateTime<Utc>, se: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
se.serialize_i64(value.naive_utc().timestamp())
}
pub fn deserialize<'de, D>(de: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let ts: i64 = i64::deserialize(de)?;
Ok(DateTime::from_utc(NaiveDateTime::from_timestamp(ts, 0), Utc))
}
}
fn error_with_text(code: u16, text: impl Into<String>) -> Response {
Response {
status_code: code,
headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
data: rouille::ResponseBody::from_string(text),
upgrade: None,
}
}
fn empty_error(code: u16) -> Response {
Response {
status_code: code,
headers: vec![("Content-Type".into(), "text/plain; charset=utf8".into())],
data: rouille::ResponseBody::empty(),
upgrade: None,
} }
} }

@ -0,0 +1,742 @@
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use std::fmt::{Write, Display, Formatter};
use std::fmt;
use serde::de::Visitor;
#[derive(Serialize,Deserialize,Debug,PartialEq,Eq,Hash)]
pub enum Mime {
WellKnown(usize),
Custom(String),
}
impl From<String> for Mime {
fn from(s: String) -> Self {
if let Ok(index) = WELL_KNOWN.binary_search(&s.as_str()) {
Mime::WellKnown(index)
} else {
Mime::Custom(s)
}
}
}
impl<'a> From<&'a str> for Mime {
fn from(s: &'a str) -> Self {
if let Ok(index) = WELL_KNOWN.binary_search(&s) {
Mime::WellKnown(index)
} else {
Mime::Custom(s.to_string())
}
}
}
impl Display for Mime {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Mime::WellKnown(n) => {
if let Some(s) = WELL_KNOWN.get(*n) {
f.write_str(s)
} else {
f.write_str("application/octet-stream")
}
},
Mime::Custom(s) => {
f.write_str(s)
},
}
}
}
// CAUTION!!!!! This list must be alphabetically sorted!
const WELL_KNOWN : &[&str] = &[
"application/andrew-inset",
"application/applixware",
"application/atom+xml",
"application/atomcat+xml",
"application/atomsvc+xml",
"application/ccxml+xml,",
"application/cdmi-capability",
"application/cdmi-container",
"application/cdmi-domain",
"application/cdmi-object",
"application/cdmi-queue",
"application/cu-seeme",
"application/davmount+xml",
"application/dssc+der",
"application/dssc+xml",
"application/ecmascript",
"application/emma+xml",
"application/epub+zip",
"application/exi",
"application/font-tdpfr",
"application/hyperstudio",
"application/ipfix",
"application/java-archive",
"application/java-serialized-object",
"application/java-vm",
"application/javascript",
"application/json",
"application/mac-binhex40",
"application/mac-compactpro",
"application/mads+xml",
"application/marc",
"application/marcxml+xml",
"application/mathematica",
"application/mathml+xml",
"application/mbox",
"application/mediaservercontrol+xml",
"application/metalink4+xml",
"application/mets+xml",
"application/mods+xml",
"application/mp21",
"application/mp4",
"application/msword",
"application/mxf",
"application/octet-stream",
"application/oda",
"application/oebps-package+xml",
"application/ogg",
"application/onenote",
"application/patch-ops-error+xml",
"application/pdf",
"application/pgp-encrypted",
"application/pgp-signature",
"application/pics-rules",
"application/pkcs10",
"application/pkcs7-mime",
"application/pkcs7-signature",
"application/pkcs8",
"application/pkix-attr-cert",
"application/pkix-cert",
"application/pkix-crl",
"application/pkix-pkipath",
"application/pkixcmp",
"application/pls+xml",
"application/postscript",
"application/prs.cww",
"application/pskc+xml",
"application/rdf+xml",
"application/reginfo+xml",
"application/relax-ng-compact-syntax",
"application/resource-lists+xml",
"application/resource-lists-diff+xml",
"application/rls-services+xml",
"application/rsd+xml",
"application/rss+xml",
"application/rtf",
"application/sbml+xml",
"application/scvp-cv-request",
"application/scvp-cv-response",
"application/scvp-vp-request",
"application/scvp-vp-response",
"application/sdp",
"application/set-payment-initiation",
"application/set-registration-initiation",
"application/shf+xml",
"application/smil+xml",
"application/sparql-query",
"application/sparql-results+xml",
"application/srgs",
"application/srgs+xml",
"application/sru+xml",
"application/ssml+xml",
"application/tei+xml",
"application/thraud+xml",
"application/timestamped-data",
"application/vnd.3gpp.pic-bw-large",
"application/vnd.3gpp.pic-bw-small",
"application/vnd.3gpp.pic-bw-var",
"application/vnd.3gpp2.tcap",
"application/vnd.3m.post-it-notes",
"application/vnd.accpac.simply.aso",
"application/vnd.accpac.simply.imp",
"application/vnd.acucobol",
"application/vnd.acucorp",
"application/vnd.adobe.air-application-installer-package+zip",
"application/vnd.adobe.fxp",
"application/vnd.adobe.xdp+xml",
"application/vnd.adobe.xfdf",
"application/vnd.ahead.space",
"application/vnd.airzip.filesecure.azf",
"application/vnd.airzip.filesecure.azs",
"application/vnd.amazon.ebook",
"application/vnd.americandynamics.acc",
"application/vnd.amiga.ami",
"application/vnd.android.package-archive",
"application/vnd.anser-web-certificate-issue-initiation",
"application/vnd.anser-web-funds-transfer-initiation",
"application/vnd.antix.game-component",
"application/vnd.apple.installer+xml",
"application/vnd.apple.mpegurl",
"application/vnd.aristanetworks.swi",
"application/vnd.audiograph",
"application/vnd.blueice.multipass",
"application/vnd.bmi",
"application/vnd.businessobjects",
"application/vnd.chemdraw+xml",
"application/vnd.chipnuts.karaoke-mmd",
"application/vnd.cinderella",
"application/vnd.claymore",
"application/vnd.cloanto.rp9",
"application/vnd.clonk.c4group",
"application/vnd.cluetrust.cartomobile-config",
"application/vnd.cluetrust.cartomobile-config-pkg",
"application/vnd.commonspace",
"application/vnd.contact.cmsg",
"application/vnd.cosmocaller",
"application/vnd.crick.clicker",
"application/vnd.crick.clicker.keyboard",
"application/vnd.crick.clicker.palette",
"application/vnd.crick.clicker.template",
"application/vnd.crick.clicker.wordbank",
"application/vnd.criticaltools.wbs+xml",
"application/vnd.ctc-posml",
"application/vnd.cups-ppd",
"application/vnd.curl.car",
"application/vnd.curl.pcurl",
"application/vnd.data-vision.rdz",
"application/vnd.denovo.fcselayout-link",
"application/vnd.dna",
"application/vnd.dolby.mlp",
"application/vnd.dpgraph",
"application/vnd.dreamfactory",
"application/vnd.dvb.ait",
"application/vnd.dvb.service",
"application/vnd.dynageo",
"application/vnd.ecowin.chart",
"application/vnd.enliven",
"application/vnd.epson.esf",
"application/vnd.epson.msf",
"application/vnd.epson.quickanime",
"application/vnd.epson.salt",
"application/vnd.epson.ssf",
"application/vnd.eszigno3+xml",
"application/vnd.ezpix-album",
"application/vnd.ezpix-package",
"application/vnd.fdf",
"application/vnd.fdsn.seed",
"application/vnd.flographit",
"application/vnd.fluxtime.clip",
"application/vnd.framemaker",
"application/vnd.frogans.fnc",
"application/vnd.frogans.ltf",
"application/vnd.fsc.weblaunch",
"application/vnd.fujitsu.oasys",
"application/vnd.fujitsu.oasys2",
"application/vnd.fujitsu.oasys3",
"application/vnd.fujitsu.oasysgp",
"application/vnd.fujitsu.oasysprs",
"application/vnd.fujixerox.ddd",
"application/vnd.fujixerox.docuworks",
"application/vnd.fujixerox.docuworks.binder",
"application/vnd.fuzzysheet",
"application/vnd.genomatix.tuxedo",
"application/vnd.geogebra.file",
"application/vnd.geogebra.tool",
"application/vnd.geometry-explorer",
"application/vnd.geonext",
"application/vnd.geoplan",
"application/vnd.geospace",
"application/vnd.gmx",
"application/vnd.google-earth.kml+xml",
"application/vnd.google-earth.kmz",
"application/vnd.grafeq",
"application/vnd.groove-account",
"application/vnd.groove-help",
"application/vnd.groove-identity-message",
"application/vnd.groove-injector",
"application/vnd.groove-tool-message",
"application/vnd.groove-tool-template",
"application/vnd.groove-vcard",
"application/vnd.hal+xml",
"application/vnd.handheld-entertainment+xml",
"application/vnd.hbci",
"application/vnd.hhe.lesson-player",
"application/vnd.hp-hpgl",
"application/vnd.hp-hpid",
"application/vnd.hp-hps",
"application/vnd.hp-jlyt",
"application/vnd.hp-pcl",
"application/vnd.hp-pclxl",
"application/vnd.hydrostatix.sof-data",
"application/vnd.hzn-3d-crossword",
"application/vnd.ibm.minipay",
"application/vnd.ibm.modcap",
"application/vnd.ibm.rights-management",
"application/vnd.ibm.secure-container",
"application/vnd.iccprofile",
"application/vnd.igloader",
"application/vnd.immervision-ivp",
"application/vnd.immervision-ivu",
"application/vnd.insors.igm",
"application/vnd.intercon.formnet",
"application/vnd.intergeo",
"application/vnd.intu.qbo",
"application/vnd.intu.qfx",
"application/vnd.ipunplugged.rcprofile",
"application/vnd.irepository.package+xml",
"application/vnd.is-xpr",
"application/vnd.isac.fcs",
"application/vnd.jam",
"application/vnd.jcp.javame.midlet-rms",
"application/vnd.jisp",
"application/vnd.joost.joda-archive",
"application/vnd.kahootz",
"application/vnd.kde.karbon",
"application/vnd.kde.kchart",
"application/vnd.kde.kformula",
"application/vnd.kde.kivio",
"application/vnd.kde.kontour",
"application/vnd.kde.kpresenter",
"application/vnd.kde.kspread",
"application/vnd.kde.kword",
"application/vnd.kenameaapp",
"application/vnd.kidspiration",
"application/vnd.kinar",
"application/vnd.koan",
"application/vnd.kodak-descriptor",
"application/vnd.las.las+xml",
"application/vnd.llamagraphics.life-balance.desktop",
"application/vnd.llamagraphics.life-balance.exchange+xml",
"application/vnd.lotus-1-2-3",
"application/vnd.lotus-approach",
"application/vnd.lotus-freelance",
"application/vnd.lotus-notes",
"application/vnd.lotus-organizer",
"application/vnd.lotus-screencam",
"application/vnd.lotus-wordpro",
"application/vnd.macports.portpkg",
"application/vnd.mcd",
"application/vnd.medcalcdata",
"application/vnd.mediastation.cdkey",
"application/vnd.mfer",
"application/vnd.mfmp",
"application/vnd.micrografx.flo",
"application/vnd.micrografx.igx",
"application/vnd.mif",
"application/vnd.mobius.daf",
"application/vnd.mobius.dis",
"application/vnd.mobius.mbk",
"application/vnd.mobius.mqy",
"application/vnd.mobius.msl",
"application/vnd.mobius.plc",
"application/vnd.mobius.txf",
"application/vnd.mophun.application",
"application/vnd.mophun.certificate",
"application/vnd.mozilla.xul+xml",
"application/vnd.ms-artgalry",
"application/vnd.ms-cab-compressed",
"application/vnd.ms-excel",
"application/vnd.ms-excel.addin.macroenabled.12",
"application/vnd.ms-excel.sheet.binary.macroenabled.12",
"application/vnd.ms-excel.sheet.macroenabled.12",
"application/vnd.ms-excel.template.macroenabled.12",
"application/vnd.ms-fontobject",
"application/vnd.ms-htmlhelp",
"application/vnd.ms-ims",
"application/vnd.ms-lrm",
"application/vnd.ms-officetheme",
"application/vnd.ms-pki.seccat",
"application/vnd.ms-pki.stl",
"application/vnd.ms-powerpoint",
"application/vnd.ms-powerpoint.addin.macroenabled.12",
"application/vnd.ms-powerpoint.presentation.macroenabled.12",
"application/vnd.ms-powerpoint.slide.macroenabled.12",
"application/vnd.ms-powerpoint.slideshow.macroenabled.12",
"application/vnd.ms-powerpoint.template.macroenabled.12",
"application/vnd.ms-project",
"application/vnd.ms-word.document.macroenabled.12",
"application/vnd.ms-word.template.macroenabled.12",
"application/vnd.ms-works",
"application/vnd.ms-wpl",
"application/vnd.ms-xpsdocument",
"application/vnd.mseq",
"application/vnd.musician",
"application/vnd.muvee.style",
"application/vnd.neurolanguage.nlu",
"application/vnd.noblenet-directory",
"application/vnd.noblenet-sealer",
"application/vnd.noblenet-web",
"application/vnd.nokia.n-gage.data",
"application/vnd.nokia.n-gage.symbian.install",
"application/vnd.nokia.radio-preset",
"application/vnd.nokia.radio-presets",
"application/vnd.novadigm.edm",
"application/vnd.novadigm.edx",
"application/vnd.novadigm.ext",
"application/vnd.oasis.opendocument.chart",
"application/vnd.oasis.opendocument.chart-template",
"application/vnd.oasis.opendocument.database",
"application/vnd.oasis.opendocument.formula",
"application/vnd.oasis.opendocument.formula-template",
"application/vnd.oasis.opendocument.graphics",
"application/vnd.oasis.opendocument.graphics-template",
"application/vnd.oasis.opendocument.image",
"application/vnd.oasis.opendocument.image-template",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.presentation-template",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.spreadsheet-template",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.text-master",
"application/vnd.oasis.opendocument.text-template",
"application/vnd.oasis.opendocument.text-web",
"application/vnd.olpc-sugar",
"application/vnd.oma.dd2+xml",
"application/vnd.openofficeorg.extension",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.slide",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"application/vnd.openxmlformats-officedocument.presentationml.template",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.osgeo.mapguide.package",
"application/vnd.osgi.dp",
"application/vnd.palm",
"application/vnd.pawaafile",
"application/vnd.pg.format",
"application/vnd.pg.osasli",
"application/vnd.picsel",
"application/vnd.pmi.widget",
"application/vnd.pocketlearn",
"application/vnd.powerbuilder6",
"application/vnd.previewsystems.box",
"application/vnd.proteus.magazine",
"application/vnd.publishare-delta-tree",
"application/vnd.pvi.ptid1",
"application/vnd.quark.quarkxpress",
"application/vnd.realvnc.bed",
"application/vnd.recordare.musicxml",
"application/vnd.recordare.musicxml+xml",
"application/vnd.rig.cryptonote",
"application/vnd.rim.cod",
"application/vnd.rn-realmedia",
"application/vnd.route66.link66+xml",
"application/vnd.sailingtracker.track",
"application/vnd.seemail",
"application/vnd.sema",
"application/vnd.semd",
"application/vnd.semf",
"application/vnd.shana.informed.formdata",
"application/vnd.shana.informed.formtemplate",
"application/vnd.shana.informed.interchange",
"application/vnd.shana.informed.package",
"application/vnd.simtech-mindmapper",
"application/vnd.smaf",
"application/vnd.smart.teacher",
"application/vnd.solent.sdkm+xml",
"application/vnd.spotfire.dxp",
"application/vnd.spotfire.sfs",
"application/vnd.stardivision.calc",
"application/vnd.stardivision.draw",
"application/vnd.stardivision.impress",
"application/vnd.stardivision.math",
"application/vnd.stardivision.writer",
"application/vnd.stardivision.writer-global",
"application/vnd.stepmania.stepchart",
"application/vnd.sun.xml.calc",
"application/vnd.sun.xml.calc.template",
"application/vnd.sun.xml.draw",
"application/vnd.sun.xml.draw.template",
"application/vnd.sun.xml.impress",
"application/vnd.sun.xml.impress.template",
"application/vnd.sun.xml.math",
"application/vnd.sun.xml.writer",
"application/vnd.sun.xml.writer.global",
"application/vnd.sun.xml.writer.template",
"application/vnd.sus-calendar",
"application/vnd.svd",
"application/vnd.symbian.install",
"application/vnd.syncml+xml",
"application/vnd.syncml.dm+wbxml",
"application/vnd.syncml.dm+xml",
"application/vnd.tao.intent-module-archive",
"application/vnd.tmobile-livetv",
"application/vnd.trid.tpt",
"application/vnd.triscape.mxs",
"application/vnd.trueapp",
"application/vnd.ufdl",
"application/vnd.uiq.theme",
"application/vnd.umajin",
"application/vnd.unity",
"application/vnd.uoml+xml",
"application/vnd.vcx",
"application/vnd.visio",
"application/vnd.visio2013",
"application/vnd.visionary",
"application/vnd.vsf",
"application/vnd.wap.wbxml",
"application/vnd.wap.wmlc",
"application/vnd.wap.wmlscriptc",
"application/vnd.webturbo",
"application/vnd.wolfram.player",
"application/vnd.wordperfect",
"application/vnd.wqd",
"application/vnd.wt.stf",
"application/vnd.xara",
"application/vnd.xfdl",
"application/vnd.yamaha.hv-dic",
"application/vnd.yamaha.hv-script",
"application/vnd.yamaha.hv-voice",
"application/vnd.yamaha.openscoreformat",
"application/vnd.yamaha.openscoreformat.osfpvg+xml",
"application/vnd.yamaha.smaf-audio",
"application/vnd.yamaha.smaf-phrase",
"application/vnd.yellowriver-custom-menu",
"application/vnd.zul",
"application/vnd.zzazz.deck+xml",
"application/voicexml+xml",
"application/widget",
"application/winhlp",
"application/wsdl+xml",
"application/wspolicy+xml",
"application/x-7z-compressed",
"application/x-abiword",
"application/x-ace-compressed",
"application/x-apple-diskimage",
"application/x-authorware-bin",
"application/x-authorware-map",
"application/x-authorware-seg",
"application/x-bcpio",
"application/x-bittorrent",
"application/x-bzip",
"application/x-bzip2",
"application/x-cdlink",
"application/x-chat",
"application/x-chess-pgn",
"application/x-cpio",
"application/x-csh",
"application/x-debian-package",
"application/x-director",
"application/x-doom",
"application/x-dtbncx+xml",
"application/x-dtbook+xml",
"application/x-dtbresource+xml",
"application/x-dvi",
"application/x-font-bdf",
"application/x-font-ghostscript",
"application/x-font-linux-psf",
"application/x-font-otf",
"application/x-font-pcf",
"application/x-font-snf",
"application/x-font-ttf",
"application/x-font-type1",
"application/x-font-woff",
"application/x-futuresplash",
"application/x-gnumeric",
"application/x-gtar",
"application/x-hdf",
"application/x-java-jnlp-file",
"application/x-latex",
"application/x-mobipocket-ebook",
"application/x-ms-application",
"application/x-ms-wmd",
"application/x-ms-wmz",
"application/x-ms-xbap",
"application/x-msaccess",
"application/x-msbinder",
"application/x-mscardfile",
"application/x-msclip",
"application/x-msdownload",
"application/x-msmediaview",
"application/x-msmetafile",
"application/x-msmoney",
"application/x-mspublisher",
"application/x-msschedule",
"application/x-msterminal",
"application/x-mswrite",
"application/x-netcdf",
"application/x-pkcs12",
"application/x-pkcs7-certificates",
"application/x-pkcs7-certreqresp",
"application/x-rar-compressed",
"application/x-sh",
"application/x-shar",
"application/x-shockwave-flash",
"application/x-silverlight-app",
"application/x-stuffit",
"application/x-stuffitx",
"application/x-sv4cpio",
"application/x-sv4crc",
"application/x-tar",
"application/x-tcl",
"application/x-tex",
"application/x-tex-tfm",
"application/x-texinfo",
"application/x-ustar",
"application/x-wais-source",
"application/x-x509-ca-cert",
"application/x-xfig",
"application/x-xpinstall",
"application/xcap-diff+xml",
"application/xenc+xml",
"application/xhtml+xml",
"application/xml",
"application/xml-dtd",
"application/xop+xml",
"application/xslt+xml",
"application/xspf+xml",
"application/xv+xml",
"application/yang",
"application/yin+xml",
"application/zip",
"audio/adpcm",
"audio/basic",
"audio/midi",
"audio/mp4",
"audio/mpeg",
"audio/ogg",
"audio/vnd.dece.audio",
"audio/vnd.digital-winds",
"audio/vnd.dra",
"audio/vnd.dts",
"audio/vnd.dts.hd",
"audio/vnd.lucent.voice",
"audio/vnd.ms-playready.media.pya",
"audio/vnd.nuera.ecelp4800",
"audio/vnd.nuera.ecelp7470",
"audio/vnd.nuera.ecelp9600",
"audio/vnd.rip",
"audio/webm",
"audio/x-aac",
"audio/x-aiff",
"audio/x-mpegurl",
"audio/x-ms-wax",
"audio/x-ms-wma",
"audio/x-pn-realaudio",
"audio/x-pn-realaudio-plugin",
"audio/x-wav",
"chemical/x-cdx",
"chemical/x-cif",
"chemical/x-cmdf",
"chemical/x-cml",
"chemical/x-csml",
"chemical/x-xyz",
"image/bmp",
"image/cgm",
"image/g3fax",
"image/gif",
"image/ief",
"image/jpeg",
"image/ktx",
"image/pjpeg",
"image/png",
"image/prs.btif",
"image/svg+xml",
"image/tiff",
"image/vnd.adobe.photoshop",
"image/vnd.dece.graphic",
"image/vnd.djvu",
"image/vnd.dvb.subtitle",
"image/vnd.dwg",
"image/vnd.dxf",
"image/vnd.fastbidsheet",
"image/vnd.fpx",
"image/vnd.fst",
"image/vnd.fujixerox.edmics-mmr",
"image/vnd.fujixerox.edmics-rlc",
"image/vnd.ms-modi",
"image/vnd.net-fpx",
"image/vnd.wap.wbmp",
"image/vnd.xiff",
"image/webp",
"image/x-citrix-jpeg",
"image/x-citrix-png",
"image/x-cmu-raster",
"image/x-cmx",
"image/x-freehand",
"image/x-icon",
"image/x-pcx",
"image/x-pict",
"image/x-png",
"image/x-portable-anymap",
"image/x-portable-bitmap",
"image/x-portable-graymap",
"image/x-portable-pixmap",
"image/x-rgb",
"image/x-xbitmap",
"image/x-xpixmap",
"image/x-xwindowdump",
"message/rfc822",
"model/iges",
"model/mesh",
"model/vnd.collada+xml",
"model/vnd.dwf",
"model/vnd.gdl",
"model/vnd.gtw",
"model/vnd.mts",
"model/vnd.vtu",
"model/vrml",
"text/calendar",
"text/css",
"text/csv",
"text/html",
"text/n3",
"text/plain",
"text/plain-bas",
"text/prs.lines.tag",
"text/richtext",
"text/sgml",
"text/tab-separated-values",
"text/troff",
"text/turtle",
"text/uri-list",
"text/vnd.curl",
"text/vnd.curl.dcurl",
"text/vnd.curl.mcurl",
"text/vnd.curl.scurl",
"text/vnd.fly",
"text/vnd.fmi.flexstor",
"text/vnd.graphviz",
"text/vnd.in3d.3dml",
"text/vnd.in3d.spot",
"text/vnd.sun.j2me.app-descriptor",
"text/vnd.wap.wml",
"text/vnd.wap.wmlscript",
"text/x-asm",
"text/x-c",
"text/x-fortran",
"text/x-java-source,java",
"text/x-pascal",
"text/x-setext",
"text/x-uuencode",
"text/x-vcalendar",
"text/x-vcard",
"text/yaml",
"video/3gpp",
"video/3gpp2",
"video/h261",
"video/h263",
"video/h264",
"video/jpeg",
"video/jpm",
"video/mj2",
"video/mp4",
"video/mpeg",
"video/ogg",
"video/quicktime",
"video/vnd.dece.hd",
"video/vnd.dece.mobile",
"video/vnd.dece.pd",
"video/vnd.dece.sd",
"video/vnd.dece.video",
"video/vnd.fvt",
"video/vnd.mpegurl",
"video/vnd.ms-playready.media.pyv",
"video/vnd.uvvu.mp4",
"video/vnd.vivo",
"video/webm",
"video/x-f4v",
"video/x-fli",
"video/x-flv",
"video/x-m4v",
"video/x-ms-asf",
"video/x-ms-wm",
"video/x-ms-wmv",
"video/x-ms-wmx",
"video/x-ms-wvx",
"video/x-msvideo",
"video/x-sgi-movie",
"x-conference/x-cooltalk",
];
Loading…
Cancel
Save