Compare commits

...

73 Commits

Author SHA1 Message Date
b1ek c534a28ad7
filed: explain about .env and config/filed.toml for filed 2024-03-09 19:55:02 +10:00
blek 830677cc0e
fix filed prod build errors 2023-12-18 00:59:14 +10:00
blek 772c5b9bf2
fix compile warnings 2023-12-14 21:04:26 +10:00
blek 18f525e0a2
include changes as requested 2023-12-14 21:04:26 +10:00
blek 66c114b203
properly implement delete method according to config 2023-12-14 21:04:26 +10:00
blek 15954258f5
remove an unused import 2023-12-14 21:04:26 +10:00
blek 27b1919416
make the get_all api method actually work 2023-12-14 21:04:26 +10:00
blek 605a0f76ff
fix stupid mistakes in config checks 2023-12-14 21:04:25 +10:00
blek 7e9d387cc2
improve the documentation on api upload method 2023-12-14 21:04:25 +10:00
blek 616ceb2ad7
add config checks to api upload 2023-12-14 21:04:25 +10:00
blek e2d2d3120d
add bodies to responses 2023-12-14 21:04:25 +10:00
blek 799baa80b5
add all the functionality into the upload api 2023-12-14 21:04:25 +10:00
blek 739bf5e370
fix any request being treated as a GUI upload form 2023-12-14 21:04:25 +10:00
blek 8de83749a3
add upload route 2023-12-14 21:04:24 +10:00
blek b4f804d39b
add file password to /api/files/upload swagger spec 2023-12-14 21:04:24 +10:00
blek 5944d73d6c
implement the /api/files/delete method 2023-12-14 21:04:24 +10:00
blek 103c7c4c8d
respect the config.api.METHOD config values 2023-12-14 21:04:24 +10:00
blek a10333ae16
move out the api check code 2023-12-14 21:04:24 +10:00
blek 1b94c87ffd
hardcode the midfix variable 2023-12-14 21:04:24 +10:00
blek b285a6476e
refactor code for readability 2023-12-14 21:04:24 +10:00
blek ac59ee4648
fix #23 2023-12-14 21:04:23 +10:00
blek 95013d49de
mention the contributing guide in readme 2023-12-14 21:04:23 +10:00
blek b925b2aaca
add contributing guide 2023-12-14 21:04:23 +10:00
blek 55b299b524
print warning in resourceD if no resources are specified and report number of resources on startup 2023-12-14 21:04:23 +10:00
blek 231e26da4a
add animation to the header 2023-12-14 21:04:23 +10:00
blek aeccccd9ec
add html-minifier to rust dev dockerfile 2023-12-14 21:04:23 +10:00
blek 7e2b2bc636
improve dragndrop zone size handling on mobile 2023-12-14 21:04:22 +10:00
blek 2ed60cd045
alternative display for header in mobile mode 2023-12-14 21:04:22 +10:00
blek 8abab247c4
print a warning if resourced is disabled 2023-12-14 21:04:22 +10:00
blek f0d7f8fd8c
deploy resourced to production dockerfile
Note: I did not test the compose file on my machine before submitting the commit. May or may not work.
2023-12-14 21:04:22 +10:00
blek 15c94b8949
deploy resourceD within dev compose file 2023-12-14 21:04:22 +10:00
blek bdd197857e
add air to resourceD
more info: https://github.com/cosmtrek/air
2023-12-14 21:04:21 +10:00
blek b306ab3eb6
add swagger reference 2023-12-14 21:04:21 +10:00
blek 77970e86e7
add route to get if the resourced is enabled 2023-12-14 21:04:21 +10:00
blek b1981db998
fix wrong symbols in the example config 2023-12-14 21:04:21 +10:00
blek f45f52a718
add option to configure min size to cache proxy 2023-12-14 21:04:21 +10:00
blek eb2afaf6d0
cached proxied resources 2023-12-14 21:04:21 +10:00
blek 9e7c207075
proxied resources 2023-12-14 21:04:20 +10:00
blek 7e664b311e
support for http(s):// urls 2023-12-14 21:04:20 +10:00
blek 7e23b3aea0
actually serve the resources 2023-12-14 21:04:20 +10:00
blek 3624cfe568
init repo 2023-12-14 21:04:17 +10:00
blek 9ca7174c85
fix a few problems with template sources 2023-12-14 21:04:17 +10:00
blek 68471b6e4f
minify templates on compile time 2023-12-14 21:04:16 +10:00
blek 4f65ec5229
fix drag and drop size 2023-12-14 21:04:16 +10:00
blek c01b4ea73f
add a ToS switch 2023-12-14 21:04:16 +10:00
blek a9955f5e99
make code blocks always in one line 2023-12-14 21:04:16 +10:00
blek 99e3f8ba67
match colors with the console UI 2023-12-14 21:04:16 +10:00
blek 60f6d973c8
web UI for curlapi/help 2023-12-14 21:04:16 +10:00
blek 82acf6cb50
curl API help 2023-12-14 21:04:15 +10:00
blek 6b4ddb8756
fix compile warning 2023-12-14 21:04:15 +10:00
blek 67ba2308b7
scratch curl API 2023-12-14 21:04:15 +10:00
blek 634afe00fe
fix wrong .env.example time format 2023-12-14 21:04:15 +10:00
blek 5e06986ae1
fix typo 2023-12-14 21:04:15 +10:00
blek 4b7899e8d4
get rid of function panics in the build.rs 2023-12-14 21:04:15 +10:00
blek b81ebab96e
fix filed dockerfile and use alpine as a base image 2023-12-14 21:04:14 +10:00
blek 3cba73fc1a
fix prod dockerfile build error 2023-12-14 21:04:14 +10:00
blek 052d40dc8c
bump Cargo.toml version 2023-12-14 21:04:14 +10:00
blek 62869da989
print version at the footer 2023-12-14 21:04:14 +10:00
blek 7bcc5e11b9
fix few obscure errors 2023-12-14 21:04:14 +10:00
blek a2020a57be
display errors 2023-12-14 21:04:14 +10:00
blek 22f9c9ac78
include branch data 2023-12-14 21:04:13 +10:00
blek 6904a670a8
move out code to a function 2023-12-14 21:04:13 +10:00
blek 98c22d2408
load commit hash at compile time 2023-12-14 21:04:13 +10:00
blek ce7f70ad7b
remove warp static dir in favor of static_dir! macro 2023-12-14 21:04:13 +10:00
blek 545d19c42f
add note about extensive configuration 2023-12-14 21:04:13 +10:00
blek 1e7783154c
init: DEPLOYING.md 2023-12-14 21:04:13 +10:00
blek cd87766fca
add password field to /api/files/upload method 2023-12-14 21:04:12 +10:00
blek 49dae841bc
add js UX for pass entry 2023-12-14 21:04:12 +10:00
blek fc036b82e2
check for password server side 2023-12-14 21:04:12 +10:00
blek d2e32d6d38
add password request on upload page 2023-12-14 21:04:12 +10:00
blek 98b4934ddb
add upload_pass config option 2023-12-14 21:04:12 +10:00
blek 8a41d4bef2
add instances list 2023-10-27 10:39:29 +10:00
blek b0cc93c488
remove the indev banner 2023-10-27 10:26:55 +10:00
61 changed files with 1788 additions and 111 deletions

17
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,17 @@
# Contributing to blek! File
blek! File is a project that aims for high quality standarts,
which means that contributions will be reviewed accordingly.
This doesnt mean that you shouldn't contribute if you have low skills;
it means that you should be ready to expect to make changes in your code as it is reviewed.
## Argument rules
The BDFL of the project is: `blek! <me@blek.codes>`. BDFL's role is to make a final call in case if an argument gets stalled or too controversial.
Based criticism and analysis is the only things that matter within an argument inside this project. Basic your argument on personal emotions against another person will not be tolerated.
## Expected workflow
The expected workflow is:
1. Fork
2. Commit changes
3. Open a PR
4. Merge

33
DEPLOYING.md Normal file
View File

@ -0,0 +1,33 @@
# Deploying a production instance
Hi fellow sysadmins!
First of all, I want to thank you for using my piece of software.
The instructions can be found below
## Deploying a basic instance
To deploy a basic instance for general public use, follow these simple steps:
1. Clone this repo
2. Copy `docker-compose.prod.yml` to `docker-compose.yml` and edit it to fit your environment
3. Now, there are a few config files that need to be edited by you: `.env`, `filed/.env` and `janitord/.env`. Each directory contains an `.env.example`, and the configuration is pretty straightforward. However, if you are lost check this out: [filed config](#filed-configuration), [janitord config](#janitord-configuration).
4. Configure fileD using `filed/config/filed.toml`. The example is in the same folder. Example contains a lot of self-documenting comments, so it should be pretty simple too.
5. Set `REDIS_PASS` to a secure long string. Not exactly required, but this is something you would want to do
6. Create and start containers with `docker-compose up -d`
7. Route your top level reverse proxy to the `caddy` service or to the port that you opened via the docker compose file.
## More extensive configuration
Well, generally, time-wise, it is not really a good idea to create a custom services configuration.
However, I will guide you through the basic minimal configuration.
Basically, the most minimal blek!File is a fileD service connected to a redis database.
I think that if you are clinically insance, you can set these up as a systemd services or a `screen`ed program.
However, its not really recommended to run this without janitorD as unused files will just clog up your filesystem.
The two requirements for janitorD are to have access to the fileD's usercontent directory and the Redis database.
The default docker configuration mounts `/opt/user_content` to the same volume for both fileD and janitorD.
## FileD configuration
Unless you are running in some kind of super customized docker compose environment, just copying the `.env.example` to `.env` should be enough to get it to run.
Don't forget to set the `REDIS_PASS` to the same value across all services
## JanitorD configuration
Same as [filed config](#filed-configuration), don't forget to set `REDIS_PASS` to a valid value

View File

@ -1,10 +1,3 @@
| ⚠️ This is in a rather early stage of development and shouldn't be deployed |
| --------------------------------------------------------------------------- |
Even though this project is mature enough to be deployed in a public instance,
this is highly discouraged.
However, if you do this, be prepared for [DOS](https://en.wikipedia.org/wiki/Denial-of-service_attack) issues and API changes.
<br/>
<h1 align='center'> <h1 align='center'>
<img src="./filed/static/android-chrome-192x192.png"/> <img src="./filed/static/android-chrome-192x192.png"/>
@ -16,6 +9,24 @@ blek! File is a free service that would help you with file sharing.
The principle is very simple: you upload a file, then download it from another device. The file will be deleted after 1 download or 30 minutes. The principle is very simple: you upload a file, then download it from another device. The file will be deleted after 1 download or 30 minutes.
## Public instances
List of official instances
| Name | Administrator | URL |
| --- | --- | --- |
| 🌠 blek! File | b1ek &lt;me@blek.codes&gt; | [https://file.blek.codes](file.blek.codes) |
To add your instance in this list, fork and open a PR.
To qualify, your instance must be having:
1. Uploads without a password turned on
2. Have proper ToS
3. Come up with a unique name
4. Have a public administrator email
## Contributing
Contributing guide is available [here](./CONTRIBUTING.md) (the CONTRIBUTING.md file).
## Licensing ## Licensing
This software is released under GPL3 license, a copyleft license that protects users' freedom by ensuring that all future copies of this software are open source as well. This software is released under GPL3 license, a copyleft license that protects users' freedom by ensuring that all future copies of this software are open source as well.

View File

@ -4,4 +4,8 @@
uri * strip_prefix /qr uri * strip_prefix /qr
reverse_proxy http://qr reverse_proxy http://qr
} }
handle /resource/* {
uri * strip_prefix /resource
reverse_proxy http://resourced
}
} }

View File

@ -0,0 +1,10 @@
FROM golang:alpine
RUN go install github.com/cosmtrek/air@latest
WORKDIR /opt/code
# The directory will be mounted
# COPY . .
CMD [ "air", "-c", ".air.toml" ]

View File

@ -7,6 +7,6 @@ RUN cargo install cargo-watch && \
RUN apt update && \ RUN apt update && \
apt install nodejs npm -y --no-install-recommends && \ apt install nodejs npm -y --no-install-recommends && \
npm i -g uglify-js npm i -g uglify-js html-minifier
CMD [ "/opt/code/dev-entry.sh" ] CMD [ "/opt/code/dev-entry.sh" ]

View File

@ -7,6 +7,7 @@ services:
networks: networks:
bfile: bfile:
volumes: volumes:
- './.git:/opt/code/.git'
- './filed:/opt/code' - './filed:/opt/code'
- './filed/config:/etc/filed' - './filed/config:/etc/filed'
- '/opt/code/target' - '/opt/code/target'
@ -20,6 +21,15 @@ services:
volumes: volumes:
- './janitor:/opt/code' - './janitor:/opt/code'
- './volatile/files:/opt/user_uploads' - './volatile/files:/opt/user_uploads'
resourced:
build:
context: containers
dockerfile: go-dev.Dockerfile
networks:
bfile:
volumes:
- './resource:/opt/code'
- '/opt/code/tmp'
caddy: caddy:
image: caddy:alpine image: caddy:alpine
volumes: volumes:

View File

@ -2,8 +2,8 @@ version: '3.7'
services: services:
filed: filed:
build: build:
context: filed context: .
dockerfile: Dockerfile.prod dockerfile: filed/Dockerfile.prod
networks: networks:
bfile: bfile:
volumes: volumes:
@ -19,6 +19,15 @@ services:
volumes: volumes:
- './janitor:/config:ro' - './janitor:/config:ro'
- './volatile/files:/opt/user_uploads' - './volatile/files:/opt/user_uploads'
resourced:
build:
context: resource
dockerfile: Dockerfile.prod
networks:
bfile:
pid: host # prefork
volumes:
- './resource:/opt/cont'
caddy: caddy:
image: caddy:alpine image: caddy:alpine
volumes: volumes:

2
filed/Cargo.lock generated
View File

@ -358,7 +358,7 @@ dependencies = [
[[package]] [[package]]
name = "filed" name = "filed"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"askama", "askama",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "filed" name = "filed"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -2,16 +2,17 @@
FROM rust:alpine as builder FROM rust:alpine as builder
WORKDIR /opt/build WORKDIR /opt/build
COPY . . COPY filed .
COPY ./.git ./.git
RUN apk add --no-cache musl-dev upx nodejs yarn && \ RUN apk add --no-cache git musl-dev upx nodejs yarn && \
yarn global add uglify-js yarn global add uglify-js@3.17.4 html-minifier
RUN cargo b -r RUN cargo b -r
RUN strip target/release/filed && upx --best target/release/filed RUN strip target/release/filed && upx --best target/release/filed
# --- deploy --- # --- deploy ---
FROM busybox:musl FROM alpine:3.17
RUN mkdir /config RUN mkdir /config
WORKDIR /config WORKDIR /config

View File

@ -2,6 +2,11 @@
This is a part of blek! File that is responsible for serving and uploading files. This is a part of blek! File that is responsible for serving and uploading files.
This module is released under the GPLv3 with additions, copy of which is included in the top level of this repository. This module is released under the GPLv3 with additions, copy of which is included in the top level of this repository.
## Required files
You need to have `.env` and `config/filed.toml` files for it to run. They have templates that end with `.example` that you can copy and edit.
There is a script `create-files.sh` that can copy them for you. The example is sufficient by itself and if you dont care much of the customization, you can leave it as is.
## Building ## Building
First, install the build dependencies: First, install the build dependencies:

View File

@ -1,5 +1,5 @@
use std::{fs, path::PathBuf, ffi::OsStr, process::Command}; use std::{fs, path::PathBuf, ffi::OsStr, process::Command, error::Error};
use css_minify::optimizations::{Minifier, Level}; use css_minify::optimizations::{Minifier, Level};
@ -9,6 +9,12 @@ fn asset_path(asset: &PathBuf) -> PathBuf {
path path
} }
fn template_path(template: &PathBuf) -> PathBuf {
let mut path = template.components().take(template.components().count() - 2).collect::<PathBuf>();
path.push(template.components().last().unwrap());
path
}
fn extfilter(valid: String, x: Option<&OsStr>) -> bool { fn extfilter(valid: String, x: Option<&OsStr>) -> bool {
if x.is_none() { if x.is_none() {
return false return false
@ -17,9 +23,23 @@ fn extfilter(valid: String, x: Option<&OsStr>) -> bool {
ext == valid ext == valid
} }
fn system(cmd: &str, args: &[&str]) -> Result<String, Box<dyn Error>> {
let out = Command::new(cmd)
.args(args)
.output()
?;
if out.stderr.len() != 0 {
panic!("Got this while running {cmd} with \"{}\": {}", args.join(" "), String::from_utf8(out.stderr).unwrap())
}
Ok(String::from_utf8(out.stdout)?)
}
fn main() { fn main() {
println!("cargo:rerun-if-changed=static/assets"); println!("cargo:rerun-if-changed=static/assets");
println!("cargo:rerun-if-changed=templates/source");
let assets = fs::read_dir("static/assets").unwrap(); let assets = fs::read_dir("static/assets").unwrap();
let assets = assets let assets = assets
@ -52,11 +72,45 @@ fn main() {
scripts.iter().for_each(|asset| { scripts.iter().for_each(|asset| {
Command::new("uglifyjs") Command::new("uglifyjs")
.arg("-c")
.arg(asset) .arg(asset)
.arg("-o") .arg("-o")
.arg(asset_path(asset)) .arg(asset_path(asset))
.arg("-c")
.spawn() .spawn()
.unwrap(); .unwrap();
}) });
// precompile templates
let templates = fs::read_dir("templates/source").unwrap();
let templates = templates
.map(|x| x.unwrap().path().canonicalize().unwrap())
.filter(|x| extfilter("html".into(), x.extension()))
.collect::<Vec<PathBuf>>();
templates.iter().for_each(|template| {
Command::new("html-minifier")
.arg(template.canonicalize().unwrap())
.arg("--collapse-whitespace")
// .arg("--minify-js")
// .arg("--minify-css")
// .arg("--keep-closing-slash")
.arg("--output")
.arg(template_path(template))
.spawn()
.unwrap();
});
let commit = system("git", &["rev-parse", "HEAD"]).map_err(|x| x.to_string());
let branch = system("git", &["rev-parse", "--abbrev-ref", "HEAD"]).map_err(|x| x.to_string());
match commit {
Err(err) => panic!("Can't get commit: {}", err),
Ok(commit) => println!("cargo:rustc-env=COMMIT_HASH={commit}")
}
match branch {
Err(err) => panic!("Can't get commit: {}", err),
Ok(branch) => println!("cargo:rustc-env=COMMIT_BRANCH={branch}")
}
} }

View File

@ -21,6 +21,11 @@ allow_pass_protection=true
# This is shown only if allow_uploads = false # This is shown only if allow_uploads = false
# upload_disable_reason="File uploads were disabled because of an ongoing attack." # upload_disable_reason="File uploads were disabled because of an ongoing attack."
# If you want to restrict the uploads
# To only the people who have a password,
# uncomment this field
# upload_pass=super_secret_pass
# Timeout for deleting a user uploaded file # Timeout for deleting a user uploaded file
file_del_timeout=1800 file_del_timeout=1800
@ -79,3 +84,7 @@ sudo_delete=false
# Whether /api/upload is enabled # Whether /api/upload is enabled
# It is not recommended to enable it if API key auth is not enabled # It is not recommended to enable it if API key auth is not enabled
upload=false upload=false
# Whether curlapi is enabled
# curl {url}/curlapi/help for more info
curlapi=true

5
filed/create-files.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
# This script will create the .env and config files
cp .env.example .env
cp config/filed.toml{,.example}

View File

@ -5,4 +5,4 @@ cd /opt/code
cargo check cargo check
cargo build cargo build
cargo watch -w src -w templates -w static -x run cargo watch -w src -w templates/source -w static -x run

View File

@ -74,7 +74,7 @@ paths:
example: {} example: {}
401: 401:
description: |- description: |-
This error code is returned if one of the two conditions are met: This error code is returned if one of these conditions are met:
1. The instance does not allow deleting files via API. 1. The instance does not allow deleting files via API.
2. The file has been uploaded from another IP, which is not this one, and the API was not authorized via an API key. 2. The file has been uploaded from another IP, which is not this one, and the API was not authorized via an API key.
@ -90,6 +90,9 @@ paths:
fid: fid:
type: string type: string
example: ID or name of the file. It is the NAME in file.blek.codes/uploads/NAME example: ID or name of the file. It is the NAME in file.blek.codes/uploads/NAME
api_key:
type: string
example: '123'
/api/files/upload: /api/files/upload:
post: post:
summary: Upload a file summary: Upload a file
@ -102,6 +105,8 @@ paths:
security: security:
- apikey: [ key ] - apikey: [ key ]
requestBody: requestBody:
description: |-
A multipart form
content: content:
multipart/form-data: multipart/form-data:
schema: schema:
@ -111,9 +116,24 @@ paths:
type: string type: string
example: binary file data example: binary file data
description: binary file data description: binary file data
instance_pass:
type: string
example: super_secret_pass
description: Instance-specific password needed to upload files
metadata: metadata:
type: object type: object
description: file info description: |-
JSON object with file info:
```
{
sha512: string,
name?: string,
pass?: string
}
```
Note that the content type does not matter on this one.
properties: properties:
sha512: sha512:
type: string type: string
@ -121,17 +141,48 @@ paths:
name: name:
type: string type: string
description: Optional name of the file so it would be accessible like file.blek.codes/uploads/{name} description: Optional name of the file so it would be accessible like file.blek.codes/uploads/{name}
pass:
type: string
description: Optional password protection for the file
responses: responses:
200: 200:
description: File uploaded successfully description: File uploaded successfully
content:
application/json:
schema:
type: object
properties:
status:
example: 'OK'
401: 401:
description: |- description: |-
This error code is returned if one of the two conditions are met: This error code is returned if one of the 4 conditions are met:
1. The instance does not allow API file uploads. 1. The instance does not allow API file uploads.
2. The instance requires API key for all API manipulations. 2. The instance requires API key for all API manipulations.
3. The provided API key is invalid. 3. The provided API key is invalid.
4. API authorization is not enabled, but a key is provided 4. API authorization is not enabled, but a key is provided
content:
application/json:
schema:
type: object
properties:
error:
example: 'APIPasswordDenied'
details:
example: 'API password authorization has been denied.'
403:
description: |-
This error code is returned if your request payload is malformed or the hash doesn't match the file
content:
application/json:
schema:
type: object
properties:
error:
example: 'APIError'
details:
example: 'Request payload invalid'
components: components:

View File

@ -22,6 +22,10 @@ pub struct FilesPolicy {
#[serde(default)] #[serde(default)]
pub upload_disable_reason: Option<String>, pub upload_disable_reason: Option<String>,
/// Upload password
#[serde(default)]
pub upload_pass: Option<String>,
/// Default time for file to be deleted /// Default time for file to be deleted
#[serde(default)] #[serde(default)]
pub file_del_timeout: usize, pub file_del_timeout: usize,
@ -42,6 +46,7 @@ impl Default for FilesPolicy {
allow_custom_names: true, allow_custom_names: true,
allow_pass_protection: true, allow_pass_protection: true,
upload_disable_reason: None, upload_disable_reason: None,
upload_pass: None,
file_del_timeout: 1800, file_del_timeout: 1800,
type_whitelist: None, type_whitelist: None,
type_blacklist: None, type_blacklist: None,
@ -107,6 +112,10 @@ pub struct APISettings {
/// Whether /api/upload is enabled /// Whether /api/upload is enabled
#[serde(default)] #[serde(default)]
pub upload: bool, pub upload: bool,
/// Whether curlapi is enabled
#[serde(default)]
pub curlapi: bool
} }
impl Default for APISettings { impl Default for APISettings {
@ -118,7 +127,8 @@ impl Default for APISettings {
get_all_own_only: true, get_all_own_only: true,
delete: false, delete: false,
sudo_delete: false, sudo_delete: false,
upload: false upload: false,
curlapi: true
} }
} }
} }

View File

@ -15,6 +15,23 @@ pub struct Redis {
pub prefix: String pub prefix: String
} }
#[derive(Debug, Clone)]
pub struct VersionData {
pub commit: String,
pub short_commit: String,
pub branch: String
}
impl Default for VersionData {
fn default() -> Self {
VersionData {
commit: env!("COMMIT_HASH").to_string(),
short_commit: env!("COMMIT_HASH").to_string().chars().take(6).collect(),
branch: env!("COMMIT_BRANCH").to_string()
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Env { pub struct Env {
pub logging: bool, pub logging: bool,
@ -24,7 +41,8 @@ pub struct Env {
pub filedir: String, pub filedir: String,
pub instanceurl: String, pub instanceurl: String,
pub uploadspath: String, pub uploadspath: String,
pub confpath: String pub confpath: String,
pub version: VersionData
} }
fn get_var<T: Into<String>, O: From<String>>(name: T) -> Result<O, String> { fn get_var<T: Into<String>, O: From<String>>(name: T) -> Result<O, String> {
@ -140,7 +158,8 @@ pub fn loadenv() -> Result<Env, Box<dyn std::error::Error>> {
return Err(format!("CONF_FILE is {}, which is not a file!", spath).into()) return Err(format!("CONF_FILE is {}, which is not a file!", spath).into())
} }
spath spath
} },
version: VersionData::default()
} }
) )
} }

View File

@ -25,23 +25,18 @@ impl FileManager {
async fn find_all(self: &Self, predicate: String) -> Result<Vec<File>, Box<dyn Error>> { async fn find_all(self: &Self, predicate: String) -> Result<Vec<File>, Box<dyn Error>> {
let mut conn = self.conn.get_async_connection().await?; let mut conn = self.conn.get_async_connection().await?;
let found: Vec<String> = conn.keys(predicate).await?; let keys: Vec<String> = conn.keys(predicate).await?;
let serialized: Vec<File> =
found.iter() let mut data: Vec<File> = vec![];
.map(|x| {
let result = serde_json::from_str(&x); for key in keys.iter() {
match result { let raw: String = conn.get(key.clone()).await?;
Ok(x) => Some(x),
Err(err) => { let mut parsed: File = serde_json::from_str(raw.as_str())?;
log::error!("Error while serializing {x}: {:?}", err); data.push(parsed);
None }
}
} Ok(data)
})
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.collect();
Ok(serialized)
} }
/// Filter options /// Filter options
@ -102,30 +97,32 @@ impl FileManager {
pub fn save(self: &Self, file: &File, kind: LookupKind) -> Result<(), Box<dyn Error>> { pub fn save(self: &Self, file: &File, kind: LookupKind) -> Result<(), Box<dyn Error>> {
let file = file.clone(); let file = file.clone();
let midfix = match kind {
LookupKind::ByName => "-name-",
LookupKind::ByHash => "-hash-"
};
match kind { match kind {
LookupKind::ByName => { LookupKind::ByName => {
if (&file).name.is_none() { if let Some(name) = file.name.clone() {
log::debug!("Using {} as a custom file name", name);
return Ok(self.save_int(
&file,
format!(
"{}-name-{}",
self.env.redis.prefix,
name
)
)?)
} else {
return Err("Filename can't be None when LookupKind is ByName!".into()) return Err("Filename can't be None when LookupKind is ByName!".into())
} }
} }
_ => () _ => log::debug!("No custom file name detected")
} }
self.save_int( self.save_int(
&file, &file,
format!( format!(
"{}{}{}", "{}-hash-{}",
self.env.redis.prefix, self.env.redis.prefix,
midfix, file.hash()
match kind {
LookupKind::ByName => (&file).name.as_ref().unwrap().clone(),
LookupKind::ByHash => (&file).hash()
}
) )
) )
} }

View File

@ -1,7 +1,7 @@
use warp::{reply::Reply, reject::Rejection, Filter}; use warp::{reply::Reply, reject::Rejection, Filter};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use self::files::get_all::get_all_f; use self::files::{get_all::get_all_f, delete::delete_f, upload::upload_f};
use super::state::SharedState; use super::state::SharedState;
@ -24,5 +24,7 @@ pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error
warp::path!("api") warp::path!("api")
.and(warp::path::end()) .and(warp::path::end())
.map(api_root) .map(api_root)
.or(get_all_f(state)) .or(get_all_f(state.clone()))
.or(delete_f(state.clone()))
.or(upload_f(state))
} }

View File

@ -1 +1,61 @@
pub mod get_all; use warp::{reply::{WithStatus, Json, json}, http::StatusCode};
use crate::web::state::SharedState;
use super::types::{ErrorMessage, Error};
fn check_api_enabled(state: &SharedState) -> Result<(), WithStatus<Json>> {
if ! state.config.api.enabled {
return Err(
warp::reply::with_status(
json(&ErrorMessage::new(Error::APIDisabled)),
StatusCode::SERVICE_UNAVAILABLE
)
)
}
Ok(())
}
fn is_api_pass(state: &SharedState) -> bool {
if let Some(keys) = state.config.api.apikeys.clone() {
keys.len() != 0
} else {
false
}
}
fn check_api_pass(state: &SharedState, key: String) -> Result<(), WithStatus<Json>> {
let mut valid = {
if let Some(keys) = state.config.api.apikeys.clone() {
keys.iter().find(|x| (**x) == key).is_some()
} else {
false
}
};
if key.len() == 0 {
valid = false
}
if valid {
Ok(())
} else {
Err(
warp::reply::with_status(
json(&ErrorMessage::new(Error::APIPasswordDenied)),
StatusCode::FORBIDDEN
)
)
}
}
fn function_disabled_err(status: StatusCode) -> WithStatus<Json> {
warp::reply::with_status(
json(&ErrorMessage::new(Error::APIFunctionDisabled)),
status
)
}
pub mod get_all;
pub mod delete;
pub mod upload;

View File

@ -0,0 +1,137 @@
use std::net::IpAddr;
use serde_json::json;
use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode};
use serde::{Serialize, Deserialize};
use warp_real_ip::real_ip;
use crate::{web::{state::SharedState, rejection::HttpReject, api::types::{ErrorMessage, Error}}, files::File};
use super::{function_disabled_err, check_api_enabled};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DeleteFunctionPayload {
pub fid: String,
pub api_key: Option<String>
}
pub async fn delete(state: SharedState, body: DeleteFunctionPayload, ip: Option<IpAddr>) -> Result<Box<dyn Reply>, Rejection> {
if let Err(res) = check_api_enabled(&state) {
return Ok(Box::new(res));
}
if (!state.config.api.delete) || (!state.config.api.enabled) {
return Ok(Box::new(function_disabled_err(StatusCode::UNAUTHORIZED)))
}
let mut sudo_authorized = false;
let mut blocked = false;
if let Some(keys) = state.config.api.apikeys.clone() {
if let Some(key) = body.api_key {
if keys.contains(&key) {
sudo_authorized = true;
blocked = false;
} else {
sudo_authorized = false;
blocked = true;
}
} else {
sudo_authorized = false;
blocked = true
}
}
if ! sudo_authorized {
if ip.is_none() { // need the ip if sudo is not authorized
blocked = true // to check if the file is the own file
}
}
let ip = ip.unwrap();
let id = body.fid;
let mut file = state.file_mgr.find_by_hash(id.clone())
.map_err(|x| HttpReject::StringError(x.to_string()))?;
if let None = file {
file = state.file_mgr.find_by_name(id)
.map_err(|x| HttpReject::StringError(x.to_string()))?;
}
if let None = file.clone() {
return Ok(
Box::new(
warp::reply::with_status(
json(
&ErrorMessage {
error: Error::APIError,
details: Some("No file with that ID was found.".into())
}
),
StatusCode::NOT_FOUND
)
)
)
}
let file: File = file.unwrap();
if let Some(uploader) = file.uploader_ip {
if uploader != ip && (!sudo_authorized) {
blocked = true;
}
} else {
blocked = true;
}
if blocked {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIPasswordDenied,
details: Some(
"Request has been denied for one of the following reasons: password auth did not pass, file was uploaded by someone else, the instance does not allow deleting files via the API".into()
)
}
),
StatusCode::UNAUTHORIZED
)
)
)
}
let res = file.delete(state).await;
if let Err(err) = res {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIError,
details: Some(format!("Couldn't delete file: {}", err))
}
),
StatusCode::INTERNAL_SERVER_ERROR
)
)
)
}
Ok(Box::new(json(&json!({}))))
}
pub fn delete_f(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let proxy_ip = state.env.proxy_addr;
warp::path!("api" / "files" / "delete")
.map(move || state.clone())
.and(warp::body::json())
.and(real_ip(vec![proxy_ip]))
.and_then(delete)
}

View File

@ -1,35 +1,77 @@
use warp::{reply::{Reply, json}, reject::Rejection, Filter, http::StatusCode}; use std::net::IpAddr;
use crate::web::{state::SharedState, rejection::HttpReject}; use warp::{reply::{Reply, json, with_status}, reject::Rejection, Filter, http::StatusCode};
use warp_real_ip::real_ip;
use super::super::types::{ErrorMessage, Error}; use crate::web::{state::SharedState, api::types::{ErrorMessage, Error}};
pub async fn get_all(state: SharedState) -> Result<Box<dyn Reply>, Rejection> { use super::{check_api_enabled, function_disabled_err};
if ! state.config.api.enabled {
pub async fn get_all(state: SharedState, ip: Option<IpAddr>) -> Result<Box<dyn Reply>, Rejection> {
if let Err(res) = check_api_enabled(&state) {
return Ok(Box::new(res))
}
if (!state.config.api.get_all) || (!state.config.api.enabled) {
return Ok(Box::new(function_disabled_err(StatusCode::UNAUTHORIZED)))
}
let found =
state.file_mgr.get_all(true, true)
.await
.map_err(|x| x.to_string());
if let Err(err) = found {
return Ok( return Ok(
Box::new( Box::new(
warp::reply::with_status( with_status(
json(&ErrorMessage::new(Error::APIDisabled)), json(
StatusCode::SERVICE_UNAVAILABLE &ErrorMessage {
error: Error::APIError,
details: Some(
format!("Error while getting all files: {err}")
)
}
),
StatusCode::INTERNAL_SERVER_ERROR
)
)
);
} else {
let mut found = found.unwrap();
if state.config.api.get_all_own_only {
found = found
.iter()
.filter(
|x| {
if let Some(owner) = x.uploader_ip {
if let Some(caller) = ip {
return owner == caller
}
}
false
}
).map(|x| x.clone()).collect();
}
Ok(
Box::new(
json(
&found
) )
) )
) )
} }
Ok(
Box::new(
json(
&state.file_mgr.get_all(true, true)
.await
.map_err(|x| x.to_string())
.map_err(|x| HttpReject::StringError(x))?
)
)
)
} }
pub fn get_all_f(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn get_all_f(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let proxy = state.env.proxy_addr;
warp::path!("api" / "files" / "get_all") warp::path!("api" / "files" / "get_all")
.map(move || state.clone()) .map(move || state.clone())
.and(real_ip(vec![proxy]))
.and_then(get_all) .and_then(get_all)
} }

View File

@ -0,0 +1,266 @@
use std::{collections::HashMap, net::IpAddr};
use serde::{Serialize, Deserialize};
use serde_json::json;
use sha2::{Sha512, Digest, digest::FixedOutput};
use warp::{reply::{Reply, with_status, json}, http::StatusCode, reject::Rejection, Filter, filters::multipart::FormData};
use warp_real_ip::real_ip;
use crate::{web::{state::SharedState, forms::FormElement, api::types::{ErrorMessage, Error}}, files::{File, lookup::LookupKind}};
use super::{is_api_pass, check_api_pass};
#[derive(Serialize, Deserialize)]
struct UploadAPIMetadata {
sha512: String,
name: Option<String>,
pass: Option<String>
}
struct UploadAPIPayload {
file: Vec<u8>,
file_type: String,
instance_pass: Option<String>,
metadata: UploadAPIMetadata
}
impl Default for UploadAPIPayload {
fn default() -> Self {
Self {
file: vec![],
file_type: "application/octet-stream".into(),
instance_pass: None,
metadata: UploadAPIMetadata {
sha512: "".into(),
name: None,
pass: None
}
}
}
}
impl UploadAPIPayload {
pub fn from_form(data: HashMap<String, FormElement>) -> Option<UploadAPIPayload> {
let mut out = Self::default();
let file = data.get("file");
let instance_pass = data.get("instance_pass");
let metadata = data.get("metadata");
let mut fields_set = false;
// required fields
if let Some(file) = file {
if let Some(metadata) = metadata {
if let Some(metadata) = metadata.as_atr_or_none() {
out.file = file.data.clone();
if let Ok(metadata) = serde_json::from_str(&metadata) {
out.metadata = metadata;
}
fields_set = true;
}
}
out.file_type = file.mime.clone();
}
// optional ones
if let Some(pass) = instance_pass {
if let Some(pass) = pass.as_atr_or_none() {
out.instance_pass = Some(pass);
}
}
if ! fields_set {
None
} else {
Some(out)
}
}
}
pub async fn upload(state: SharedState, data: FormData, ip: Option<IpAddr>) -> Result<Box<dyn Reply>, Rejection> {
if (!state.config.api.enabled) || (!state.config.api.upload) {
return Ok(
Box::new(
with_status(
json(&ErrorMessage::new(Error::APIDisabled)),
StatusCode::UNAUTHORIZED
)
)
)
}
if ! state.config.files.allow_uploads {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIDisabled,
details: Some(
match state.config.files.upload_disable_reason {
Some(reason) => format!("Uploads were disabled for the following reason: {reason}"),
None => format!("Uploads were disabled by the administrator")
}
)
}
),
StatusCode::UNAUTHORIZED
)
)
)
}
let data = FormElement::from_formdata(data)
.await;
if let Err(err) = data {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIError,
details: Some(
format!("Error while parsing payload: {err}")
)
}
),
StatusCode::BAD_REQUEST
)
)
)
}
let data = data.unwrap(); // this is guaranteed to be `Ok` at this point
let payload = UploadAPIPayload::from_form(data);
if let Some(payload) = payload {
if is_api_pass(&state) {
if let Err(res) = check_api_pass(
&state,
match payload.instance_pass {
Some(x) => x,
None => "".into()
}
) {
return Ok(Box::new(res))
}
}
// payload is all valid and accessible at this point
let mut hash: Sha512 = Sha512::new();
hash.update(&payload.file);
let hash = hex::encode(hash.finalize_fixed());
if hash != payload.metadata.sha512 {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIError,
details: Some("Hash does not match file".into())
}
),
StatusCode::BAD_REQUEST
)
)
)
}
let file = File::create(
payload.file,
payload.file_type,
payload.metadata.name.clone(),
state.env,
crate::files::DeleteMode::Time,
payload.metadata.pass,
ip
).await;
if let Err(err) = file {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIError,
details: Some(
format!("Error while saving the file: {err}")
)
}
),
StatusCode::INTERNAL_SERVER_ERROR
)
)
)
}
let file = file.unwrap();
let saved = state.file_mgr.save(
&file,
match payload.metadata.name {
Some(_) => LookupKind::ByName,
None => LookupKind::ByHash
}
);
if let Err(err) = saved {
return Ok(
Box::new(
with_status(
json(
&ErrorMessage {
error: Error::APIError,
details: Some(
format!("Error while saving the file: {err}")
)
}
),
StatusCode::INTERNAL_SERVER_ERROR
)
)
)
}
return Ok(
Box::new(
json(
&json!({
"status": "OK"
})
)
)
)
}
Ok(
Box::new(
with_status(
json(&ErrorMessage {
error: Error::APIError,
details: Some("Request payload invalid".into())
}),
StatusCode::BAD_REQUEST
)
)
)
}
pub fn upload_f(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let proxy = state.env.proxy_addr.clone();
warp::path!("api" / "files" / "upload")
.and(warp::post())
.map(move || state.clone())
.and(warp::multipart::form())
.and(real_ip(vec![proxy]))
.and_then(upload)
}

View File

@ -3,6 +3,9 @@ use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Error { pub enum Error {
APIDisabled, APIDisabled,
APIFunctionDisabled,
APIError,
APIPasswordDenied
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -15,7 +18,10 @@ impl ErrorMessage {
pub fn new(error: Error) -> ErrorMessage { pub fn new(error: Error) -> ErrorMessage {
ErrorMessage { ErrorMessage {
details: match error { details: match error {
Error::APIDisabled => Some("API is disabled by the administrator. Please contact them for further details".into()) Error::APIDisabled => Some("API is disabled by the administrator. Please contact them for further details".into()),
Error::APIFunctionDisabled => Some("This API function is disabled by the administrator. Please contact them for further details.".into()),
Error::APIError => Some("An error has occured while executing the API request".into()),
Error::APIPasswordDenied => Some("API password authorization has been denied.".into())
}, },
error, error,
} }

11
filed/src/web/curlapi.rs Normal file
View File

@ -0,0 +1,11 @@
use warp::{Filter, reply::Reply, reject::Rejection};
use super::state::SharedState;
mod upload;
mod help;
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
upload::get_routes(state.clone())
.or(help::get_routes(state))
}

View File

@ -0,0 +1,85 @@
use askama::Template;
use warp::{Filter, reply::{Reply, html}, reject::Rejection};
use crate::web::{state::SharedState, pages::CurlHelpPage, rejection::HttpReject};
pub async fn help(state: SharedState, ua: String) -> Result<Box<dyn Reply>, Rejection> {
if ! ua.starts_with("curl/") {
let page = CurlHelpPage { conf: state.config.clone(), env: state.env.clone() };
return Ok(
Box::new(
html(page.render().map_err(|x| HttpReject::AskamaError(x))?)
)
)
}
let brand = format!(
"{} \x1b[1m{}\x1b[0m {}",
state.config.brand.instance_emoji,
state.config.brand.instance_name,
{
if state.config.brand.instance_name != "blek! File" {
"\n\x1b[90mPowered by blek! File\x1b[0m"
} else { "" }
}
);
let mut warns: String = String::new();
if ! state.config.api.curlapi {
warns += "\x1b[1;31mWarning: curl API is disabled on this instance.\nYou can use the web UI to upload files.\x1b[0m\n\n"
}
if ! state.config.files.allow_uploads {
warns += {
format!(
"\x1b[1;31mWarning: all uploads are disabled on this instance{}\x1b[0m",
{
if let Some(reason) = state.config.files.upload_disable_reason {
format!(" for this reason:\n\"{}\"", reason)
} else { ".".to_string() }
}
).as_str()
}
}
let instance = state.env.instanceurl;
let help =
format!(
"To upload a new file, you can POST it like this:
\x1b[32mcurl\x1b[0m \x1b[33m-X POST\x1b[0m \x1b[34m{instance}/curlapi/upload\x1b[0m \x1b[33m-F'file=@file.txt'\x1b[0m \x1b[33m-F'tos_consent=on'\x1b[0m
You can also add a password:
\x1b[32mcurl\x1b[0m \x1b[33m-X POST\x1b[0m \x1b[34m{instance}/curlapi/upload\x1b[0m \x1b[33m-F'file=@file.txt'\x1b[0m \x1b[33m-F'filename=uwu'\x1b[0m \x1b[33m-F'tos_consent=on'\x1b[0m \x1b[33m-F'named=on'\x1b[0m
The `named=on` switch is neede because this API is basically
the HTML used at the regular web UI form wrapped into this URL
\x1b[1;32mIMPORTANT:\x1b[0m Read the terms of service \x1b[1mbefore\x1b[0m uploading the file!
The ToS can be found here: \x1b[34m{instance}/tos\x1b[0m .
{warns}"
);
Ok(
Box::new(
format!("
\x1b[31m \x1b[0m \x1b[35m \x1b[0m
\x1b[32m\x1b[0m \x1b[95m \x1b[0m
\x1b[34m\x1b[0m \x1b[35m \x1b[0m
{brand}
{help}
").to_string())
)
}
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::any()
.and(warp::path!("curlapi" / "help"))
.and(
warp::any()
.map(move || state.clone())
)
.and(
warp::header::<String>("user-agent")
)
.and_then(help)
}

View File

@ -0,0 +1,139 @@
use std::net::IpAddr;
use warp::{filters::multipart::FormData, reply::{Reply, with_status}, reject::Rejection, Filter};
use warp_real_ip::real_ip;
use crate::{web::{state::SharedState, forms::{FormElement, UploadFormData}, rejection::HttpReject}, files::File};
pub async fn upload(form: FormData, ip: Option<IpAddr>, state: SharedState) -> Result<Box<dyn Reply>, Rejection> {
if ! state.config.files.allow_uploads {
return Ok(
Box::new(
with_status(
match state.config.files.upload_disable_reason {
Some(reason) => format!("Uploads are disabled for the following reason:\n{reason}"),
None => "Uploads are disabled.".into()
},
warp::http::StatusCode::SERVICE_UNAVAILABLE
)
)
)
}
let params = FormElement::from_formdata(form)
.await
.map_err(|x| HttpReject::WarpError(x))?;
if let Some(consent) = params.get("tos_consent") {
if consent.data != "on".bytes().collect::<Vec<u8>>() {
return Ok(
Box::new(
format!("You need to agree to the ToS to upload a file.\nSee {}/curlapi/help for details\n\nTo agree to the ToS, add a -F'tos_consent=on'\n", state.env.instanceurl)
)
)
}
} else {
return Ok(
Box::new(
format!("You need to agree to the ToS to upload a file.\nSee {}/curlapi/help for details\n\nTo agree to the ToS, add a -F'tos_consent=on'\n", state.env.instanceurl)
)
)
}
let formdata = UploadFormData::from_formdata(params, true);
if let Some(formdata) = formdata {
let mut breaks_conf = false;
if (!state.config.files.allow_custom_names) && formdata.filename.is_some() {
breaks_conf = true;
}
if (!state.config.files.allow_pass_protection) && formdata.password.is_some() {
breaks_conf = true;
}
if breaks_conf {
return Ok(
Box::new(
with_status(
"Attempt to set name or password when they are disabled".to_string(),
warp::http::StatusCode::BAD_REQUEST
)
)
);
}
if let Some(pass) = state.config.files.upload_pass {
let pass_valid: bool;
if let Some(upass) = formdata.instancepass {
pass_valid = upass == pass;
} else {
pass_valid = false
}
if ! pass_valid {
return Ok(
Box::new(
with_status(
"Invalid instance password".to_string(),
warp::http::StatusCode::BAD_REQUEST
)
)
);
}
}
let file = File::create(
formdata.file,
formdata.mime,
formdata.filename.clone(),
state.env.clone(),
formdata.delmode,
formdata.password,
ip
).await.map_err(|x| HttpReject::StringError(x.to_string()))?;
state.file_mgr.save(&file, formdata.lookup_kind).map_err(|x| HttpReject::StringError(x.to_string()))?;
return Ok(
Box::new(
format!(
concat!(
"File uploaded successfully.\n",
"It is available via this link:\n\n",
"{}/upload/{}\n"
),
state.env.instanceurl,
urlencoding::encode(
match formdata.filename {
Some(name) => name,
None => file.hash()
}.as_str()
)
)
)
);
} else {
Ok(
Box::new(
with_status(
"Invalid form".to_string(),
warp::http::StatusCode::BAD_REQUEST
)
)
)
}
}
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::post()
.and(warp::path!("curlapi" / "upload"))
.and(warp::multipart::form())
.and(real_ip(vec![state.env.proxy_addr]))
.and(
warp::any()
.map(move || state.clone())
)
.and_then(upload)
}

View File

@ -17,9 +17,9 @@ use crate::files::{File, lookup::LookupKind, DeleteMode};
use super::{state::SharedState, pages::{UploadSuccessPage, ErrorPage}, rejection::HttpReject}; use super::{state::SharedState, pages::{UploadSuccessPage, ErrorPage}, rejection::HttpReject};
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
struct FormElement { pub struct FormElement {
data: Vec<u8>, pub data: Vec<u8>,
mime: String pub mime: String
} }
impl FormElement { impl FormElement {
@ -58,14 +58,15 @@ impl FormElement {
} }
} }
struct UploadFormData { pub struct UploadFormData {
filename: Option<String>, pub filename: Option<String>,
password: Option<String>, pub password: Option<String>,
lookup_kind: LookupKind, pub instancepass: Option<String>,
delmode: DeleteMode, pub lookup_kind: LookupKind,
file: Vec<u8>, pub delmode: DeleteMode,
mime: String, pub file: Vec<u8>,
tos_consent: bool pub mime: String,
pub tos_consent: bool
} }
impl Default for UploadFormData { impl Default for UploadFormData {
@ -73,6 +74,7 @@ impl Default for UploadFormData {
UploadFormData { UploadFormData {
filename: None, filename: None,
password: None, password: None,
instancepass: None,
lookup_kind: LookupKind::ByHash, lookup_kind: LookupKind::ByHash,
delmode: DeleteMode::Time, delmode: DeleteMode::Time,
file: vec![], file: vec![],
@ -84,7 +86,7 @@ impl Default for UploadFormData {
impl UploadFormData { impl UploadFormData {
pub fn from_formdata(data: HashMap<String, FormElement>) -> Option<UploadFormData> { pub fn from_formdata(data: HashMap<String, FormElement>, use_defaults: bool) -> Option<UploadFormData> {
let mut out = Self::default(); let mut out = Self::default();
// Add a name // Add a name
@ -93,7 +95,7 @@ impl UploadFormData {
if val.is_checked() { if val.is_checked() {
let name = data.get("filename")?; let name = data.get("filename")?;
out.filename = Some(name.as_atr_or_none()?); out.filename = Some(name.as_atr_or_none()?);
out.lookup_kind = LookupKind::ByHash out.lookup_kind = LookupKind::ByName
} }
}, },
None => () None => ()
@ -105,7 +107,6 @@ impl UploadFormData {
if val.is_checked() { if val.is_checked() {
let pass = data.get("password")?; let pass = data.get("password")?;
out.password = Some(pass.as_atr_or_none()?); out.password = Some(pass.as_atr_or_none()?);
out.lookup_kind = LookupKind::ByName
} }
}, },
None => () None => ()
@ -123,10 +124,22 @@ impl UploadFormData {
} }
}, },
None => { None => {
return None if ! use_defaults {
return None
}
} }
} }
match data.get("instancepass") {
Some(val) => {
let val = val.data.clone();
if let Ok(pass) = String::from_utf8(val) {
out.instancepass = Some(pass);
}
},
None => ()
};
let file = data.get("file")?; let file = data.get("file")?;
out.file = file.data.clone(); out.file = file.data.clone();
out.mime = file.mime.clone(); out.mime = file.mime.clone();
@ -157,7 +170,7 @@ pub async fn upload(form: FormData, ip: Option<IpAddr>, state: SharedState) -> R
} }
let params: HashMap<String, FormElement> = FormElement::from_formdata(form).await.map_err(|x| HttpReject::WarpError(x))?; let params: HashMap<String, FormElement> = FormElement::from_formdata(form).await.map_err(|x| HttpReject::WarpError(x))?;
let formdata = UploadFormData::from_formdata(params.clone()); let formdata = UploadFormData::from_formdata(params.clone(), false);
if let Some(formdata) = formdata { if let Some(formdata) = formdata {
@ -201,6 +214,47 @@ pub async fn upload(form: FormData, ip: Option<IpAddr>, state: SharedState) -> R
) )
} }
if let Some(upload_pass) = state.config.files.upload_pass.clone() {
if let Some(pass) = formdata.instancepass {
if upload_pass != pass {
let error = ErrorPage {
env: state.env.clone(),
conf: state.config.clone(),
error_text: "Password is invalid".into(),
link: Some("/".into()),
link_text: Some("Go back".into())
};
return Ok(
Box::new(
html(
error.render()
.map_err(|x| HttpReject::AskamaError(x))?
)
)
)
}
} else {
let error = ErrorPage {
env: state.env.clone(),
conf: state.config.clone(),
error_text: "Password is not available".into(),
link: Some("/".into()),
link_text: Some("Go back".into())
};
return Ok(
Box::new(
html(
error.render()
.map_err(|x| HttpReject::AskamaError(x))?
)
)
)
}
}
let file = File::create( let file = File::create(
formdata.file, formdata.file,
formdata.mime, formdata.mime,
@ -253,7 +307,8 @@ pub async fn upload(form: FormData, ip: Option<IpAddr>, state: SharedState) -> R
} }
pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn get_routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::post() warp::path("upload")
.and(warp::post())
.and(warp::multipart::form()) .and(warp::multipart::form())
.and(real_ip(vec![state.env.proxy_addr])) .and(real_ip(vec![state.env.proxy_addr]))
.and( .and(

View File

@ -2,32 +2,28 @@
web - The part of filed that handles everything related to HTTP web - The part of filed that handles everything related to HTTP
*/ */
use std::env::current_dir;
use static_dir::static_dir; use static_dir::static_dir;
use warp::{Filter, reply::Reply, reject::Rejection}; use warp::{Filter, reply::Reply, reject::Rejection};
use crate::{env::Env, files::lookup::FileManager, config::types::Config}; use crate::{env::Env, files::lookup::FileManager, config::types::Config};
mod pages; mod pages;
mod forms; pub mod forms;
pub mod state; pub mod state;
mod rejection; mod rejection;
mod api; mod api;
mod uploaded; mod uploaded;
mod curlapi;
use state::SharedState; use state::SharedState;
pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let staticpath = current_dir().unwrap(); static_dir!("static")
let staticpath = staticpath.to_str().unwrap().to_string() + "/static"; .or(curlapi::get_routes(state.clone()))
pages::get_routes(state.clone())
.or(forms::get_routes(state.clone())) .or(forms::get_routes(state.clone()))
.or(api::get_routes(state.clone())) .or(api::get_routes(state.clone()))
.or(uploaded::get_uploaded(state)) .or(uploaded::get_uploaded(state.clone()))
.or(static_dir!("static")) .or(pages::get_routes(state))
.or(warp::fs::dir(staticpath.to_string()))
} }
/* /*

View File

@ -88,6 +88,14 @@ pub struct ErrorPage {
pub link_text: Option<String> pub link_text: Option<String>
} }
#[derive(Template)]
#[template( path = "curlapi_help.html" )]
#[allow(dead_code)]
pub struct CurlHelpPage {
pub env: Env,
pub conf: Config
}
pub async fn uploaded(query: HashMap<String, String>, state: SharedState) -> Result<Html<String>, Rejection> { pub async fn uploaded(query: HashMap<String, String>, state: SharedState) -> Result<Html<String>, Rejection> {
if ! query.contains_key("file") { if ! query.contains_key("file") {

View File

@ -20,4 +20,11 @@
.alert.danger .alert-title { .alert.danger .alert-title {
background: #602020; background: #602020;
}
.alert.blue .alert-title {
background: #203050;
}
.alert.green .alert-title {
background: #205030;
} }

View File

@ -0,0 +1,23 @@
.code {
display: block;
padding: 1em;
border: 1px solid var(--header-sec-color);
border-radius: 12px;
overflow-x: auto;
}
.code .inner {
width: max-content;
display: block;
}
.code-inline {
display: inline;
background: #00000010;
border: 1px solid #c2c4c210;
border-radius: 4px;
padding: 2px 4px;
}
.code, .code-inline {
font-family: monospace;
line-height: 1.5em;
}

View File

@ -6,6 +6,27 @@
/** @type {HTMLElement} */ /** @type {HTMLElement} */
const root_drag_rop = document.getElementsByClassName('file-drag-n-drop')[0]; const root_drag_rop = document.getElementsByClassName('file-drag-n-drop')[0];
// make the root drag&drop element an ideal circle
function updateDragNDrop() {
if (document.body.scrollWidth < 667) {
// mobile
delete root_drag_rop.style.width;
delete root_drag_rop.style.height;
return
}
const width = root_drag_rop.offsetWidth;
root_drag_rop.style.width = width + 'px';
root_drag_rop.style.height = width + 'px';
}
updateDragNDrop();
document.onresize = updateDragNDrop();
/** @type {HTMLElement} */ /** @type {HTMLElement} */
const drag_rop = document.getElementsByClassName('file-drag-n-drop-inside')[0]; const drag_rop = document.getElementsByClassName('file-drag-n-drop-inside')[0];
const dr_rop_t = document.getElementsByClassName('file-drag-n-drop-inside-text')[0]; const dr_rop_t = document.getElementsByClassName('file-drag-n-drop-inside-text')[0];

View File

@ -1,7 +1,7 @@
.file-drag-n-drop { .file-drag-n-drop {
display: block; display: block;
cursor: pointer; cursor: pointer;
width: 440px; width: 100%;
height: 440px; height: 440px;
background: var(--header-sec-color); background: var(--header-sec-color);
border-radius: 16px; border-radius: 16px;

2
filed/templates/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -16,6 +16,22 @@
<title>{{ conf.brand.instance_name }}</title> <title>{{ conf.brand.instance_name }}</title>
<style> .footer svg { height: 32px; margin: 10px 0 } </style> <style> .footer svg { height: 32px; margin: 10px 0 } </style>
<style>
.header .header-text, .header .header-home, .header .header-bg { transition: 250ms ease; }
@media (max-width:667px) {
.header .header-text { text-align: center }
.header .header-home { width: 100% }
.header .header-bg { left: 50% !important; transform: translateX(-50%); -webkit-mask-image: linear-gradient(90deg, #0000, #000, #0000); }
{%- if conf.brand.instance_motto.len() != 0 -%}
.header .header-home-motto {
display:none
}
{%- endif -%}
}
</style>
{%- if cfg!(debug_assertions) -%} {%- if cfg!(debug_assertions) -%}
<link rel="stylesheet" href="/alert.css" /> <link rel="stylesheet" href="/alert.css" />
@ -33,7 +49,7 @@
{{- conf.brand.instance_name -}} {{- conf.brand.instance_name -}}
{%- if conf.brand.instance_motto.len() != 0 -%} {%- if conf.brand.instance_motto.len() != 0 -%}
{#- Whitespace control is stupid -#} {#- Whitespace control is stupid -#}
{{- " - " -}}{{- conf.brand.instance_motto -}} <span class="header-home-motto">{{- " - " -}}{{- conf.brand.instance_motto -}}</span>
{%- endif -%} {%- endif -%}
</a> </a>
</div> </div>
@ -66,6 +82,13 @@
<tr> <tr>
<td> <td>
<small>Made with Rust and &lt;3</small> <small>Made with Rust and &lt;3</small>
<small style="display:block">
Version
<a href="https://git.blek.codes/blek/bfile/commit/{{ env.version.commit }}" target="_blank">
{{ env!("CARGO_PKG_VERSION") }} ({{ env.version.branch -}}/{{- env.version.short_commit }})
</a>
</small>
<ul style='margin:10px 0'> <ul style='margin:10px 0'>
<li> <li>

View File

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 217.2 80.2'><path stroke-linecap='round' stroke-width='.94' d='M167.5 78.8h-30.6v-1h.1a9.56 9.56 0 0 0 2.6-.34 7.21 7.21 0 0 0 3.2-1.86 7.38 7.38 0 0 0 2.08-4.19 10.34 10.34 0 0 0 .12-1.61v-57a9.37 9.37 0 0 0-.34-2.6 7.18 7.18 0 0 0-1.91-3.2 7.62 7.62 0 0 0-4.42-2.1 10.54 10.54 0 0 0-1.43-.1v-1h10.2q4.71 0 7.38-.94a9.48 9.48 0 0 0 .42-.16q1.74-.77 2.75-1.77a5.19 5.19 0 0 0 .75-.93h1v52.6l17-14.3a28.74 28.74 0 0 0 1.94-1.76q1.86-1.85 2.62-3.47a6.17 6.17 0 0 0 .34-.87 5.92 5.92 0 0 0 .09-.43q.3-1.9-1.08-2.38a2.57 2.57 0 0 0-.31-.09l-1.3-.3v-1h14.9v1l-1 .2a11.7 11.7 0 0 0-2.6.76q-1.12.47-2.28 1.2a19.22 19.22 0 0 0-.07.04q-1.94 1.22-5.2 3.84a159.62 159.62 0 0 0-2.15 1.76l-6.9 5.8 19.9 30.6a33.2 33.2 0 0 0 1.08 1.36q1.06 1.24 1.93 1.97a7.93 7.93 0 0 0 .29.22q1.4 1.05 3.1 1.05v1h-26.4v-1q2.27 0 3.13-.33a1.76 1.76 0 0 0 .17-.07q.74-.25.87-.97a1.86 1.86 0 0 0 .03-.33q0-.81-.57-2.04a11.36 11.36 0 0 0-.13-.26 85.38 85.38 0 0 0-.32-.63l-.3-.59a148.04 148.04 0 0 0-.33-.63 31.93 31.93 0 0 0-.53-.97l-.6-1.02a54.25 54.25 0 0 0-.22-.36l-11.4-18.3-3.7 3.2v15.3a9.56 9.56 0 0 0 .34 2.6 7.21 7.21 0 0 0 1.86 3.2 7.38 7.38 0 0 0 4.19 2.08 10.34 10.34 0 0 0 1.61.12h.1v1ZM9.1 80.2h-1V12.8a9.37 9.37 0 0 0-.34-2.6A7.18 7.18 0 0 0 5.85 7a7.62 7.62 0 0 0-4.42-2.1A10.54 10.54 0 0 0 0 4.8v-1h10.2a49.91 49.91 0 0 0 2.64-.06q3.77-.2 5.44-1.03a4.6 4.6 0 0 0 .02-.01 10.17 10.17 0 0 0 1.48-.9q.81-.6 1.37-1.3a5.73 5.73 0 0 0 .35-.5h1v36.4q2.3-4.5 6.45-7.2t9.15-2.7q5.9 0 10.5 3.5a22.42 22.42 0 0 1 5.7 6.49 28.24 28.24 0 0 1 1.55 3.01q2.65 6 2.65 13.7a36.88 36.88 0 0 1-.7 7.3 28.48 28.48 0 0 1-2.25 6.8q-2.95 6.1-8.25 9.5a21.45 21.45 0 0 1-9.17 3.21 27.24 27.24 0 0 1-3.23.19 21.87 21.87 0 0 1-6.22-.83 14.8 14.8 0 0 1-9.78-9.07l-9.8 9.9Zm128-34.4.1 1.5h-33.6v.5a38.84 38.84 0 0 0 .58 6.93q.7 3.87 2.26 6.98a20.6 20.6 0 0 0 1.86 3.04 15.79 15.79 0 0 0 4.92 4.45q3.36 1.9 7.68 1.9 4.8 0 9.15-2.35 4.35-2.35 7.05-6.35l.9.3q-1.7 5.1-5.3 9-3.6 3.9-8.5 6.1a25.45 25.45 0 0 1-10.07 2.2 29.25 29.25 0 0 1-.53 0q-6.9 0-12.25-3.4a22.86 22.86 0 0 1-7.97-8.7 27.29 27.29 0 0 1-.38-.75 28.67 28.67 0 0 1-2.7-9.03 36.6 36.6 0 0 1-.3-4.82 32.79 32.79 0 0 1 .77-7.28 25.67 25.67 0 0 1 2.53-6.72q3.3-6 9.05-9.35 5.75-3.35 13.05-3.35a26.46 26.46 0 0 1 6.02.66 21.48 21.48 0 0 1 4.83 1.74q4.75 2.4 7.6 6.75a20.08 20.08 0 0 1 3.04 8.13 24.45 24.45 0 0 1 .21 1.92Zm-47.2 33H59.3v-1a9.82 9.82 0 0 0 2.65-.34 7.46 7.46 0 0 0 3.2-1.81 7.12 7.12 0 0 0 2.08-3.94 10.33 10.33 0 0 0 .17-1.91v-57a9.37 9.37 0 0 0-.34-2.6A7.18 7.18 0 0 0 65.15 7a7.62 7.62 0 0 0-4.42-2.1 10.54 10.54 0 0 0-1.43-.1v-1h10.2q4.71 0 7.38-.94a9.48 9.48 0 0 0 .42-.16q1.74-.77 2.75-1.77A5.19 5.19 0 0 0 80.8 0h1v69.8q0 3.7 2.25 5.85 2.25 2.15 5.85 2.15v1ZM22.5 39.9V65a27.97 27.97 0 0 0 .52 3.96q.74 3.5 2.38 5.89 2.5 3.65 6.6 3.65 4.95 0 7.63-5.75a19.3 19.3 0 0 0 .37-.85 28.56 28.56 0 0 0 1.42-4.73Q42.7 61.3 42.7 52.5a97.73 97.73 0 0 0-.13-5.24q-.3-5.58-1.29-9.26a20.42 20.42 0 0 0-.68-2.1q-1.23-3.15-3.27-4.46A6.03 6.03 0 0 0 34 30.5a9.9 9.9 0 0 0-6.95 2.81 12.76 12.76 0 0 0-.1.09 18.73 18.73 0 0 0-3.3 4.1 16.72 16.72 0 0 0-1.15 2.4Zm186.6 11.8h-1a115.54 115.54 0 0 0-.18-6.57q-.29-5.13-1.07-9.13a148.1 148.1 0 0 0-.96-4.55q-.5-2.23-1.04-4.2a78.12 78.12 0 0 0-.65-2.25 142.68 142.68 0 0 0-1.2-3.63q-.58-1.64-1.13-3.08a78.3 78.3 0 0 0-.37-.94 18.06 18.06 0 0 1-1.17-4.4 15.99 15.99 0 0 1-.13-2.05 10.23 10.23 0 0 1 .35-2.75 7.75 7.75 0 0 1 1.95-3.4 7.7 7.7 0 0 1 4.48-2.23 10.83 10.83 0 0 1 1.62-.12 10.48 10.48 0 0 1 2.76.34 7.7 7.7 0 0 1 3.49 2.01 7.91 7.91 0 0 1 2.25 4.67 11 11 0 0 1 .1 1.48 17.47 17.47 0 0 1-.87 5.38 20.4 20.4 0 0 1-.38 1.07q-.86 2.23-1.84 5.01a284.93 284.93 0 0 0-.91 2.64q-1.6 4.6-2.85 11a59.41 59.41 0 0 0-.76 5.2q-.49 4.69-.49 10.5Zm-105.4-5.9H124q0-7.9-2.55-12.8a11.57 11.57 0 0 0-1.65-2.42 7.12 7.12 0 0 0-5.5-2.48 7.41 7.41 0 0 0-5.34 2.27q-.99.97-1.85 2.34a16.41 16.41 0 0 0-.11.19 19.79 19.79 0 0 0-1.94 4.45q-.69 2.22-1.04 4.87a43.13 43.13 0 0 0-.32 3.58Zm98.88 31.98a8.17 8.17 0 0 0 5.92 2.42 9.73 9.73 0 0 0 .1 0 8.18 8.18 0 0 0 5.9-2.5 10.03 10.03 0 0 0 .08-.08A8.17 8.17 0 0 0 217 71.7a10.28 10.28 0 0 0 0-.34 7.88 7.88 0 0 0-2.6-5.76 10.87 10.87 0 0 0-.33-.3 8.38 8.38 0 0 0-5.57-2.1 8.2 8.2 0 0 0-3.22.64 9.25 9.25 0 0 0-2.68 1.76 7.87 7.87 0 0 0-2.03 2.92 8.6 8.6 0 0 0-.57 3.18 9.73 9.73 0 0 0 0 .1 8.18 8.18 0 0 0 2.5 5.9 10.03 10.03 0 0 0 .08.08Z' font-size='12' vector-effect='non-scaling-stroke'/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,113 @@
{% extends "base.html" %}
{% block head %}
<link rel='stylesheet' href="/code.css" />
<link rel='stylesheet' href="/alert.css" />
<link rel='stylesheet' href="/js-only.css" />
<style>
.copy-btn { font-size: 70%; transform: translateY(-25%); display: inline-block }
</style>
{% endblock %}
{% block body %}
<div style="max-width:95vw;width:fit-content;margin:0 auto">
<h1 style="text-align:center">Curl API</h1>
<p>
blek! File has an API for uploading files via cURL.
To upload a file via cURL, follow these instructions:
</p>
<p>
To upload a file, POST it like this:
<a href="#" class="copy-btn" data-clipboard-text="curl -X POST {{env.instanceurl}}/curlapi/upload -F'file=@file.txt' -F'tos_consent=on'">
Copy!
</a>
</p>
<div class='code'>
<span class='inner'>
<span style='color:green'>curl</span>
<span style='color:orange'>-X POST</span>
<span style='color:darkcyan'>{{env.instanceurl}}/curlapi/upload</span>
<span style='color:orange'>-F'file=@file.txt'</span>
<span style='color:orange'>-F'tos_consent=on'</span>
</span>
</div>
<p>
To add a password, do it like this:
<a href="#" class="copy-btn" data-clipboard-text="curl -X POST {{env.instanceurl}}/curlapi/upload -F'file=@file.txt' -F'filename=uwu' -F'tos_consent=on' -F'named=on'">
Copy!
</a>
</p>
<div class='code'>
<span class='inner'>
<span style='color:green'>curl</span>
<span style='color:orange'>-X POST</span>
<span style='color:darkcyan'>{{env.instanceurl}}/curlapi/upload</span>
<span style='color:orange'>-F'file=@file.txt'</span>
<span style='color:orange'>-F'tos_consent=on'</span>
<span style='color:orange'>-F'named=on'</span>
</span>
</div>
<p>
Note that the
<span class='code-inline'>named=on</span>
switch is required.
Its needed because the curl API is basically a wrapper of
<a href="/">this</a>
HTML form.
</p>
<div class="alert green">
<h1 class="alert-title">
Important
</h1>
<p class="alert-text">
Read the
<a href="/tos">Terms of Service</a>
<b>before</b>
uploading a file.
<br/>
You agree to them by adding the
<span class="code-inline">tos_consent=on</span>
switch.
</p>
</div>
<div class='alert blue'>
<h1 class='alert-title'>Web UI</h1>
<div class='alert-text'>
<p>
Hey, it looks like you are viewing this page from a browser!<br/>
You can use the Web UI as well to upload a file!
</p>
<p style='margin:32px 0'>
<a href='/' role='button' class='btn'>
Go to the web UI
</a>
</p>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://unpkg.com/clipboard@2/dist/clipboard.min.js"></script>
<script>
new ClipboardJS('.copy-btn');
(
() => {
let btns = document.getElementsByClassName('copy-btn')
for (const button of btns) {
button.onclick = () => {
let old = button.innerHTML;
button.innerHTML = 'Copied!';
setTimeout(() => { button.innerHTML = old }, 500);
}
}
}
)()
</script>
<script src='/js-only.js'></script>
{% endblock %}

View File

@ -9,6 +9,31 @@
{% endblock %} {% endblock %}
{% block scripts %}
{%- if conf.files.upload_pass.is_some() -%}
{#- Script to disable button when password is not entered -#}
{#- -#}<script>
{#- -#} (
{#- -#} ()=>{
{#- -#} const pass_inp=document.getElementById("instancepass");
{#- -#} const submit=document.getElementById("bfile-upload-submit");
{#- -#} submit.setAttribute('disabled',true);
{#- -#}
{#- -#} pass_inp.onchange=()=>{
{#- -#} if(pass_inp.value.length==0)
{#- -#} submit.setAttribute('disabled',true);
{#- -#} else submit.removeAttribute('disabled')
{#- -#} }
{#- -#} }
{#- -#} )()
{#- -#}</script>
{%- endif -%}
{% endblock %}
{% block body %} {% block body %}
<div style="max-width:95vw;width:fit-content;margin:0 auto"> <div style="max-width:95vw;width:fit-content;margin:0 auto">
@ -114,6 +139,24 @@
</p> </p>
</div> </div>
{%- else -%} {%- else -%}
{%- if let Some(pass) = conf.files.upload_pass -%}
<div class="alert blue">
<h1 class="alert-title">
Upload password
</h1>
<div class="alert-text">
<p>This instance requires a password to upload a file.</p>
<p>
<label>
Password:
<input type="password" name="instancepass" id="instancepass">
</label>
</p>
</div>
</div>
{%- endif -%}
<p> <p>
<input type="file" name="file" id="bfile-formupload-file" style="display: none" /> <input type="file" name="file" id="bfile-formupload-file" style="display: none" />
<label for="bfile-formupload-file"> <label for="bfile-formupload-file">
@ -142,7 +185,7 @@
</label> </label>
</p> </p>
<p> <p>
<button class='btn btn-fill'> <button class='btn btn-fill' id="bfile-upload-submit">
Upload! Upload!
</button> </button>
</p> </p>

View File

@ -5,5 +5,5 @@ REDIS_PREFIX=bfile-
USERCONTENT_DIR=/opt/user_uploads USERCONTENT_DIR=/opt/user_uploads
CLEAN_DEL=30 minutes CLEAN_DEL=30min
CLEAN_ERRDEL=2 minutes CLEAN_ERRDEL=2min

View File

@ -10,7 +10,7 @@ RUN cargo b -r
RUN strip target/release/janitor && upx --best target/release/janitor RUN strip target/release/janitor && upx --best target/release/janitor
# --- deploy --- # --- deploy ---
FROM busybox:musl FROM alpine:3.17
RUN mkdir /config RUN mkdir /config
WORKDIR /config WORKDIR /config

46
resource/.air.toml Normal file
View File

@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = [ ".*\\.md" ]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

5
resource/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
resourced.toml
resourced
.DS_Store
.env
tmp

22
resource/Dockerfile.prod Normal file
View File

@ -0,0 +1,22 @@
FROM golang:alpine3.17 as builder
WORKDIR /opt/build
COPY . .
RUN apk add --no-cache git musl-dev upx binutils
RUN go build . && \
strip resourced && \
upx resourced
FROM alpine:3.17
WORKDIR /opt/code
COPY --from=builder /opt/build/resourced /usr/bin/resourced
# Note
# -----
# Since this is running with prefork, don't
# forget to set --pid=host when running this app
CMD [ "sh", "-c", "/usr/bin/resourced" ]

15
resource/README.md Normal file
View File

@ -0,0 +1,15 @@
# resourceD
A simple microservice for serving resources
## Building
```sh
go build .
```
## Running
Either run it the normal way via the provided docker compose file, or `go run .`.
Also dont forget to create `resourced.toml`
## Configuration
The file `resourced.toml.example` serves as a both an example and reference, since it has a lot of comments.

View File

@ -0,0 +1,48 @@
openapi: 3.0.3
info:
title: ResourceD API
description: |-
This is the ResourceD API docs.
license:
name: GPLv3
url: https://www.gnu.org/licenses/gpl-3.0.en.html
version: "1.0"
servers:
- url: http://localhost/resource
tags:
- name: Data API
description: API for serving data
- name: System API
description: API for serving system data
paths:
/{id}:
get:
description: Get a resource
summary: Get a resource. Send browser requests to this URL
tags:
- Data API
responses:
200:
description: Returns a resource in its binary data
302:
description: Resource is an external (http) link and the redirect is being forwarded to that link
404:
description: Not found
500:
description: Internal error
/info/is_enabled:
get:
description: Check if resourceD is enabled
summary: Check if resourceD is enabled
tags:
- System API
responses:
200:
description: Ok
content:
application/json:
schema:
type: boolean
example: true

20
resource/go.mod Normal file
View File

@ -0,0 +1,20 @@
module blek/resourced
go 1.21.3
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gofiber/fiber/v2 v2.50.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.13.0 // indirect
)

31
resource/go.sum Normal file
View File

@ -0,0 +1,31 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw=
github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

158
resource/main.go Normal file
View File

@ -0,0 +1,158 @@
package main
import (
"log"
"fmt"
"regexp"
"strings"
"net/http"
"io/ioutil"
)
import (
"github.com/BurntSushi/toml"
"github.com/gofiber/fiber/v2"
"github.com/dustin/go-humanize"
)
type Resource struct {
Url string `toml:"url"`
Proxied bool `toml:"proxied",default:false`
Mime string `toml:"mime"`
}
func (self Resource) Get() ([]byte, error) {
return ioutil.ReadFile(self.Url[7:])
}
type ResourceDConfig struct {
Enabled bool `toml:"enabled"`
ListenURL string `toml:"listen_url"`
ProxyCacheMinSize string `toml:"proxy_cache_min_size",default:5MB`
}
type Config struct {
ResourceD ResourceDConfig `toml:"resourceD"`
Resource map[string]Resource `toml:"resource"`
}
func (self Config) Validate() int {
re, err := regexp.Compile(`^(file|http(s|))://`)
if err != nil { panic(err) }
for key, res := range self.Resource {
if ! re.MatchString(res.Url) {
panic(fmt.Sprintf("Resource %s has invalid URL: %s\nOnly file://, http:// and https:// URLs are allowed", key, res.Url))
}
}
return 0
}
func (self Resource) GetProxied() ([]byte, error) {
cached, exists := ProxyResourceCache[self.Url];
if exists {
return cached, nil
}
resp, err := http.Get(self.Url)
if err != nil { return make([]byte, 0, 0), err }
buf, err := ioutil.ReadAll(resp.Body)
if err != nil { return make([]byte, 0, 0), err }
// cache only those that are less than 5 mb
if len(buf) > ProxyCacheMinSize {
ProxyResourceCache[self.Url] = buf
}
return buf, nil
}
var ProxyResourceCache map[string][]byte = make(map[string][]byte)
var ProxyCacheMinSize int
func main() {
var conf Config
data, err := ioutil.ReadFile("resourced.toml")
if err != nil { panic(err) }
a, err := toml.Decode(string(data), &conf)
if err != nil { panic(err) }
_ = a
cache_min, err := humanize.ParseBytes(conf.ResourceD.ProxyCacheMinSize)
if err != nil { panic(err) }
ProxyCacheMinSize = int(cache_min)
conf.Validate()
if len(conf.Resource) == 0 {
fmt.Println("\x1b[33m[warn] No resources are specified\x1b[0m");
} else {
fmt.Println(fmt.Sprintf("[info] Loaded %d resources", len(conf.Resource)))
}
if ! conf.ResourceD.Enabled {
fmt.Println("\x1b[33m[warn] resourceD is disabled. No resources will be served\x1b[0m")
}
app := fiber.New(fiber.Config {
Prefork: true,
CaseSensitive: false,
StrictRouting: true,
ServerHeader: "",
AppName: "blek! File resourceD",
})
app.Get("/info/is_enabled", func (c *fiber.Ctx) error {
return c.JSON(conf.ResourceD.Enabled)
})
app.Use(func (c *fiber.Ctx) error {
if ! conf.ResourceD.Enabled {
return c.Status(fiber.StatusNotFound).SendString("ResourceD is disabled")
}
return c.Next()
})
app.Get("/:id", func (c *fiber.Ctx) error {
res, exists := conf.Resource[c.Params("id")]
if ! exists {
return c.Status(fiber.StatusNotFound).SendString("Resource not found")
}
if ! strings.HasPrefix(res.Url, "file://") {
if res.Proxied {
data, err := res.GetProxied()
if err != nil {
log.Fatalln(err)
// we failed, send a redirect instead
// the next line would be the one with
// c.Location(res.Url)
} else {
c.Response().Header.SetContentType(res.Mime)
c.Response().Header.SetContentLength(len(data))
return c.Send(data)
}
}
c.Location(res.Url)
c.Status(302)
return nil
}
data, err := res.Get()
if err != nil {
panic(err)
}
c.Response().Header.SetContentType(res.Mime)
c.Response().Header.SetContentLength(len(data))
return c.Send(data)
})
log.Fatal(app.Listen(conf.ResourceD.ListenURL))
}

View File

@ -0,0 +1,47 @@
# The resourceD config
[resourceD]
# Whether to enable the resourceD.
# If this is false, resourceD will start but respond to
# all requests with 404
# It is false by default because resourceD is not required in a default installation.
enabled=true
# URL to listen on
listen_url="0.0.0.0:80"
# Minibal size of a file to be cached
# File size is parsed via this library:
# https://github.com/dustin/go-humanize
proxy_cache_min_size="5MB"
# Resource ID must be like a java package name
# At least one X.X. is required
#
# Examples:
# org.university.logo
# dev.indie_guy.logo
# com.pany.logo
# Test your names here: https://regex101.com/r/wQdOup/2
[resource."com.example.logo"]
# Can also be an external link
# If an external link is specified,
# the resource will be returned as a 302 redirect to the link
url="file:///some/where"
# File type, as according to this:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
mime="image/jpg"
# (if url is an http(s) url))
# Whether to proxy the resource.
# true - Send the resource like a regular file
# false - Send a 302 HTTP redirect to the URL (default)
#
# It is not recommended to set this to `true`
# unless you are referring to a resource that is
# available only in the local network
#
# proxied=true