Compare commits
7 Commits
eb79a65500
...
614bdc49b5
Author | SHA1 | Date |
---|---|---|
Emi Simpson | 614bdc49b5 | |
Emi Simpson | aa145f9a6d | |
Emi Simpson | 7c2eb4d88e | |
Emi Simpson | b3a6c51ff8 | |
Emi Simpson | 04417a96a4 | |
Emi Simpson | 4dc5ffa936 | |
Emi Simpson | 0530bf42f6 |
|
@ -97,6 +97,7 @@ dependencies = [
|
|||
"itertools",
|
||||
"mime",
|
||||
"protobuf",
|
||||
"sanitise-file-name",
|
||||
"ureq",
|
||||
"webp",
|
||||
]
|
||||
|
@ -604,6 +605,12 @@ dependencies = [
|
|||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sanitise-file-name"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d36299972b96b8ae7e8f04ecbf75fb41a27bf3781af00abcf57609774cb911"
|
||||
|
||||
[[package]]
|
||||
name = "scoped_threadpool"
|
||||
version = "0.1.9"
|
||||
|
|
|
@ -14,6 +14,7 @@ blurhash = "0.1.1"
|
|||
base64 = "0.13.0"
|
||||
mime = "0.3.16"
|
||||
protobuf = "3.1.0"
|
||||
sanitise-file-name = "1.0.0"
|
||||
|
||||
[dependencies.iota-crypto]
|
||||
version = "0.13.0"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crypto::Error;
|
||||
use crypto::ciphers::{
|
||||
aes_gcm::Aes256Gcm,
|
||||
traits::Aead,
|
||||
|
@ -19,6 +20,23 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn decrypt<'p>(key: &[u8; 32], encrypted: &[u8], plaintext_dest: &'p mut Vec<u8>) -> Option<&'p mut Vec<u8>> {
|
||||
let (cyphertext, tag) = encrypted.split_at(encrypted.len() - 16);
|
||||
plaintext_dest.resize(cyphertext.len(), 0);
|
||||
let e = Aes256Gcm::try_decrypt(
|
||||
key,
|
||||
&NONCE,
|
||||
&[],
|
||||
plaintext_dest,
|
||||
cyphertext,
|
||||
tag);
|
||||
match e {
|
||||
Ok(_) => Some(plaintext_dest),
|
||||
Err(Error::CipherError { alg: "AES-256-GCM" }) => None, // Invalid key or tag
|
||||
Err(e) => panic!("Unexpected decryption return value: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_key() -> [u8; 32] {
|
||||
let mut result = [0; 32];
|
||||
rand::fill(&mut result).expect("Unexpected error generating random data");
|
||||
|
|
103
src/main.rs
103
src/main.rs
|
@ -5,14 +5,16 @@ mod thumbnailing;
|
|||
mod protobuf;
|
||||
mod errors;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io::Read;
|
||||
use std::fs::File;
|
||||
use std::{borrow::Cow, io::stdout};
|
||||
use std::io::{Read, Write};
|
||||
use std::fs::{File, self, OpenOptions};
|
||||
use std::path::Path;
|
||||
|
||||
use errors::AviaryError;
|
||||
use itertools::{Itertools, Either};
|
||||
use parse::{CreateArgs, Command, DownloadArgs};
|
||||
use ::protobuf::Message;
|
||||
use sanitise_file_name::sanitise_with_options;
|
||||
|
||||
fn trim_url<'a>(base_url: &str, url: &'a str) -> Option<&'a str> {
|
||||
if url.starts_with(base_url) {
|
||||
|
@ -33,6 +35,19 @@ fn trim_url<'a>(base_url: &str, url: &'a str) -> Option<&'a str> {
|
|||
|
||||
fn main() {
|
||||
let args: parse::Args = argh::from_env();
|
||||
let server_no_trailing_slash = args.server.trim_end_matches('/');
|
||||
let full_server = if server_no_trailing_slash.starts_with("http") {
|
||||
Cow::Borrowed(server_no_trailing_slash)
|
||||
} else {
|
||||
Cow::Owned(format!("https://{}", server_no_trailing_slash))
|
||||
};
|
||||
match args.command {
|
||||
Command::Create(create_args) => create(&*full_server, create_args),
|
||||
Command::Download(download_args) => download(&*full_server, download_args),
|
||||
}
|
||||
}
|
||||
|
||||
fn create(server: &str, args: CreateArgs) {
|
||||
print!("Checking files...");
|
||||
let (files, errors): (Vec<_>, Vec<_>) = args.images.iter()
|
||||
.map(Path::new)
|
||||
|
@ -61,13 +76,9 @@ fn main() {
|
|||
} else {
|
||||
println!(" \x1b[32mDone!\n");
|
||||
let agent = upload::get_agent();
|
||||
let full_server = if args.server.starts_with("http") {
|
||||
Cow::Borrowed(&args.server)
|
||||
} else {
|
||||
Cow::Owned(format!("https://{}", args.server))
|
||||
};
|
||||
let index_url = files.into_iter()
|
||||
.inspect(|(path, _)| print!("\x1b[36m{}\x1b[0m\n\x1b[37m├─\x1b[0m Reading... ", path.display()))
|
||||
.inspect(|_| drop(stdout().flush()))
|
||||
.map(|(path, mut file)|
|
||||
(|| {
|
||||
let mut buff = Vec::with_capacity(file.metadata()?.len() as usize);
|
||||
|
@ -76,12 +87,14 @@ fn main() {
|
|||
})().map_err(|e| AviaryError::StreamReadError(path.to_owned(), e))
|
||||
)
|
||||
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Thumbnailing... ") })
|
||||
.inspect(|_| drop(stdout().flush()))
|
||||
.map(|r| r.and_then(|(path, raw_dat)| {
|
||||
let (thumbnail, blurhash) = thumbnailing::thumbnail(&raw_dat)
|
||||
.map_err(|_| AviaryError::ImageFormatError(path.to_owned()))?;
|
||||
Ok((raw_dat, thumbnail, blurhash))
|
||||
}))
|
||||
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Encrypting... ")})
|
||||
.inspect(|_| drop(stdout().flush()))
|
||||
.map_ok(|(raw_dat, thumbnail, blurhash)| {
|
||||
let key = crypto::make_key();
|
||||
(
|
||||
|
@ -92,16 +105,17 @@ fn main() {
|
|||
)
|
||||
})
|
||||
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m└─\x1b[0m Uploading... ")})
|
||||
.inspect(|_| drop(stdout().flush()))
|
||||
.map(|r| r.and_then(|(key, full_img, thumb, blurhash)|
|
||||
upload::put_data(&agent, &*full_server, &thumb)
|
||||
upload::put_data(&agent, server, &thumb)
|
||||
.and_then(|thumb_url|
|
||||
upload::put_data(&agent, &*full_server, &full_img)
|
||||
upload::put_data(&agent, server, &full_img)
|
||||
.map(|full_url| (key, full_url, thumb_url, blurhash)))
|
||||
.map_err(AviaryError::from_upload_error)
|
||||
))
|
||||
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash)| {
|
||||
let full_trimmed = trim_url(&*full_server, &full_url);
|
||||
let thmb_trimmed = trim_url(&*full_server, &thumb_url);
|
||||
let full_trimmed = trim_url(server, &full_url);
|
||||
let thmb_trimmed = trim_url(server, &thumb_url);
|
||||
if let (Some(full_url), Some(thmb_url)) = (full_trimmed, thmb_trimmed) {
|
||||
Ok((key, full_url.to_owned(), thmb_url.to_owned(), blurhash))
|
||||
} else {
|
||||
|
@ -130,7 +144,7 @@ fn main() {
|
|||
);
|
||||
let encoded_key = base64::encode(index_key);
|
||||
print!("\x1b[0mUploading index... ");
|
||||
upload::put_data(&agent, &*full_server, &encrypted_index)
|
||||
upload::put_data(&agent, server, &encrypted_index)
|
||||
.map_err(|e| AviaryError::from_upload_error(e))
|
||||
.map(|url| format!("{}#{}", url.trim().trim_end_matches(".bin"), &encoded_key))
|
||||
});
|
||||
|
@ -142,3 +156,66 @@ fn main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SANITIZATION_OPTIONS: sanitise_file_name::Options<fn(char) -> Option<char>> = sanitise_file_name::Options {
|
||||
length_limit: 127,
|
||||
reserve_extra: 0,
|
||||
extension_cleverness: false,
|
||||
most_fs_safe: true,
|
||||
windows_safe: true,
|
||||
url_safe: false,
|
||||
normalise_whitespace: false,
|
||||
trim_spaces_and_full_stops: true,
|
||||
trim_more_punctuation: false,
|
||||
remove_control_characters: true,
|
||||
remove_reordering_characters: false,
|
||||
replace_with: |_| Some('_'),
|
||||
collapse_replacements: true,
|
||||
six_measures_of_barley: "Unnamed-Album"
|
||||
};
|
||||
|
||||
fn download(server: &str, args: DownloadArgs) {
|
||||
let mut download_buffer = Vec::with_capacity(5_000_000);
|
||||
let mut decrypt_buffer = Vec::with_capacity(5_000_000);
|
||||
let index_url = format!("{}/{}.bin", server, args.id);
|
||||
let key: [u8; 32] = base64::decode(args.key)
|
||||
.expect("key is malformed b64")
|
||||
.try_into()
|
||||
.expect("key is the wrong size");
|
||||
let download_agent = upload::get_agent();
|
||||
let encrypted_index = upload::get_data(&download_agent, &index_url, 500_000, &mut download_buffer)
|
||||
.expect("GET failed");
|
||||
let serialized_index = crypto::decrypt(&key, &encrypted_index, &mut decrypt_buffer)
|
||||
.expect("wrong key or corrupt index");
|
||||
let index = protobuf::index::Index::parse_from_bytes(&serialized_index)
|
||||
.expect("malformed index");
|
||||
|
||||
let dest_dir: Cow<Path> = args.output.map(Cow::Owned)
|
||||
.unwrap_or_else(||
|
||||
index.title.map(|title|
|
||||
Cow::Owned(sanitise_with_options(&title, &SANITIZATION_OPTIONS).into()))
|
||||
.unwrap_or(Cow::Borrowed("Unnamed-Album".as_ref())));
|
||||
fs::create_dir_all(&dest_dir).expect("Failed to create destination directory");
|
||||
for (indx, image) in index.images.into_iter().enumerate() {
|
||||
let path = dest_dir.join(format!("{indx:03}.webp"));
|
||||
let mut dest_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(path)
|
||||
.expect("Failed to open");
|
||||
let key = image.key.try_into().expect("Invalid key size");
|
||||
let encrypted_thumbnail = upload::get_data(
|
||||
&download_agent,
|
||||
&format!("{}/{}.bin", server, image.thumb_url),
|
||||
7680_000,
|
||||
&mut download_buffer
|
||||
).expect("Failed to retrieve image data");
|
||||
let decrypted_thumb = crypto::decrypt(
|
||||
&key,
|
||||
encrypted_thumbnail,
|
||||
&mut decrypt_buffer
|
||||
).expect("Invalid image data referenced by index or bad key in index");
|
||||
dest_file.write_all(&decrypted_thumb).expect("Failed to write to disk");
|
||||
}
|
||||
}
|
||||
|
|
53
src/parse.rs
53
src/parse.rs
|
@ -1,17 +1,64 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use argh_derive::FromArgs;
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// Create Aviary galleries
|
||||
pub struct Args {
|
||||
/// the title of the gallery
|
||||
#[argh(option, short = 't')]
|
||||
pub title: Option<String>,
|
||||
|
||||
/// the null pointer server to use
|
||||
#[argh(option, short = 's', default = "\"envs.sh\".to_owned()")]
|
||||
pub server: String,
|
||||
|
||||
#[argh(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand)]
|
||||
pub enum Command {
|
||||
Create(CreateArgs),
|
||||
Download(DownloadArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand, name = "create")]
|
||||
/// upload a series of images to a new gallery
|
||||
pub struct CreateArgs {
|
||||
/// the title of the gallery
|
||||
#[argh(option, short = 't')]
|
||||
pub title: Option<String>,
|
||||
|
||||
/// A list of image files to post
|
||||
#[argh(positional)]
|
||||
pub images: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand, name = "download")]
|
||||
/// download a gallery
|
||||
pub struct DownloadArgs {
|
||||
#[argh(positional)]
|
||||
/// the file id
|
||||
///
|
||||
/// This is a short series of characters that identifies the file
|
||||
/// For the url:
|
||||
/// https://0x0.st/asdj.bin#omONdEzrY6SfdBgHn/2P6yG33PeIhuj3/SGm/lDhd2U=
|
||||
/// the file id is asdj
|
||||
pub id: String,
|
||||
|
||||
#[argh(positional)]
|
||||
/// the encryption key
|
||||
///
|
||||
/// This is the text after the # sign which is the secret for decrypting the pictures.
|
||||
/// For the url:
|
||||
/// https://0x0.st/asdj.bin#omONdEzrY6SfdBgHn/2P6yG33PeIhuj3/SGm/lDhd2U=
|
||||
/// the encryption key is omONdEzrY6SfdBgHn/2P6yG33PeIhuj3/SGm/lDhd2U=
|
||||
pub key: String,
|
||||
|
||||
#[argh(option, short='o')]
|
||||
/// the directory to put the new files into
|
||||
///
|
||||
/// A directory will be generated based on the name of the gallery if this is not set
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::io::Read;
|
|||
use ureq::{Agent, AgentBuilder};
|
||||
|
||||
const DATA_HEADER: &'static [u8] = b"--FuckLiterallyAllCops\r\nContent-Disposition: form-data; name=\"file\"; filename=\"pigeon.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n";
|
||||
const DATA_FOOTER: &'static [u8] = b"\x00\r\n--FuckLiterallyAllCops--\r\n";
|
||||
const DATA_FOOTER: &'static [u8] = b"\r\n--FuckLiterallyAllCops--\r\n";
|
||||
|
||||
pub fn get_agent() -> Agent {
|
||||
AgentBuilder::new()
|
||||
|
@ -18,3 +18,15 @@ pub fn put_data(agent: &Agent, server: &str, data: &[u8]) -> Result<String, ureq
|
|||
.set("Content-Type", "multipart/form-data; boundary=FuckLiterallyAllCops")
|
||||
.send(DATA_HEADER.chain(data).chain(DATA_FOOTER))?.into_string()?)
|
||||
}
|
||||
|
||||
pub fn get_data<'b>(agent: &Agent, location: &str, size_limit: u64, buffer: &'b mut Vec<u8>) -> Result<&'b mut Vec<u8>, ureq::Error> {
|
||||
buffer.clear();
|
||||
println!("{}", location);
|
||||
let read = agent.get(location)
|
||||
.call()?
|
||||
.into_reader()
|
||||
.take(size_limit)
|
||||
.read_to_end(buffer)?;
|
||||
println!("{}", read);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue