Compare commits
113 Commits
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | 9ceb0d4c21 | |
Orhun Parmaksız | 7afea1721d | |
Orhun Parmaksız | c04c39b975 | |
Orhun Parmaksız | 32f95ef938 | |
dependabot[bot] | 15e3619479 | |
Orhun Parmaksız | 2037530078 | |
Orhun Parmaksız | 44c07a3eb6 | |
Orhun Parmaksız | 64d783bd0d | |
Orhun Parmaksız | 1fd561f869 | |
dependabot[bot] | c6d6da6296 | |
Orhun Parmaksız | 54e2ddc91a | |
dependabot[bot] | 8e505c0da8 | |
Orhun Parmaksız | 0f0ba72305 | |
dependabot[bot] | 77e97573ef | |
dependabot[bot] | 40f5d909ca | |
dependabot[bot] | 567480a21e | |
dependabot[bot] | e8c342af46 | |
dependabot[bot] | 1442771a57 | |
Orhun Parmaksız | dadd88c240 | |
Orhun Parmaksız | e3b00453d9 | |
Helmut K. C. Tessarek | b2acb71d0d | |
Orhun Parmaksız | db24d911f6 | |
Helmut K. C. Tessarek | 4774de6652 | |
Helmut K. C. Tessarek | 4987cfe5e5 | |
Orhun Parmaksız | c60a614258 | |
Helmut K. C. Tessarek | fa5105deab | |
Jake Howard | dae00c42b5 | |
Orhun Parmaksız | 12f0e8f3a7 | |
Helmut K. C. Tessarek | db971e6434 | |
Helmut K. C. Tessarek | 8e6393c6f4 | |
Helmut K. C. Tessarek | e06c18279e | |
Helmut K. C. Tessarek | 48a3626c7d | |
dependabot[bot] | 8de2450931 | |
dependabot[bot] | 2003abe8bd | |
dependabot[bot] | 4703b26194 | |
dependabot[bot] | e383b6f426 | |
dependabot[bot] | fa7304a5d0 | |
dependabot[bot] | 90c0477edb | |
dependabot[bot] | 7f8428c75d | |
dependabot[bot] | 25a013266c | |
Orhun Parmaksız | 274bbd3307 | |
dependabot[bot] | c9ba166865 | |
dependabot[bot] | 4af7970f48 | |
dependabot[bot] | 87a0ba21ed | |
Orhun Parmaksız | c8dd41549d | |
Helmut K. C. Tessarek | ec9b55f3e2 | |
dependabot[bot] | f905648866 | |
dependabot[bot] | cf21df8a7c | |
dependabot[bot] | a806af9a41 | |
Orhun Parmaksız | f3962485df | |
Orhun Parmaksız | d6e1968191 | |
Orhun Parmaksız | e34f747761 | |
Artem Medvedev | e21f99ac4a | |
Artem Medvedev | b74e9ceeaf | |
Artem Medvedev | c5c9c6d233 | |
dependabot[bot] | d7c67e0350 | |
Artem Medvedev | adbd67a182 | |
Orhun Parmaksız | 3e34206884 | |
dependabot[bot] | 1e9a3b4a71 | |
Orhun Parmaksız | d4b02eca91 | |
Orhun Parmaksız | 0f377fc44c | |
Orhun Parmaksız | 62948abdc0 | |
Orhun Parmaksız | bb33f437ba | |
Orhun Parmaksız | ef08b9e838 | |
dependabot[bot] | a291307bec | |
Orhun Parmaksız | 3c3a4b58ac | |
Orhun Parmaksız | fd75750a43 | |
Orhun Parmaksız | 339963f2bd | |
Orhun Parmaksız | ed9f6aa586 | |
dependabot[bot] | 7c9f443943 | |
dependabot[bot] | 50fad6e333 | |
dependabot[bot] | cc6815d783 | |
Orhun Parmaksız | 92a4360752 | |
dependabot[bot] | 89459e6be7 | |
dependabot[bot] | 616e5846cc | |
Orhun Parmaksız | 54a906ddf7 | |
dependabot[bot] | 4bd3082566 | |
dependabot[bot] | 0e7aafaf0f | |
Orhun Parmaksız | b0c61cdb09 | |
dependabot[bot] | 12b1c713a6 | |
Orhun Parmaksız | c6d32d6df9 | |
DtxdF | 9a73b3e9c5 | |
Orhun Parmaksız | 79662d64ab | |
Orhun Parmaksız | eee3355107 | |
Orhun Parmaksız | 87c0b11c60 | |
Orhun Parmaksız | 25be13d9bf | |
dependabot[bot] | d943ad5bc6 | |
dependabot[bot] | f0e0b247eb | |
Orhun Parmaksız | 30400cab0d | |
dependabot[bot] | 2477b7428e | |
Jean SIMARD | 70541bb842 | |
dependabot[bot] | cd9ace952c | |
Orhun Parmaksız | 77bb4ca299 | |
dependabot[bot] | 5d0e38ad15 | |
dependabot[bot] | 4cb6548851 | |
dependabot[bot] | db97b32955 | |
dependabot[bot] | 25171ad828 | |
dependabot[bot] | d3394d0d30 | |
Orhun Parmaksız | 48cdd09298 | |
dependabot[bot] | b2cdc072ca | |
dependabot[bot] | 6be7b850a1 | |
Orhun Parmaksız | c60b548cc7 | |
dependabot[bot] | 8d4063c051 | |
Orhun Parmaksız | 0eebb78694 | |
dependabot[bot] | 9e6dfeeaaa | |
Orhun Parmaksız | 86759450b9 | |
Orhun Parmaksız | 613f1cd73d | |
dependabot[bot] | ab3e4844a9 | |
Orhun Parmaksız | 3f13beccaf | |
dependabot[bot] | 6a1b911652 | |
dependabot[bot] | fb0c075fc2 | |
dependabot[bot] | 1fb0858f01 | |
dependabot[bot] | ca68abd2a6 |
|
@ -1,3 +1,3 @@
|
|||
github: orhun
|
||||
patreon: orhunp
|
||||
custom: ["https://www.buymeacoffee.com/orhun"]
|
||||
buy_me_a_coffee: orhun
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
pull_request_rules:
|
||||
- name: Automatic merge for Dependabot pull requests
|
||||
conditions:
|
||||
- author=dependabot[bot]
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
|
@ -7,17 +7,11 @@ on:
|
|||
jobs:
|
||||
audit:
|
||||
name: Audit
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: Run cargo-audit
|
||||
uses: actions-rs/audit-check@v1
|
||||
uses: rustsec/audit-check@v1.4.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -30,30 +30,40 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup cargo-tarpaulin
|
||||
run: |
|
||||
curl -s https://api.github.com/repos/xd009642/tarpaulin/releases/latest | \
|
||||
grep "browser_download_url.*x86_64-unknown-linux-musl.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -qi -
|
||||
tar -xzf cargo-tarpaulin-*.tar.gz
|
||||
mv cargo-tarpaulin ~/.cargo/bin/
|
||||
- name: Run tests
|
||||
run: cargo tarpaulin --out Xml --verbose -- --test-threads 1
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Generate code coverage
|
||||
run: cargo llvm-cov --lcov --output-path lcov.info -- --test-threads 1
|
||||
env:
|
||||
OUT_DIR: target
|
||||
- name: Upload reports to codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
file: cobertura.xml
|
||||
file: lcov.info
|
||||
flags: unit-tests
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
fixtures:
|
||||
name: Test fixtures
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- name: Linux
|
||||
runner: ubuntu-22.04
|
||||
- name: macOS
|
||||
runner: macos-12
|
||||
name: Test fixtures [${{ matrix.os.name }}]
|
||||
runs-on: ${{ matrix.os.runner }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install coreutils for MacOS
|
||||
if: matrix.os.name == 'macOS'
|
||||
run: brew install coreutils
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Build the project
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
orhunp/rustypaste
|
||||
|
@ -35,12 +35,17 @@ jobs:
|
|||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
@ -49,14 +54,14 @@ jobs:
|
|||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: orhunp
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
@ -64,11 +69,11 @@ jobs:
|
|||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
|
|
@ -6,12 +6,8 @@ on:
|
|||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build / Deploy
|
||||
|
@ -35,7 +31,7 @@ jobs:
|
|||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
- name: Deploy
|
||||
if: ${{ startsWith(github.event.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
|
||||
run: |
|
||||
cargo shuttle login --api-key ${{ secrets.SHUTTLE_TOKEN }}
|
||||
cargo shuttle project restart
|
||||
|
|
88
CHANGELOG.md
88
CHANGELOG.md
|
@ -5,6 +5,94 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.15.0] - 2024-03-27
|
||||
|
||||
### Added
|
||||
|
||||
- Allow to override filename when using `random_url` by @tessus in [#233](https://github.com/orhun/rustypaste/pull/233)
|
||||
|
||||
Now you can use the `filename` header to override the name of the uploaded file.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
curl -F "file=@x.txt" -H "filename:override.txt" http://localhost:8000
|
||||
```
|
||||
|
||||
Even if `random_url` is set, the filename will be override.txt
|
||||
|
||||
[`rustypaste-cli`](https://github.com/orhun/rustypaste-cli) also has a new argument for overriding the file name:
|
||||
|
||||
```sh
|
||||
rpaste -n filename-on-server.txt awesome.txt
|
||||
```
|
||||
|
||||
- Use more specific HTTP status codes by @tessus in [#262](https://github.com/orhun/rustypaste/pull/262)
|
||||
|
||||
`rustypaste` now returns more appropriate status codes in the following 2 cases (instead of a generic 500 code):
|
||||
|
||||
- If the mime type is on the blacklist: `UnsupportedMediaType` (415)
|
||||
- If the file already exists: `Conflict` (409)
|
||||
|
||||
### Changed
|
||||
|
||||
- Do path joins more safely by @RealOrangeOne in [#247](https://github.com/orhun/rustypaste/pull/247)
|
||||
- Gracefully exit when there is no config file found by @orhun
|
||||
- Switch to cargo-llvm-cov for code coverage by @orhun in [#260](https://github.com/orhun/rustypaste/pull/260)
|
||||
- Replace unmaintained action by @tessus in [#266](https://github.com/orhun/rustypaste/pull/266)
|
||||
- Set up mergify by @orhun
|
||||
- Apply clippy suggestions by @orhun
|
||||
- Update funding options by @orhun
|
||||
- Update the copyright years by @orhun
|
||||
- Bump dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve logging for deleted file by @tessus in [#235](https://github.com/orhun/rustypaste/pull/235)
|
||||
- Fix deployment by @tessus in [#236](https://github.com/orhun/rustypaste/pull/236)
|
||||
- Return the correct file on multiple files with same name by @tessus in [#234](https://github.com/orhun/rustypaste/pull/234)
|
||||
- Update the hash of the example file by @tessus in [#254](https://github.com/orhun/rustypaste/pull/254)
|
||||
- Error on upload with the same filename by @tessus in [#258](https://github.com/orhun/rustypaste/pull/258)
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @RealOrangeOne made their first contribution in [#247](https://github.com/orhun/rustypaste/pull/247)
|
||||
|
||||
## [0.14.4] - 2023-12-20
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove excessive warning messages when auth tokens are not found (#210)
|
||||
|
||||
## [0.14.3] - 2023-12-12
|
||||
|
||||
### Changed
|
||||
|
||||
- Return `404` for not exposed endpoints instead of `403`
|
||||
- Disallow blank `delete_tokens` and `auth_tokens`
|
||||
- Bump dependencies
|
||||
|
||||
## [0.14.2] - 2023-12-05
|
||||
|
||||
### Added
|
||||
|
||||
- Add installation instructions for FreeBSD (#177)
|
||||
- Add auth token handling to HTML form example (#183)
|
||||
- Add release instructions
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Shuttle to `0.34.1`
|
||||
- Bump dependencies
|
||||
|
||||
## [0.14.1] - 2023-11-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch to `tracing` for logging (#163)
|
||||
- Bump Shuttle to `0.31.0`
|
||||
- Bump dependencies
|
||||
|
||||
## [0.14.0] - 2023-09-05
|
||||
|
||||
### Added
|
||||
|
|
File diff suppressed because it is too large
Load Diff
44
Cargo.toml
44
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypaste"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
description = "A minimal file upload/pastebin service"
|
||||
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
|
||||
|
@ -16,51 +16,47 @@ include = ["src/**/*", "Cargo.*", "LICENSE", "README.md", "CHANGELOG.md"]
|
|||
default = ["rustls"]
|
||||
openssl = ["actix-web/openssl", "awc/openssl"]
|
||||
rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
|
||||
shuttle = [
|
||||
"dep:shuttle-actix-web",
|
||||
"dep:shuttle-runtime",
|
||||
"dep:shuttle-static-folder",
|
||||
"dep:tokio",
|
||||
]
|
||||
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.4.0" }
|
||||
actix-web = { version = "4.5.1" }
|
||||
actix-web-grants = { version = "4.0.3" }
|
||||
actix-multipart = "0.6.1"
|
||||
actix-files = "0.6.2"
|
||||
awc = { version = "3.2.0" }
|
||||
env_logger = "0.10.0"
|
||||
log = "0.4.20"
|
||||
serde = "1.0.188"
|
||||
futures-util = "0.3.28"
|
||||
actix-files = "0.6.5"
|
||||
shuttle-actix-web = { version = "0.42.0", optional = true }
|
||||
shuttle-runtime = { version = "0.42.0", optional = true }
|
||||
awc = { version = "3.4.0" }
|
||||
serde = "1.0.197"
|
||||
futures-util = "0.3.30"
|
||||
petname = { version = "1.1.3", default-features = false, features = [
|
||||
"std_rng",
|
||||
"default_dictionary",
|
||||
] }
|
||||
rand = "0.8.5"
|
||||
dotenvy = "0.15.7"
|
||||
url = "2.4.1"
|
||||
url = "2.5.0"
|
||||
mime = "0.3.17"
|
||||
regex = "1.9.5"
|
||||
regex = "1.10.4"
|
||||
serde_regex = "1.1.0"
|
||||
lazy-regex = "3.0.1"
|
||||
lazy-regex = "3.1.0"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1.1"
|
||||
glob = "0.3.1"
|
||||
ring = "0.16.20"
|
||||
ring = "0.17.8"
|
||||
hotwatch = "0.5.0"
|
||||
shuttle-actix-web = { version = "0.25.0", optional = true }
|
||||
shuttle-runtime = { version = "0.25.0", optional = true }
|
||||
shuttle-static-folder = { version = "0.25.0", optional = true }
|
||||
tokio = { version = "1.32.0", optional = true }
|
||||
tokio = { version = "1.37.0", optional = true }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
uts2ts = "0.4.1"
|
||||
path-clean = "1.0.1"
|
||||
|
||||
[dependencies.config]
|
||||
version = "0.13.3"
|
||||
version = "0.14.0"
|
||||
default-features = false
|
||||
features = ["toml", "yaml"]
|
||||
|
||||
[dependencies.byte-unit]
|
||||
version = "4.0.19"
|
||||
version = "5.1.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.infer]
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-2023 Orhun Parmaksız
|
||||
Copyright (c) 2021-2024 Orhun Parmaksız
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
34
README.md
34
README.md
|
@ -20,10 +20,6 @@ $ curl https://paste.site.com/safe-toad.txt
|
|||
some text
|
||||
```
|
||||
|
||||
The public instance is available at [https://rustypaste.shuttleapp.rs](https://rustypaste.shuttleapp.rs) 🚀
|
||||
|
||||
Here you can read the blog post about how it is deployed on Shuttle: [https://blog.orhun.dev/blazingly-fast-file-sharing](https://blog.orhun.dev/blazingly-fast-file-sharing)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
@ -36,6 +32,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
|||
- [From crates.io](#from-cratesio)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Alpine Linux](#alpine-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Feature flags](#feature-flags)
|
||||
|
@ -51,6 +48,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
|||
- [Paste file from remote URL](#paste-file-from-remote-url)
|
||||
- [Cleaning up expired files](#cleaning-up-expired-files)
|
||||
- [Delete file from server](#delete-file-from-server)
|
||||
- [Override the filename when using `random_url`](#override-the-filename-when-using-random_url)
|
||||
- [Server](#server)
|
||||
- [List endpoint](#list-endpoint)
|
||||
- [HTML Form](#html-form)
|
||||
|
@ -87,6 +85,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
|||
- supports hot reloading
|
||||
- Easy to deploy
|
||||
- [docker images](https://hub.docker.com/r/orhunp/rustypaste)
|
||||
- [appjail images](https://github.com/AppJail-makejails/rustypaste)
|
||||
- No database
|
||||
- filesystem is used
|
||||
- Self-hosted
|
||||
|
@ -96,6 +95,13 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
|||
|
||||
## Installation
|
||||
|
||||
<details>
|
||||
<summary>Packaging status</summary>
|
||||
|
||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustypaste.svg)](https://repology.org/project/rustypaste/versions)
|
||||
|
||||
</details>
|
||||
|
||||
### From crates.io
|
||||
|
||||
```sh
|
||||
|
@ -116,6 +122,12 @@ pacman -S rustypaste
|
|||
apk add rustypaste
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
pkg install rustypaste
|
||||
```
|
||||
|
||||
### Binary releases
|
||||
|
||||
See the available binaries on the [releases](https://github.com/orhun/rustypaste/releases/) page.
|
||||
|
@ -244,6 +256,16 @@ Set `delete_tokens` array in [config.toml](./config.toml) to activate the [`DELE
|
|||
$ curl -H "Authorization: <auth_token>" -X DELETE "<server_address>/file.txt"
|
||||
```
|
||||
|
||||
> The `DELETE` endpoint will not be exposed and will return `404` error if `delete_tokens` are not set.
|
||||
|
||||
#### Override the filename when using `random_url`
|
||||
|
||||
The generation of a random filename can be overridden by sending a header called `filename`:
|
||||
|
||||
```sh
|
||||
curl -F "file=@x.txt" -H "filename: <file_name>" "<server_address>"
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
To start the server:
|
||||
|
@ -267,6 +289,10 @@ $ rustypaste
|
|||
|
||||
You can also set multiple auth tokens via the array field `[server].auth_tokens` in your `config.toml`.
|
||||
|
||||
> If neither `AUTH_TOKEN` nor `[server].auth_tokens` are set, the server will not require any authentication.
|
||||
>
|
||||
> Exception is the `DELETE` endpoint, which requires at least one token to be set. See [deleting files from server](#delete-file-from-server) for more information.
|
||||
|
||||
See [config.toml](./config.toml) for configuration options.
|
||||
|
||||
#### List endpoint
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# Creating a Release
|
||||
|
||||
1. Update the version in `Cargo.toml` accordingly to [Semantic Versioning](https://semver.org/).
|
||||
2. Run `cargo build` to update the version in `Cargo.lock`.
|
||||
3. Update [CHANGELOG.md](./CHANGELOG.md) accordingly.
|
||||
4. Commit the changes. (see [this](https://github.com/orhun/rustypaste/commit/79662d64abfe497baa5e9690c0f56ca183391809) example commit)
|
||||
5. Create a release [on GitHub](https://github.com/orhun/rustypaste/releases/new) with the same entries in `CHANGELOG.md`.
|
|
@ -26,7 +26,7 @@
|
|||
form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
input[type="url"],
|
||||
input[type="text"],
|
||||
input[type="file"] {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
|
@ -67,13 +67,13 @@ by default, pastes expire every hour.
|
|||
>
|
||||
<h2>share url</h2>
|
||||
<form action="/" method="post" enctype="multipart/form-data">
|
||||
<input type="url" name="url" />
|
||||
<input type="text" name="url" />
|
||||
<input type="submit" value="share" />
|
||||
</form>
|
||||
|
||||
<h2>share file from url</h2>
|
||||
<form action="/" method="post" enctype="multipart/form-data">
|
||||
<input type="url" name="remote" />
|
||||
<input type="text" name="remote" />
|
||||
<input type="submit" value="share" />
|
||||
</form>
|
||||
|
||||
|
@ -88,5 +88,56 @@ by default, pastes expire every hour.
|
|||
<input type="file" name="oneshot" />
|
||||
<input type="submit" value="share" />
|
||||
</form>
|
||||
|
||||
<h2>share file with auth token</h2>
|
||||
<div id="shareForm">
|
||||
<input type="file" id="file" name="file" /><br>
|
||||
<input type="text" id="authToken" placeholder="Auth Token" />
|
||||
<input type="submit" value="share" onclick="shareFileWithAuth()" />
|
||||
</form>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
function shareFileWithAuth() {
|
||||
const fileInput = document.getElementById("file");
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
alert("Please select a file");
|
||||
return;
|
||||
}
|
||||
|
||||
const authTokenInput = document.getElementById("authToken");
|
||||
const authToken = authTokenInput.value;
|
||||
|
||||
if (!authToken) {
|
||||
alert("Please provide an Auth Token");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
fetch("/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: authToken,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
alert("Failed to upload");
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((data) => {
|
||||
window.open(data, "_blank");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("There was an error uploading the file:", error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -7,6 +7,9 @@ This directory contains the [test fixtures](https://en.wikipedia.org/wiki/Test_f
|
|||
1. Build the project in debug mode: `cargo build`
|
||||
2. Execute the runner script in this directory: `./test-fixtures.sh`
|
||||
|
||||
On `macOS` you need to have [coreutils](https://www.gnu.org/software/coreutils/) installed to run the script.
|
||||
The simplest way is to install it via [Homebrew](https://brew.sh/): `brew install coreutils`
|
||||
|
||||
### Adding new fixtures
|
||||
|
||||
Create an appropriately named directory for the test fixture you want to add. e.g. `test-file-upload`
|
||||
|
|
|
@ -9,7 +9,7 @@ setup() {
|
|||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" -H "expire:1s" localhost:8000)
|
||||
test "$content" = "$(cat upload/file.txt.*)"
|
||||
sleep 2s
|
||||
sleep 2
|
||||
|
||||
result="$(curl -s $file_url)"
|
||||
test "file is not found or expired :(" = "$result"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "alphanumeric", length = "4", suffix_mode = true }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
|
||||
test "$file_url" = "http://localhost:8000/fn_from_header.txt"
|
||||
test "$content" = "$(cat upload/fn_from_header.txt)"
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "alphanumeric", length = "4", suffix_mode = true }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
|
||||
test "$file_url" = "http://localhost:8000/fn_from_header.txt"
|
||||
test "$content" = "$(cat upload/fn_from_header.txt)"
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
|
||||
test "$file_url" = "file already exists"
|
||||
status_code=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" -w "%{response_code}" -o /dev/null localhost:8000)
|
||||
test "$status_code" = "409"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
expose_list = false
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
setup() {
|
||||
:;
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s --write-out "%{http_code}" http://localhost:8000/list)
|
||||
test "404" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -r upload
|
||||
}
|
|
@ -10,7 +10,7 @@ setup() {
|
|||
|
||||
run_test() {
|
||||
seq $file_count | xargs -I -- curl -s -F "file=@file" -H "Authorization: $auth_token" localhost:8000 >/dev/null
|
||||
test "$file_count" = "$(curl -s -H "Authorization: $auth_token" localhost:8000/list | grep -o 'file_name' | wc -l)"
|
||||
test $file_count = $(curl -s -H "Authorization: $auth_token" localhost:8000/list | grep -o 'file_name' | wc -l)
|
||||
test "unauthorized" = "$(curl -s localhost:8000/list)"
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
||||
delete_expired_files = { enabled = true, interval = "1h" }
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" -H "expire:2s" localhost:8000)
|
||||
file_url=$(curl -s -F "file=@file" -H "expire:1s" localhost:8000)
|
||||
sleep 2
|
||||
file_url=$(curl -s -F "file=@file" -H "expire:1m" localhost:8000)
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="topsecret"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "unauthorized" != "$result"
|
||||
test "$content" = "$(cat upload/file.txt)"
|
||||
test "$content" = "$(curl -s $result)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -11,10 +11,10 @@ run_test() {
|
|||
second_file_url=$(curl -s -F "file=@file" -H "expire:4s" localhost:8000)
|
||||
test "$content" = "$(curl -s $first_file_url)"
|
||||
test "$content" = "$(curl -s $second_file_url)"
|
||||
sleep 3s
|
||||
sleep 3
|
||||
test "file is not found or expired :(" = "$(curl -s $first_file_url)"
|
||||
test "$content" = "$(curl -s $second_file_url)"
|
||||
sleep 1s
|
||||
sleep 1
|
||||
test "file is not found or expired :(" = "$(curl -s $second_file_url)"
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$file_url" = "http://localhost:8000/file.txt"
|
||||
|
||||
result=$(curl -s --write-out "%{http_code}" -X DELETE http://localhost:8000/file.txt)
|
||||
test "404" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -11,6 +11,7 @@ setup() {
|
|||
run_test() {
|
||||
test "this file type is not permitted" = "$(curl -s -F "file=@file.html" localhost:8000)"
|
||||
test "this file type is not permitted" = "$(curl -s -F "file=@file.xml" localhost:8000)"
|
||||
test "415" = "$(curl -s -F "file=@file.xml" -w "%{response_code}" -o /dev/null localhost:8000)"
|
||||
file_url=$(curl -s -F "file=@file.txt" localhost:8000)
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10kb"
|
||||
max_content_length = "10KB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
setup() {
|
||||
touch emptyfile
|
||||
truncate -s 9KB smallfile
|
||||
fallocate -l 10000 normalfile
|
||||
# On Linux, `fallocate -l 10000 normalfile` can be used for a better precision.
|
||||
dd if=/dev/random of=normalfile count=10000 bs=1024 status=none
|
||||
truncate -s 11KB bigfile
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
expose_version = false
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s --write-out "%{http_code}" http://localhost:8000/version)
|
||||
test "404" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
187
src/auth.rs
187
src/auth.rs
|
@ -1,55 +1,162 @@
|
|||
use actix_web::http::header::{HeaderMap, AUTHORIZATION};
|
||||
use actix_web::{error, Error};
|
||||
use crate::config::{Config, TokenType};
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::http::header::AUTHORIZATION;
|
||||
use actix_web::http::Method;
|
||||
use actix_web::middleware::ErrorHandlerResponse;
|
||||
use actix_web::{error, web, Error};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Checks the authorization header for the specified token.
|
||||
/// Extracts the tokens from the authorization header by token type.
|
||||
///
|
||||
/// `Authorization: (type) <token>`
|
||||
pub fn check(host: &str, headers: &HeaderMap, tokens: Option<Vec<String>>) -> Result<(), Error> {
|
||||
if let Some(tokens) = tokens {
|
||||
let auth_header = headers
|
||||
.get(AUTHORIZATION)
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
.map(|v| v.split_whitespace().last().unwrap_or_default());
|
||||
if !tokens.iter().any(|v| v == auth_header.unwrap_or_default()) {
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!(
|
||||
"authorization failure for {host} (token: {})",
|
||||
auth_header.unwrap_or("none"),
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("authorization failure for {host}");
|
||||
return Err(error::ErrorUnauthorized("unauthorized\n"));
|
||||
pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<TokenType>, Error> {
|
||||
let config = req
|
||||
.app_data::<web::Data<RwLock<Config>>>()
|
||||
.map(|cfg| cfg.read())
|
||||
.and_then(Result::ok)
|
||||
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
|
||||
let mut user_tokens = HashSet::with_capacity(2);
|
||||
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get(AUTHORIZATION)
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
.map(|v| v.split_whitespace().last().unwrap_or_default());
|
||||
|
||||
for token_type in [TokenType::Auth, TokenType::Delete] {
|
||||
let maybe_tokens = config.get_tokens(token_type);
|
||||
if let Some(configured_tokens) = maybe_tokens {
|
||||
if configured_tokens.contains(auth_header.unwrap_or_default()) {
|
||||
user_tokens.insert(token_type);
|
||||
}
|
||||
} else if token_type == TokenType::Auth {
|
||||
// not configured `auth_tokens` means that the user is allowed to access the endpoints
|
||||
user_tokens.insert(token_type);
|
||||
} else if token_type == TokenType::Delete && req.method() == Method::DELETE {
|
||||
// explicitly disable `DELETE` methods if no `delete_tokens` are set
|
||||
warn!("delete endpoint is not served because there are no delete_tokens set");
|
||||
Err(error::ErrorNotFound(""))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Ok(user_tokens)
|
||||
}
|
||||
|
||||
/// Returns `HttpResponse` with unauthorized (`401`) error and `unauthorized\n` as body.
|
||||
pub(crate) fn unauthorized_error() -> actix_web::HttpResponse {
|
||||
error::ErrorUnauthorized("unauthorized\n").into()
|
||||
}
|
||||
|
||||
/// Log all unauthorized requests.
|
||||
pub(crate) fn handle_unauthorized_error<B>(
|
||||
res: ServiceResponse<B>,
|
||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
let connection = res.request().connection_info().clone();
|
||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let auth_header = res
|
||||
.request()
|
||||
.headers()
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("none");
|
||||
|
||||
warn!("authorization failure for {host} (token: {auth_header})",);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
warn!("authorization failure for {host}");
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use actix_web::http::header::HeaderValue;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::HttpResponse;
|
||||
use awc::http::StatusCode;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_extract_tokens() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
|
||||
// request without configured auth-tokens
|
||||
let request = TestRequest::default()
|
||||
.app_data(Data::new(RwLock::new(config.clone())))
|
||||
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
|
||||
.to_srv_request();
|
||||
let tokens = extract_tokens(&request).await?;
|
||||
assert_eq!(HashSet::from([TokenType::Auth]), tokens);
|
||||
|
||||
// request with configured auth-tokens
|
||||
config.server.auth_tokens = Some(["test_token".to_string()].into());
|
||||
let request = TestRequest::default()
|
||||
.app_data(Data::new(RwLock::new(config.clone())))
|
||||
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
|
||||
.to_srv_request();
|
||||
let tokens = extract_tokens(&request).await?;
|
||||
assert_eq!(HashSet::from([TokenType::Auth]), tokens);
|
||||
|
||||
// request with configured auth-tokens but wrong token in request
|
||||
config.server.auth_tokens = Some(["test_token".to_string()].into());
|
||||
let request = TestRequest::default()
|
||||
.app_data(Data::new(RwLock::new(config.clone())))
|
||||
.insert_header((
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_static("basic invalid_token"),
|
||||
))
|
||||
.to_srv_request();
|
||||
let tokens = extract_tokens(&request).await?;
|
||||
assert_eq!(HashSet::new(), tokens);
|
||||
|
||||
// DELETE request without configured delete-tokens
|
||||
let request = TestRequest::default()
|
||||
.method(Method::DELETE)
|
||||
.app_data(Data::new(RwLock::new(config.clone())))
|
||||
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
|
||||
.to_srv_request();
|
||||
let res = extract_tokens(&request).await;
|
||||
assert!(res.is_err());
|
||||
assert_eq!(
|
||||
Some(StatusCode::NOT_FOUND),
|
||||
res.err()
|
||||
.as_ref()
|
||||
.map(Error::error_response)
|
||||
.as_ref()
|
||||
.map(HttpResponse::status)
|
||||
);
|
||||
|
||||
// DELETE request with configured delete-tokens
|
||||
config.server.delete_tokens = Some(["delete_token".to_string()].into());
|
||||
let request = TestRequest::default()
|
||||
.method(Method::DELETE)
|
||||
.app_data(Data::new(RwLock::new(config.clone())))
|
||||
.insert_header((
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_static("basic delete_token"),
|
||||
))
|
||||
.to_srv_request();
|
||||
let tokens = extract_tokens(&request).await?;
|
||||
assert_eq!(HashSet::from([TokenType::Delete]), tokens);
|
||||
|
||||
// DELETE request with configured delete-tokens but wrong token in request
|
||||
let request = TestRequest::default()
|
||||
.method(Method::DELETE)
|
||||
.app_data(Data::new(RwLock::new(config.clone())))
|
||||
.insert_header((
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_static("basic invalid_token"),
|
||||
))
|
||||
.to_srv_request();
|
||||
let tokens = extract_tokens(&request).await?;
|
||||
assert_eq!(HashSet::new(), tokens);
|
||||
|
||||
#[test]
|
||||
fn test_check_auth() -> Result<(), Error> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, HeaderValue::from_static("basic test_token"));
|
||||
assert!(check("", &headers, Some(vec!["test_token".to_string()])).is_ok());
|
||||
assert!(check("", &headers, Some(vec!["invalid_token".to_string()])).is_err());
|
||||
assert!(check(
|
||||
"",
|
||||
&headers,
|
||||
Some(vec!["invalid1".to_string(), "test_token".to_string()])
|
||||
)
|
||||
.is_ok());
|
||||
assert!(check(
|
||||
"",
|
||||
&headers,
|
||||
Some(vec!["invalid1".to_string(), "invalid2".to_string()])
|
||||
)
|
||||
.is_err());
|
||||
assert!(check("", &headers, None).is_ok());
|
||||
assert!(check("", &HeaderMap::new(), None).is_ok());
|
||||
assert!(check("", &HeaderMap::new(), Some(vec!["token".to_string()])).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::random::RandomURLConfig;
|
|||
use crate::{AUTH_TOKEN_ENV, DELETE_TOKEN_ENV};
|
||||
use byte_unit::Byte;
|
||||
use config::{self, ConfigError};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
@ -49,7 +50,7 @@ pub struct ServerConfig {
|
|||
#[deprecated(note = "use [server].auth_tokens instead")]
|
||||
pub auth_token: Option<String>,
|
||||
/// Authentication tokens.
|
||||
pub auth_tokens: Option<Vec<String>>,
|
||||
pub auth_tokens: Option<HashSet<String>>,
|
||||
/// Expose version.
|
||||
pub expose_version: Option<bool>,
|
||||
/// Landing page text.
|
||||
|
@ -63,7 +64,7 @@ pub struct ServerConfig {
|
|||
/// Path of the JSON index.
|
||||
pub expose_list: Option<bool>,
|
||||
/// Authentication tokens for deleting.
|
||||
pub delete_tokens: Option<Vec<String>>,
|
||||
pub delete_tokens: Option<HashSet<String>>,
|
||||
}
|
||||
|
||||
/// Enum representing different strategies for handling spaces in filenames.
|
||||
|
@ -130,6 +131,7 @@ pub struct CleanupConfig {
|
|||
}
|
||||
|
||||
/// Type of access token.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum TokenType {
|
||||
/// Token for authentication.
|
||||
Auth,
|
||||
|
@ -148,48 +150,52 @@ impl Config {
|
|||
}
|
||||
|
||||
/// Retrieves all configured auth/delete tokens.
|
||||
pub fn get_tokens(&self, token_type: TokenType) -> Option<Vec<String>> {
|
||||
pub fn get_tokens(&self, token_type: TokenType) -> Option<HashSet<String>> {
|
||||
let mut tokens = match token_type {
|
||||
TokenType::Auth => {
|
||||
let mut tokens = self.server.auth_tokens.clone().unwrap_or_default();
|
||||
let mut tokens: HashSet<_> = self.server.auth_tokens.clone().unwrap_or_default();
|
||||
|
||||
#[allow(deprecated)]
|
||||
if let Some(token) = &self.server.auth_token {
|
||||
tokens.insert(0, token.to_string());
|
||||
tokens.insert(token.to_string());
|
||||
}
|
||||
if let Ok(env_token) = env::var(AUTH_TOKEN_ENV) {
|
||||
tokens.insert(0, env_token);
|
||||
tokens.insert(env_token);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
TokenType::Delete => {
|
||||
let mut tokens = self.server.delete_tokens.clone().unwrap_or_default();
|
||||
let mut tokens: HashSet<_> = self.server.delete_tokens.clone().unwrap_or_default();
|
||||
|
||||
if let Ok(env_token) = env::var(DELETE_TOKEN_ENV) {
|
||||
tokens.insert(0, env_token);
|
||||
tokens.insert(env_token);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
};
|
||||
tokens.retain(|v| !v.is_empty());
|
||||
(!tokens.is_empty()).then_some(tokens)
|
||||
|
||||
// filter out blank tokens
|
||||
tokens.retain(|v| !v.trim().is_empty());
|
||||
Some(tokens).filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
/// Print deprecation warnings.
|
||||
#[allow(deprecated)]
|
||||
pub fn warn_deprecation(&self) {
|
||||
if self.server.auth_token.is_some() {
|
||||
log::warn!("[server].auth_token is deprecated, please use [server].auth_tokens");
|
||||
warn!("[server].auth_token is deprecated, please use [server].auth_tokens");
|
||||
}
|
||||
if self.server.landing_page.is_some() {
|
||||
log::warn!("[server].landing_page is deprecated, please use [landing_page].text");
|
||||
warn!("[server].landing_page is deprecated, please use [landing_page].text");
|
||||
}
|
||||
if self.server.landing_page_content_type.is_some() {
|
||||
log::warn!(
|
||||
warn!(
|
||||
"[server].landing_page_content_type is deprecated, please use [landing_page].content_type"
|
||||
);
|
||||
}
|
||||
if let Some(random_url) = &self.paste.random_url {
|
||||
if random_url.enabled.is_some() {
|
||||
log::warn!(
|
||||
warn!(
|
||||
"[paste].random_url.enabled is deprecated, disable it by commenting out [paste].random_url"
|
||||
);
|
||||
}
|
||||
|
@ -241,24 +247,33 @@ mod tests {
|
|||
env::set_var("AUTH_TOKEN", "env_auth");
|
||||
env::set_var("DELETE_TOKEN", "env_delete");
|
||||
let mut config = Config::parse(&config_path)?;
|
||||
config.server.auth_tokens = Some(vec!["may_the_force_be_with_you".to_string()]);
|
||||
config.server.delete_tokens = Some(vec!["i_am_your_father".to_string()]);
|
||||
// empty tokens will be filtered
|
||||
config.server.auth_tokens =
|
||||
Some(["may_the_force_be_with_you".to_string(), "".to_string()].into());
|
||||
config.server.delete_tokens = Some(["i_am_your_father".to_string(), "".to_string()].into());
|
||||
assert_eq!(
|
||||
Some(vec![
|
||||
Some(HashSet::from([
|
||||
"env_auth".to_string(),
|
||||
"may_the_force_be_with_you".to_string()
|
||||
]),
|
||||
])),
|
||||
config.get_tokens(TokenType::Auth)
|
||||
);
|
||||
assert_eq!(
|
||||
Some(vec![
|
||||
Some(HashSet::from([
|
||||
"env_delete".to_string(),
|
||||
"i_am_your_father".to_string()
|
||||
]),
|
||||
])),
|
||||
config.get_tokens(TokenType::Delete)
|
||||
);
|
||||
env::remove_var("AUTH_TOKEN");
|
||||
env::remove_var("DELETE_TOKEN");
|
||||
|
||||
// `get_tokens` returns `None` if no tokens are configured
|
||||
config.server.auth_tokens = Some([" ".to_string()].into());
|
||||
config.server.delete_tokens = Some(HashSet::new());
|
||||
assert_eq!(None, config.get_tokens(TokenType::Auth));
|
||||
assert_eq!(None, config.get_tokens(TokenType::Delete));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ use std::time::Duration;
|
|||
/// Custom HTTP header for expiry dates.
|
||||
pub const EXPIRE: &str = "expire";
|
||||
|
||||
/// Custom HTTP header to override filename.
|
||||
const FILENAME: &str = "filename";
|
||||
|
||||
/// Parses the expiry date from the [`custom HTTP header`](EXPIRE).
|
||||
pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u128>, ActixError> {
|
||||
if let Some(expire_time) = headers.get(EXPIRE).and_then(|v| v.to_str().ok()) {
|
||||
|
@ -18,6 +21,15 @@ pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses the filename from the header.
|
||||
pub fn parse_header_filename(headers: &HeaderMap) -> Result<Option<String>, ActixError> {
|
||||
if let Some(file_name) = headers.get(FILENAME).and_then(|v| v.to_str().ok()) {
|
||||
Ok(Some(file_name.to_string()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for Actix content disposition header.
|
||||
///
|
||||
/// Aims to parse the file data from multipart body.
|
||||
|
|
|
@ -31,6 +31,10 @@ pub mod util;
|
|||
/// Custom middleware implementation.
|
||||
pub mod middleware;
|
||||
|
||||
// Use macros from tracing crate.
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
/// Environment variable for setting the configuration file path.
|
||||
pub const CONFIG_ENV: &str = "CONFIG";
|
||||
|
||||
|
|
62
src/main.rs
62
src/main.rs
|
@ -18,12 +18,20 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::{mpsc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
#[cfg(not(feature = "shuttle"))]
|
||||
use tracing_subscriber::{
|
||||
filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter,
|
||||
};
|
||||
#[cfg(feature = "shuttle")]
|
||||
use {
|
||||
actix_web::web::{self, ServiceConfig},
|
||||
shuttle_actix_web::ShuttleActixWeb,
|
||||
};
|
||||
|
||||
// Use macros from tracing crate.
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
/// Sets up the application.
|
||||
///
|
||||
/// * loads the configuration
|
||||
|
@ -36,7 +44,14 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
|
||||
// Initialize logger.
|
||||
#[cfg(not(feature = "shuttle"))]
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
// Parse configuration.
|
||||
let config_path = match env::var(CONFIG_ENV).ok() {
|
||||
|
@ -46,8 +61,15 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
}
|
||||
None => config_folder.join("config.toml"),
|
||||
};
|
||||
if !config_path.exists() {
|
||||
error!(
|
||||
"{} is not found, please provide a configuration file.",
|
||||
config_path.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
let config = Config::parse(&config_path).expect("failed to parse config");
|
||||
log::trace!("{:#?}", config);
|
||||
trace!("{:#?}", config);
|
||||
config.warn_deprecation();
|
||||
let server_config = config.server.clone();
|
||||
let paste_config = RwLock::new(config.paste.clone());
|
||||
|
@ -56,7 +78,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
// Create necessary directories.
|
||||
fs::create_dir_all(&server_config.upload_path)?;
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||
fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?;
|
||||
fs::create_dir_all(paste_type.get_path(&server_config.upload_path)?)?;
|
||||
}
|
||||
|
||||
// Set up a watcher for the configuration file changes.
|
||||
|
@ -74,24 +96,24 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
let cloned_config = Data::clone(&config);
|
||||
let config_watcher = move |event: Event| {
|
||||
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
|
||||
(event.kind, event.paths.get(0))
|
||||
(event.kind, event.paths.first())
|
||||
{
|
||||
match Config::parse(path) {
|
||||
Ok(config) => match cloned_config.write() {
|
||||
Ok(mut cloned_config) => {
|
||||
*cloned_config = config.clone();
|
||||
log::info!("Configuration has been updated.");
|
||||
info!("Configuration has been updated.");
|
||||
if let Err(e) = config_sender.send(config) {
|
||||
log::error!("Failed to send config for the cleanup routine: {}", e)
|
||||
error!("Failed to send config for the cleanup routine: {}", e)
|
||||
}
|
||||
cloned_config.warn_deprecation();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to acquire config: {}", e);
|
||||
error!("Failed to acquire config: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to update config: {}", e);
|
||||
error!("Failed to update config: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,11 +132,11 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
.and_then(|v| v.delete_expired_files.clone())
|
||||
{
|
||||
if cleanup_config.enabled {
|
||||
log::debug!("Running cleanup...");
|
||||
debug!("Running cleanup...");
|
||||
for file in util::get_expired_files(&upload_path) {
|
||||
match fs::remove_file(&file) {
|
||||
Ok(()) => log::info!("Removed expired file: {:?}", file),
|
||||
Err(e) => log::error!("Cannot remove expired file: {}", e),
|
||||
Ok(()) => info!("Removed expired file: {:?}", file),
|
||||
Err(e) => error!("Cannot remove expired file: {}", e),
|
||||
}
|
||||
}
|
||||
thread::sleep(cleanup_config.interval);
|
||||
|
@ -131,7 +153,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
*paste_config = new_config.paste;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to update config for the cleanup routine: {}", e);
|
||||
error!("Failed to update config for the cleanup routine: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -162,9 +184,7 @@ async fn main() -> IoResult<()> {
|
|||
.wrap(Logger::new(
|
||||
"%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
|
||||
))
|
||||
.wrap(ContentLengthLimiter::new(
|
||||
server_config.max_content_length.get_bytes(),
|
||||
))
|
||||
.wrap(ContentLengthLimiter::new(server_config.max_content_length))
|
||||
.configure(server::configure_routes)
|
||||
})
|
||||
.bind(&server_config.address)?;
|
||||
|
@ -175,17 +195,15 @@ async fn main() -> IoResult<()> {
|
|||
}
|
||||
|
||||
// Run the server.
|
||||
log::info!("Server is running at {}", server_config.address);
|
||||
info!("Server is running at {}", server_config.address);
|
||||
http_server.run().await
|
||||
}
|
||||
|
||||
#[cfg(feature = "shuttle")]
|
||||
#[shuttle_runtime::main]
|
||||
async fn actix_web(
|
||||
#[shuttle_static_folder::StaticFolder(folder = "shuttle")] static_folder: PathBuf,
|
||||
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
|
||||
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
|
||||
// Set up the application.
|
||||
let (config, server_config, _hotwatch) = setup(&static_folder)?;
|
||||
let (config, server_config, _hotwatch) = setup(Path::new("shuttle"))?;
|
||||
|
||||
// Create the service.
|
||||
let service_config = move |cfg: &mut ServiceConfig| {
|
||||
|
@ -204,9 +222,7 @@ async fn actix_web(
|
|||
.wrap(Logger::new(
|
||||
"%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
|
||||
))
|
||||
.wrap(ContentLengthLimiter::new(
|
||||
server_config.max_content_length.get_bytes(),
|
||||
))
|
||||
.wrap(ContentLengthLimiter::new(server_config.max_content_length))
|
||||
.configure(server::configure_routes),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ use actix_web::http::header::CONTENT_LENGTH;
|
|||
use actix_web::http::StatusCode;
|
||||
use actix_web::{body::EitherBody, Error};
|
||||
use actix_web::{HttpMessage, HttpResponseBuilder};
|
||||
use byte_unit::Byte;
|
||||
use futures_util::{Future, TryStreamExt};
|
||||
use std::{
|
||||
future::{ready, Ready},
|
||||
|
@ -14,12 +15,12 @@ use std::{
|
|||
#[derive(Debug)]
|
||||
pub struct ContentLengthLimiter {
|
||||
// Maximum amount of bytes to allow.
|
||||
max_bytes: u128,
|
||||
max_bytes: Byte,
|
||||
}
|
||||
|
||||
impl ContentLengthLimiter {
|
||||
/// Contructs a new instance.
|
||||
pub fn new(max_bytes: u128) -> Self {
|
||||
/// Constructs a new instance.
|
||||
pub fn new(max_bytes: Byte) -> Self {
|
||||
Self { max_bytes }
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +48,7 @@ where
|
|||
#[derive(Debug)]
|
||||
pub struct ContentLengthLimiterMiddleware<S> {
|
||||
service: Rc<S>,
|
||||
max_bytes: u128,
|
||||
max_bytes: Byte,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for ContentLengthLimiterMiddleware<S>
|
||||
|
@ -66,10 +67,13 @@ where
|
|||
.headers()
|
||||
.get(CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u128>().ok())
|
||||
.and_then(|v| v.parse::<Byte>().ok())
|
||||
{
|
||||
if content_length > self.max_bytes {
|
||||
log::warn!("Upload rejected due to exceeded limit.");
|
||||
warn!(
|
||||
"Upload rejected due to exceeded limit. ({:-#} > {:-#})",
|
||||
content_length, self.max_bytes
|
||||
);
|
||||
return Box::pin(async move {
|
||||
// drain the body due to https://github.com/actix/actix-web/issues/2695
|
||||
let mut payload = request.take_payload();
|
||||
|
|
112
src/paste.rs
112
src/paste.rs
|
@ -58,12 +58,12 @@ impl PasteType {
|
|||
}
|
||||
|
||||
/// Returns the given path with [`directory`](Self::get_dir) adjoined.
|
||||
pub fn get_path(&self, path: &Path) -> PathBuf {
|
||||
pub fn get_path(&self, path: &Path) -> IoResult<PathBuf> {
|
||||
let dir = self.get_dir();
|
||||
if dir.is_empty() {
|
||||
path.to_path_buf()
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
path.join(dir)
|
||||
util::safe_path_join(path, Path::new(&dir))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,7 @@ impl Paste {
|
|||
/// - If `file_name` does not have an extension, it is replaced with [`default_extension`].
|
||||
/// - If `file_name` is "-", it is replaced with "stdin".
|
||||
/// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string.
|
||||
/// - If `header_filename` is set, it will override the filename.
|
||||
///
|
||||
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||
|
@ -95,15 +96,15 @@ impl Paste {
|
|||
&self,
|
||||
file_name: &str,
|
||||
expiry_date: Option<u128>,
|
||||
header_filename: Option<String>,
|
||||
config: &Config,
|
||||
) -> IoResult<String> {
|
||||
) -> Result<String, Error> {
|
||||
let file_type = infer::get(&self.data);
|
||||
if let Some(file_type) = file_type {
|
||||
for mime_type in &config.paste.mime_blacklist {
|
||||
if mime_type == file_type.mime_type() {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::Other,
|
||||
String::from("this file type is not permitted"),
|
||||
return Err(error::ErrorUnsupportedMediaType(
|
||||
"this file type is not permitted",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -120,10 +121,9 @@ impl Paste {
|
|||
if let Some(handle_spaces_config) = config.server.handle_spaces {
|
||||
file_name = handle_spaces_config.process_filename(&file_name);
|
||||
}
|
||||
let mut path = self
|
||||
.type_
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(&file_name);
|
||||
|
||||
let mut path =
|
||||
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
|
||||
let mut parts: Vec<&str> = file_name.split('.').collect();
|
||||
let mut dotfile = false;
|
||||
let mut lower_bound = 1;
|
||||
|
@ -166,11 +166,20 @@ impl Paste {
|
|||
}
|
||||
path.set_file_name(file_name);
|
||||
path.set_extension(extension);
|
||||
if let Some(header_filename) = header_filename {
|
||||
file_name = header_filename;
|
||||
path.set_file_name(file_name);
|
||||
}
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|v| v.to_string_lossy())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let file_path = util::glob_match_file(path.clone())
|
||||
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
|
||||
if file_path.is_file() && file_path.exists() {
|
||||
return Err(error::ErrorConflict("file already exists\n"));
|
||||
}
|
||||
if let Some(timestamp) = expiry_date {
|
||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||
}
|
||||
|
@ -210,7 +219,6 @@ impl Paste {
|
|||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
.server
|
||||
.max_content_length
|
||||
.get_bytes()
|
||||
.try_into()
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
let bytes = response
|
||||
|
@ -236,7 +244,7 @@ impl Paste {
|
|||
.to_string());
|
||||
}
|
||||
}
|
||||
Ok(self.store_file(file_name, expiry_date, &config)?)
|
||||
self.store_file(file_name, expiry_date, None, &config)
|
||||
}
|
||||
|
||||
/// Writes an URL to a file in upload directory.
|
||||
|
@ -256,10 +264,8 @@ impl Paste {
|
|||
file_name = random_text;
|
||||
}
|
||||
}
|
||||
let mut path = self
|
||||
.type_
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(&file_name);
|
||||
let mut path =
|
||||
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
|
||||
if let Some(timestamp) = expiry_date {
|
||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||
}
|
||||
|
@ -277,6 +283,7 @@ mod tests {
|
|||
use awc::ClientBuilder;
|
||||
use byte_unit::Byte;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -295,7 +302,7 @@ mod tests {
|
|||
data: vec![65, 66, 67],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("test.txt", None, &config)?;
|
||||
let file_name = paste.store_file("test.txt", None, None, &config)?;
|
||||
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(
|
||||
Some("txt"),
|
||||
|
@ -315,7 +322,7 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
assert!(file_name.starts_with("foo."));
|
||||
|
@ -331,7 +338,7 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(".foo.tar.gz", None, &config)?;
|
||||
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
assert!(file_name.starts_with(".foo."));
|
||||
|
@ -347,7 +354,7 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
fs::remove_file(file_name)?;
|
||||
|
@ -358,7 +365,7 @@ mod tests {
|
|||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(".foo", None, &config)?;
|
||||
let file_name = paste.store_file(".foo", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(".foo.txt", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
|
@ -373,7 +380,7 @@ mod tests {
|
|||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("random", None, &config)?;
|
||||
let file_name = paste.store_file("random", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(
|
||||
Some("bin"),
|
||||
|
@ -383,8 +390,52 @@ mod tests {
|
|||
);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
type_: RandomURLType::Alphanumeric,
|
||||
suffix_mode: Some(true),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let paste = Paste {
|
||||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header.txt".to_string()),
|
||||
&config,
|
||||
)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert_eq!("fn_from_header.txt", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
type_: RandomURLType::Alphanumeric,
|
||||
suffix_mode: Some(true),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let paste = Paste {
|
||||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header".to_string()),
|
||||
&config,
|
||||
)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert_eq!("fn_from_header", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?;
|
||||
fs::create_dir_all(
|
||||
paste_type
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path"),
|
||||
)?;
|
||||
}
|
||||
|
||||
config.paste.random_url = None;
|
||||
|
@ -393,9 +444,10 @@ mod tests {
|
|||
type_: PasteType::Oneshot,
|
||||
};
|
||||
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
|
||||
let file_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(format!("{file_name}.{expiry_date}"));
|
||||
assert_eq!("test", fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
|
@ -412,6 +464,7 @@ mod tests {
|
|||
let file_name = paste.store_url(None, &config)?;
|
||||
let file_path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(&file_name);
|
||||
assert_eq!(url, fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
|
@ -439,15 +492,20 @@ mod tests {
|
|||
.await?;
|
||||
let file_path = PasteType::RemoteFile
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(file_name);
|
||||
assert_eq!(
|
||||
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
|
||||
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
||||
util::sha256_digest(&*paste.data)?
|
||||
);
|
||||
fs::remove_file(file_path)?;
|
||||
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::remove_dir(paste_type.get_path(&config.server.upload_path))?;
|
||||
fs::remove_dir(
|
||||
paste_type
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path"),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
243
src/server.rs
243
src/server.rs
|
@ -1,15 +1,18 @@
|
|||
use crate::auth;
|
||||
use crate::auth::{extract_tokens, handle_unauthorized_error, unauthorized_error};
|
||||
use crate::config::{Config, LandingPageConfig, TokenType};
|
||||
use crate::file::Directory;
|
||||
use crate::header::{self, ContentDisposition};
|
||||
use crate::mime as mime_util;
|
||||
use crate::paste::{Paste, PasteType};
|
||||
use crate::util;
|
||||
use crate::util::{self, safe_path_join};
|
||||
use actix_files::NamedFile;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::middleware::ErrorHandlers;
|
||||
use actix_web::{delete, error, get, post, web, Error, HttpRequest, HttpResponse};
|
||||
use actix_web_grants::GrantsMiddleware;
|
||||
use awc::Client;
|
||||
use byte_unit::Byte;
|
||||
use byte_unit::{Byte, UnitType};
|
||||
use futures_util::stream::StreamExt;
|
||||
use mime::TEXT_PLAIN_UTF_8;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -86,12 +89,11 @@ async fn serve(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let path = config.server.upload_path.join(&*file);
|
||||
let mut path = util::glob_match_file(path)?;
|
||||
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
||||
let mut paste_type = PasteType::File;
|
||||
if !path.exists() || path.is_dir() {
|
||||
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||
let alt_path = type_.get_path(&config.server.upload_path).join(&*file);
|
||||
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
|
||||
let alt_path = util::glob_match_file(alt_path)?;
|
||||
if alt_path.exists()
|
||||
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
|
||||
|
@ -148,31 +150,22 @@ async fn serve(
|
|||
|
||||
/// Remove a file from the upload directory.
|
||||
#[delete("/{file}")]
|
||||
#[actix_web_grants::protect("TokenType::Delete", ty = TokenType, error = unauthorized_error)]
|
||||
async fn delete(
|
||||
request: HttpRequest,
|
||||
file: web::Path<String>,
|
||||
config: web::Data<RwLock<Config>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let path = config.server.upload_path.join(&*file);
|
||||
let path = util::glob_match_file(path)?;
|
||||
let connection = request.connection_info().clone();
|
||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||
let tokens = config.get_tokens(TokenType::Delete);
|
||||
if tokens.is_none() {
|
||||
log::warn!("delete endpoint is not served because there are no delete_tokens set");
|
||||
return Err(error::ErrorForbidden("endpoint is not exposed\n"));
|
||||
}
|
||||
auth::check(host, request.headers(), tokens)?;
|
||||
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
||||
if !path.is_file() || !path.exists() {
|
||||
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
||||
}
|
||||
match fs::remove_file(path) {
|
||||
Ok(_) => log::info!("deleted file: {:?}", file),
|
||||
Ok(_) => info!("deleted file: {:?}", file.to_string()),
|
||||
Err(e) => {
|
||||
log::error!("cannot delete file: {}", e);
|
||||
error!("cannot delete file: {}", e);
|
||||
return Err(error::ErrorInternalServerError("cannot delete file"));
|
||||
}
|
||||
}
|
||||
|
@ -181,27 +174,23 @@ async fn delete(
|
|||
|
||||
/// Expose version endpoint
|
||||
#[get("/version")]
|
||||
async fn version(
|
||||
request: HttpRequest,
|
||||
config: web::Data<RwLock<Config>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let connection = request.connection_info().clone();
|
||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||
let tokens = config.get_tokens(TokenType::Auth);
|
||||
auth::check(host, request.headers(), tokens)?;
|
||||
if !config.server.expose_version.unwrap_or(false) {
|
||||
log::warn!("server is not configured to expose version endpoint");
|
||||
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
|
||||
warn!("server is not configured to expose version endpoint");
|
||||
Err(error::ErrorNotFound(""))?;
|
||||
}
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
Ok(HttpResponse::Ok().body(version.to_owned() + "\n"))
|
||||
}
|
||||
|
||||
/// Handles file upload by processing `multipart/form-data`.
|
||||
#[post("/")]
|
||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||
async fn upload(
|
||||
request: HttpRequest,
|
||||
mut payload: Multipart,
|
||||
|
@ -210,13 +199,6 @@ async fn upload(
|
|||
) -> Result<HttpResponse, Error> {
|
||||
let connection = request.connection_info().clone();
|
||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||
{
|
||||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let tokens = config.get_tokens(TokenType::Auth);
|
||||
auth::check(host, request.headers(), tokens)?;
|
||||
}
|
||||
let server_url = match config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
|
@ -241,6 +223,7 @@ async fn upload(
|
|||
}
|
||||
let mut urls: Vec<String> = Vec::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
let header_filename = header::parse_header_filename(request.headers())?;
|
||||
let mut field = item?;
|
||||
let content = ContentDisposition::from(field.content_disposition().clone());
|
||||
if let Ok(paste_type) = PasteType::try_from(&content) {
|
||||
|
@ -249,7 +232,7 @@ async fn upload(
|
|||
bytes.append(&mut chunk?.to_vec());
|
||||
}
|
||||
if bytes.is_empty() {
|
||||
log::warn!("{} sent zero bytes", host);
|
||||
warn!("{} sent zero bytes", host);
|
||||
return Err(error::ErrorBadRequest("invalid file size"));
|
||||
}
|
||||
if paste_type != PasteType::Oneshot
|
||||
|
@ -290,7 +273,12 @@ async fn upload(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
paste.store_file(content.get_file_name()?, expiry_date, &config)?
|
||||
paste.store_file(
|
||||
content.get_file_name()?,
|
||||
expiry_date,
|
||||
header_filename,
|
||||
&config,
|
||||
)?
|
||||
}
|
||||
PasteType::RemoteFile => {
|
||||
paste
|
||||
|
@ -304,10 +292,12 @@ async fn upload(
|
|||
paste.store_url(expiry_date, &config)?
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
info!(
|
||||
"{} ({}) is uploaded from {}",
|
||||
file_name,
|
||||
Byte::from_bytes(paste.data.len() as u128).get_appropriate_unit(false),
|
||||
Byte::from_u128(paste.data.len() as u128)
|
||||
.unwrap_or_default()
|
||||
.get_appropriate_unit(UnitType::Decimal),
|
||||
host
|
||||
);
|
||||
let config = config
|
||||
|
@ -318,7 +308,7 @@ async fn upload(
|
|||
}
|
||||
urls.push(format!("{}/{}\n", server_url, file_name));
|
||||
} else {
|
||||
log::warn!("{} sent an invalid form field", host);
|
||||
warn!("{} sent an invalid form field", host);
|
||||
return Err(error::ErrorBadRequest("invalid form field"));
|
||||
}
|
||||
}
|
||||
|
@ -338,21 +328,15 @@ pub struct ListItem {
|
|||
|
||||
/// Returns the list of files.
|
||||
#[get("/list")]
|
||||
async fn list(
|
||||
request: HttpRequest,
|
||||
config: web::Data<RwLock<Config>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
.clone();
|
||||
let connection = request.connection_info().clone();
|
||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||
let tokens = config.get_tokens(TokenType::Auth);
|
||||
auth::check(host, request.headers(), tokens)?;
|
||||
if !config.server.expose_list.unwrap_or(false) {
|
||||
log::warn!("server is not configured to expose list endpoint");
|
||||
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
|
||||
warn!("server is not configured to expose list endpoint");
|
||||
Err(error::ErrorNotFound(""))?;
|
||||
}
|
||||
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
|
||||
.filter_map(|entry| {
|
||||
|
@ -365,7 +349,7 @@ async fn list(
|
|||
metadata
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to read metadata: {e}");
|
||||
error!("failed to read metadata: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
@ -398,13 +382,20 @@ async fn list(
|
|||
|
||||
/// Configures the server routes.
|
||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(index)
|
||||
.service(version)
|
||||
.service(list)
|
||||
.service(serve)
|
||||
.service(upload)
|
||||
.service(delete)
|
||||
.route("", web::head().to(HttpResponse::MethodNotAllowed));
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
.service(index)
|
||||
.service(version)
|
||||
.service(list)
|
||||
.service(serve)
|
||||
.service(upload)
|
||||
.service(delete)
|
||||
.route("", web::head().to(HttpResponse::MethodNotAllowed))
|
||||
.wrap(GrantsMiddleware::with_extractor(extract_tokens))
|
||||
.wrap(
|
||||
ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized_error),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -564,7 +555,7 @@ mod tests {
|
|||
#[actix_web::test]
|
||||
async fn test_version_without_auth() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.auth_tokens = Some(vec!["test".to_string()]);
|
||||
config.server.auth_tokens = Some(["test".to_string()].into());
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new(RwLock::new(config)))
|
||||
|
@ -598,8 +589,8 @@ mod tests {
|
|||
.uri("/version")
|
||||
.to_request();
|
||||
let response = test::call_service(&app, request).await;
|
||||
assert_eq!(StatusCode::FORBIDDEN, response.status());
|
||||
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
assert_body(response.into_body(), "").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -719,7 +710,7 @@ mod tests {
|
|||
#[actix_web::test]
|
||||
async fn test_auth() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.auth_tokens = Some(vec!["test".to_string()]);
|
||||
config.server.auth_tokens = Some(["test".to_string()].into());
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
|
@ -743,7 +734,7 @@ mod tests {
|
|||
App::new()
|
||||
.app_data(Data::new(RwLock::new(Config::default())))
|
||||
.app_data(Data::new(Client::default()))
|
||||
.wrap(ContentLengthLimiter::new(1))
|
||||
.wrap(ContentLengthLimiter::new(Byte::from_u64(1)))
|
||||
.configure(configure_routes),
|
||||
)
|
||||
.await;
|
||||
|
@ -762,7 +753,7 @@ mod tests {
|
|||
#[actix_web::test]
|
||||
async fn test_delete_file() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.delete_tokens = Some(vec!["test".to_string()]);
|
||||
config.server.delete_tokens = Some(["test".to_string()].into());
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
|
||||
let app = test::init_service(
|
||||
|
@ -816,8 +807,8 @@ mod tests {
|
|||
.to_request();
|
||||
let response = test::call_service(&app, request).await;
|
||||
|
||||
assert_eq!(StatusCode::FORBIDDEN, response.status());
|
||||
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
assert_body(response.into_body(), "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -866,6 +857,108 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_upload_file_override_filename() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new(RwLock::new(config)))
|
||||
.app_data(Data::new(Client::default()))
|
||||
.configure(configure_routes),
|
||||
)
|
||||
.await;
|
||||
|
||||
let file_name = "test_file.txt";
|
||||
let header_filename = "fn_from_header.txt";
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
get_multipart_request(×tamp, "file", file_name)
|
||||
.insert_header((
|
||||
header::HeaderName::from_static("filename"),
|
||||
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||
))
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(
|
||||
response.into_body(),
|
||||
&format!("http://localhost:8080/{header_filename}\n"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{header_filename}"))
|
||||
.to_request();
|
||||
let response = test::call_service(&app, serve_request).await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(response.into_body(), ×tamp).await?;
|
||||
|
||||
fs::remove_file(header_filename)?;
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{header_filename}"))
|
||||
.to_request();
|
||||
let response = test::call_service(&app, serve_request).await;
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_upload_same_filename() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new(RwLock::new(config)))
|
||||
.app_data(Data::new(Client::default()))
|
||||
.configure(configure_routes),
|
||||
)
|
||||
.await;
|
||||
|
||||
let file_name = "test_file.txt";
|
||||
let header_filename = "fn_from_header.txt";
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
get_multipart_request(×tamp, "file", file_name)
|
||||
.insert_header((
|
||||
header::HeaderName::from_static("filename"),
|
||||
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||
))
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(
|
||||
response.into_body(),
|
||||
&format!("http://localhost:8080/{header_filename}\n"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
get_multipart_request(×tamp, "file", file_name)
|
||||
.insert_header((
|
||||
header::HeaderName::from_static("filename"),
|
||||
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||
))
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::CONFLICT, response.status());
|
||||
assert_body(response.into_body(), "file already exists\n").await?;
|
||||
|
||||
fs::remove_file(header_filename)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[allow(deprecated)]
|
||||
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
||||
|
@ -975,7 +1068,7 @@ mod tests {
|
|||
async fn test_upload_remote_file() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
config.server.max_content_length = Byte::from_bytes(30000);
|
||||
config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default();
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
|
@ -1016,7 +1109,7 @@ mod tests {
|
|||
let body = response.into_body();
|
||||
let body_bytes = actix_web::body::to_bytes(body).await?;
|
||||
assert_eq!(
|
||||
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
|
||||
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
||||
util::sha256_digest(&*body_bytes)?
|
||||
);
|
||||
|
||||
|
@ -1044,7 +1137,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let url_upload_path = PasteType::Url.get_path(&config.server.upload_path);
|
||||
let url_upload_path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&url_upload_path)?;
|
||||
|
||||
let response = test::call_service(
|
||||
|
@ -1082,7 +1177,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let oneshot_upload_path = PasteType::Oneshot.get_path(&config.server.upload_path);
|
||||
let oneshot_upload_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&oneshot_upload_path)?;
|
||||
|
||||
let file_name = "oneshot.txt";
|
||||
|
@ -1142,7 +1239,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let url_upload_path = PasteType::OneshotUrl.get_path(&config.server.upload_path);
|
||||
let url_upload_path = PasteType::OneshotUrl
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&url_upload_path)?;
|
||||
|
||||
let response = test::call_service(
|
||||
|
|
56
src/util.rs
56
src/util.rs
|
@ -2,8 +2,11 @@ use crate::paste::PasteType;
|
|||
use actix_web::{error, Error as ActixError};
|
||||
use glob::glob;
|
||||
use lazy_regex::{lazy_regex, Lazy, Regex};
|
||||
use path_clean::PathClean;
|
||||
use ring::digest::{Context, SHA256};
|
||||
use std::fmt::Write;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
@ -35,7 +38,7 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
|||
);
|
||||
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.next()
|
||||
.last()
|
||||
{
|
||||
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
||||
if let Some(extension) = glob_path
|
||||
|
@ -62,7 +65,8 @@ pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
|
|||
PasteType::OneshotUrl,
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok())
|
||||
.filter_map(|v| v.get_path(base_path).ok())
|
||||
.filter_map(|v| glob(&v.join("*.[0-9]*").to_string_lossy()).ok())
|
||||
.flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>())
|
||||
.filter(|path| {
|
||||
if let Some(extension) = path
|
||||
|
@ -99,8 +103,32 @@ pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
|
|||
.iter()
|
||||
.collect::<Vec<&u8>>()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>())
|
||||
.try_fold::<String, _, IoResult<String>>(String::new(), |mut output, b| {
|
||||
write!(output, "{b:02x}")
|
||||
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
||||
Ok(output)
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Joins the paths whilst ensuring the path doesn't drastically change.
|
||||
/// `base` is assumed to be a trusted value.
|
||||
pub fn safe_path_join<B: AsRef<Path>, P: AsRef<Path>>(base: B, part: P) -> IoResult<PathBuf> {
|
||||
let new_path = base.as_ref().join(part).clean();
|
||||
|
||||
let cleaned_base = base.as_ref().clean();
|
||||
|
||||
if !new_path.starts_with(cleaned_base) {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} is outside of {}",
|
||||
new_path.display(),
|
||||
base.as_ref().display()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -165,4 +193,24 @@ mod tests {
|
|||
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(¤t_dir));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_safe_join_path() {
|
||||
assert_eq!(safe_path_join("/foo", "bar").ok(), Some("/foo/bar".into()));
|
||||
assert_eq!(safe_path_join("/", "bar").ok(), Some("/bar".into()));
|
||||
assert_eq!(safe_path_join("/", "././bar").ok(), Some("/bar".into()));
|
||||
assert_eq!(
|
||||
safe_path_join("/foo/bar", "baz/").ok(),
|
||||
Some("/foo/bar/baz/".into())
|
||||
);
|
||||
assert_eq!(
|
||||
safe_path_join("/foo/bar/../", "baz").ok(),
|
||||
Some("/foo/baz".into())
|
||||
);
|
||||
|
||||
assert!(safe_path_join("/foo", "/foobar").is_err());
|
||||
assert!(safe_path_join("/foo", "/bar").is_err());
|
||||
assert!(safe_path_join("/foo/bar", "..").is_err());
|
||||
assert!(safe_path_join("/foo/bar", "../").is_err());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue