Compare commits
330 Commits
Author | SHA1 | Date |
---|---|---|
Orhun Parmaksız | 81d25401ff | |
dependabot[bot] | 2bdd62bf1d | |
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 | |
Orhun Parmaksız | f46d2f5af5 | |
dependabot[bot] | cef84b482f | |
dependabot[bot] | a6c17f14e0 | |
Helmut K. C. Tessarek | cc9633cc95 | |
Andy Baird | 27e3be5a2d | |
Helmut K. C. Tessarek | 2292c24aaf | |
Orhun Parmaksız | 614b44884d | |
dependabot[bot] | 15cea9a157 | |
Orhun Parmaksız | a44ae94132 | |
Orhun Parmaksız | ec7a81ee6d | |
Ömer Üstün | 95379c9f66 | |
dependabot[bot] | 38d76fcb0f | |
dependabot[bot] | 31c6cbfcfb | |
Helmut K. C. Tessarek | d2d07ad345 | |
dependabot[bot] | 679a26475a | |
Orhun Parmaksız | 852e409b22 | |
Helmut K. C. Tessarek | c9a3ba4b86 | |
Helmut K. C. Tessarek | 9145c46e18 | |
dependabot[bot] | 0b8f63de19 | |
dependabot[bot] | 58e5558a10 | |
Orhun Parmaksız | 77f0e429d1 | |
Helmut K. C. Tessarek | 17ed34fc12 | |
dependabot[bot] | 0e9688e8a4 | |
Orhun Parmaksız | 61d4a9e3e6 | |
Orhun Parmaksız | f473966f9d | |
Orhun Parmaksız | 2734f371c9 | |
Orhun Parmaksız | 97ec45ed0b | |
Andy Baird | d0a67751dc | |
dependabot[bot] | ee8996193d | |
dependabot[bot] | cee95d9f85 | |
Orhun Parmaksız | 4daf41e9be | |
dependabot[bot] | 8a82513a7c | |
dependabot[bot] | c2c93a45ef | |
dependabot[bot] | 278a97cc2e | |
dependabot[bot] | 2580e5cb7d | |
dependabot[bot] | 8172f1f316 | |
Helmut K. C. Tessarek | 33e038cd4b | |
dependabot[bot] | 2e13d7a0a1 | |
Helmut K. C. Tessarek | 0d4808880f | |
dependabot[bot] | b1bdc45767 | |
Orhun Parmaksız | f09c467f6e | |
dependabot[bot] | 12afa9e468 | |
dependabot[bot] | c6863ab8bf | |
dependabot[bot] | 43cbb56599 | |
Helmut K. C. Tessarek | a868a5fdde | |
dependabot[bot] | 8a0445727d | |
dependabot[bot] | ced71c71f9 | |
dependabot[bot] | 4071792555 | |
dependabot[bot] | 49e8763257 | |
dependabot[bot] | e18372bb4b | |
Orhun Parmaksız | 39f9a62425 | |
Orhun Parmaksız | a402776739 | |
Orhun Parmaksız | 936feaedd5 | |
Orhun Parmaksız | 4ca95cb381 | |
Orhun Parmaksız | fe38ef8835 | |
Orhun Parmaksız | b23e9f64bf | |
Orhun Parmaksız | 074991810e | |
Helmut K. C. Tessarek | 62bbfef6a3 | |
dependabot[bot] | d740ae7e89 | |
Orhun Parmaksız | b549e3df7b | |
dependabot[bot] | 38fd3cf8df | |
Ömer Furkan Demircioğlu | d51b68da10 | |
Orhun Parmaksız | 40a87c586b | |
Orhun Parmaksız | 4df136870a | |
Helmut K. C. Tessarek | aa1734b3f8 | |
Orhun Parmaksız | 62c461702a | |
dependabot[bot] | 8f8e0caab0 | |
Orhun Parmaksız | ff9fd68e89 | |
Orhun Parmaksız | 5b01c98243 | |
Orhun Parmaksız | 1f041d3f74 | |
Helmut K. C. Tessarek | e0d6712dd3 | |
Helmut K. C. Tessarek | f7beaef502 | |
Helmut K. C. Tessarek | 917074158a | |
dependabot[bot] | 6d3d224307 | |
dependabot[bot] | b3fd28795d | |
Orhun Parmaksız | a1bc77430e | |
dependabot[bot] | 07ec392867 | |
dependabot[bot] | b4d1178ca4 | |
Orhun Parmaksız | c9bc01fe65 | |
dependabot[bot] | 39c85977bf | |
Orhun Parmaksız | 19cb7cccb7 | |
Orhun Parmaksız | 1670a71cdd | |
Orhun Parmaksız | a13c0f123a | |
Orhun Parmaksız | d4d8a28783 | |
Orhun Parmaksız | 4f539d2246 | |
Orhun Parmaksız | f165c29faf | |
Orhun Parmaksız | ebe2087845 | |
Chris Jones | 1a89589669 | |
Orhun Parmaksız | 1a9163639c | |
dependabot[bot] | ea01ba5794 | |
dependabot[bot] | cc16878b00 | |
Rahul Garg | bd88146430 | |
Chris Jones | 9d153ad907 | |
dependabot[bot] | 8e22d4b71a | |
Orhun Parmaksız | 6f2d27d589 | |
Orhun Parmaksız | 848e503870 | |
Orhun Parmaksız | 9738a750e3 | |
dependabot[bot] | 30ba212465 | |
Orhun Parmaksız | f20e2d8d12 | |
Orhun Parmaksız | 1966982198 | |
Orhun Parmaksız | 9ead53097e | |
Orhun Parmaksız | 8359aadec6 | |
Orhun Parmaksız | bfe78c067e | |
Orhun Parmaksız | f28fe68ba7 | |
alice | dca8041990 | |
Orhun Parmaksız | 5b45b35a44 | |
Orhun Parmaksız | c1b47e28c9 | |
Orhun Parmaksız | b092b0047d | |
Orhun Parmaksız | 00d46b4b21 | |
Orhun Parmaksız | e3a634b7af | |
Orhun Parmaksız | 9e61cca98e | |
Orhun Parmaksız | d57ed1125f | |
Orhun Parmaksız | a415ef60a0 | |
Orhun Parmaksız | 82f1a3207f | |
Orhun Parmaksız | 29ddef8df0 | |
Orhun Parmaksız | 019d1556da | |
Orhun Parmaksız | ab5e153a4e | |
Orhun Parmaksız | 4a4301ee72 | |
Orhun Parmaksız | 607f07b6e1 | |
Orhun Parmaksız | 9ea7d5de8a | |
Orhun Parmaksız | 4e45b6a20d | |
dependabot[bot] | 9fcbb1dfb0 | |
Orhun Parmaksız | 85e0c410d4 | |
dependabot[bot] | ad4c990b13 | |
Orhun Parmaksız | 18ba29e04a | |
Orhun Parmaksız | 5e8ea66256 | |
Orhun Parmaksız | 70085f3ad0 | |
Orhun Parmaksız | d82e99c128 | |
Orhun Parmaksız | dc499e402e | |
Orhun Parmaksız | ce51c184a5 | |
Orhun Parmaksız | a0ba66cac1 | |
Orhun Parmaksız | 29d5856566 | |
Orhun Parmaksız | 99ac6156a8 | |
Orhun Parmaksız | e459ba5f3c | |
Orhun Parmaksız | e013ac26bd | |
Orhun Parmaksız | c5389a1200 | |
Orhun Parmaksız | 240907c53a | |
Orhun Parmaksız | e82914f0f1 | |
Orhun Parmaksız | 695bb738cd | |
Marcin Puc | 96b919e5ee | |
Orhun Parmaksız | 8bac3cc91f | |
Orhun Parmaksız | e83b8e6f4a | |
Orhun Parmaksız | a954676547 | |
Leonidas Spyropoulos | a324305a7d | |
Orhun Parmaksız | df0d5391b4 | |
Orhun Parmaksız | 8394aacb49 | |
Leonidas Spyropoulos | 1050448ebd | |
Orhun Parmaksız | 20a1482aa7 | |
Leonidas Spyropoulos | 5cdbc8da61 | |
Orhun Parmaksız | 00cee6d9ba | |
Orhun Parmaksız | 6ffc5d6209 | |
TheTechRobo | 37cb4d3fcb | |
Orhun Parmaksız | 1e1da30b83 | |
Orhun Parmaksız | a7dcc39f91 | |
PLANTROON | a0f076269e | |
Orhun Parmaksız | 59f9b46bb8 | |
Orhun Parmaksız | 16628e402a | |
Orhun Parmaksız | deec5d4c29 | |
Orhun Parmaksız | a98f0ae22e | |
Orhun Parmaksız | 5cdca1d9aa | |
Orhun Parmaksız | c06fff92ac | |
Orhun Parmaksız | d262a3d297 | |
Orhun Parmaksız | a6a8261a73 | |
Orhun Parmaksız | 00167db3d6 | |
Orhun Parmaksız | cccbf98acc | |
Orhun Parmaksız | c60090d461 | |
Orhun Parmaksız | 685d9607c8 | |
Orhun Parmaksız | 4d16e65edb | |
Orhun Parmaksız | 3467038353 | |
Orhun Parmaksız | 63c79b8297 | |
Orhun Parmaksız | 48063d736a | |
Orhun Parmaksız | 87d2423d1a | |
Orhun Parmaksız | 013c6ac810 | |
Orhun Parmaksız | 1f8f462299 | |
Orhun Parmaksız | a330b59dca | |
Orhun Parmaksız | 657ca8c1d4 | |
Orhun Parmaksız | 59415cd31a | |
Orhun Parmaksız | 1d44cbd9c8 | |
Orhun Parmaksız | 74237d56c5 | |
Orhun Parmaksız | 527fbc4f49 | |
frederik | 8679ff91dc | |
Orhun Parmaksız | 8ed0b7bbf1 | |
Orhun Parmaksız | 9136d1ce09 | |
Orhun Parmaksız | dd91c50d50 | |
Orhun Parmaksız | a3e266b8b4 | |
Orhun Parmaksız | c48e45d68c | |
Orhun Parmaksız | 51d6fdd0d8 | |
Orhun Parmaksız | 2d5c14a6e0 | |
sh | 44ab1318ee | |
Sven-Hendrik Haase | d3a9e0c69d | |
Orhun Parmaksız | 525129ddcb | |
Orhun Parmaksız | ffe067f9da | |
Orhun Parmaksız | 08dc063a7a | |
Orhun Parmaksız | 1dd5dcf167 | |
Orhun Parmaksız | e3a97045f4 | |
Orhun Parmaksız | bdb8492a11 | |
Orhun Parmaksız | 94516c95bb | |
Orhun Parmaksız | fda6f91211 | |
Orhun Parmaksız | 9e5bd112e7 | |
Orhun Parmaksız | 0f893ba058 | |
Orhun Parmaksız | d4a12cdcee | |
Orhun Parmaksız | 2ef453be28 | |
Orhun Parmaksız | d6ba6ac630 | |
Orhun Parmaksız | 6f1dcab15a | |
Orhun Parmaksız | 3cc40deca0 | |
Orhun Parmaksız | 53686c6c93 | |
Orhun Parmaksız | 4f6456a3c8 | |
Orhun Parmaksız | 17b8c1940c | |
Orhun Parmaksız | ebbe8dbfae | |
Orhun Parmaksız | 2b80c78a02 | |
Orhun Parmaksız | 8f3f89716f | |
Orhun Parmaksız | 5cbb41c247 | |
Sven Assmann | 8a03fe3655 | |
Orhun Parmaksız | dbda1bb94a | |
Orhun Parmaksız | 8b17137c52 |
|
@ -1,8 +1,9 @@
|
|||
# Directories
|
||||
/.git/
|
||||
/.github/
|
||||
/target/
|
||||
/upload/
|
||||
/shuttle/
|
||||
/examples/
|
||||
|
||||
# Files
|
||||
.gitignore
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# configuration for https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.sh]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -1 +1,3 @@
|
|||
github: orhun
|
||||
patreon: orhunp
|
||||
buy_me_a_coffee: orhun
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<!--- Thank you for contributing to rustypaste! -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Motivation and Context
|
||||
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
<!--- see how your change affects other areas of the code, etc. -->
|
||||
|
||||
## Changelog Entry
|
||||
|
||||
<!--- Please write the changelog entry for these changes. -->
|
||||
<!--- Follow the <https://keepachangelog.com/en/1.0.0/> format. -->
|
||||
<!--- Use one of the Added, Changed, Deprecated, Removed, Fixed, and Security headers accordingly. -->
|
||||
<!-- For example:
|
||||
````
|
||||
### Added
|
||||
|
||||
- Add a middleware for checking the content length
|
||||
- Before, the upload size was checked after full upload which was clearly wrong.
|
||||
````
|
||||
-->
|
||||
|
||||
## Types of Changes
|
||||
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Documentation (no code change)
|
||||
- [ ] Refactor (refactoring production code)
|
||||
- [ ] Other <!--- (provide information) -->
|
||||
|
||||
## Checklist:
|
||||
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
- [ ] I have formatted the code with [rustfmt](https://github.com/rust-lang/rustfmt).
|
||||
- [ ] I checked the lints with [clippy](https://github.com/rust-lang/rust-clippy).
|
||||
- [ ] I have added tests to cover my changes.
|
||||
- [ ] All new and existing tests passed.
|
|
@ -0,0 +1,15 @@
|
|||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for Cargo
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
|
@ -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-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
uses: actions/checkout@v4
|
||||
- name: Run cargo-audit
|
||||
uses: actions-rs/audit-check@v1
|
||||
uses: rustsec/audit-check@v1.4.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -8,13 +8,13 @@ on:
|
|||
jobs:
|
||||
publish-github:
|
||||
name: Publish on GitHub
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
TARGET: [x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v4
|
||||
- name: Set the release version
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
|
||||
- name: Install musl-tools
|
||||
|
@ -24,13 +24,13 @@ jobs:
|
|||
sudo apt-get install -y --no-install-recommends \
|
||||
--allow-unauthenticated musl-tools
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.TARGET }}
|
||||
override: true
|
||||
- name: Build
|
||||
run: cargo build --release --locked --target ${{ matrix.TARGET }}
|
||||
run: |
|
||||
cargo build --release --locked --target ${{ matrix.TARGET }}
|
||||
strip target/${{ matrix.TARGET }}/release/rustypaste
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
mkdir release/
|
||||
|
@ -55,12 +55,11 @@ jobs:
|
|||
publish-crates-io:
|
||||
name: Publish on crates.io
|
||||
needs: publish-github
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Publish
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --locked --token ${{ secrets.CARGO_TOKEN }}
|
||||
run: cargo publish --locked --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
|
|
@ -13,95 +13,100 @@ on:
|
|||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Check the project files
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --locked --verbose
|
||||
run: cargo check --locked --verbose
|
||||
|
||||
test:
|
||||
name: Test suite
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
- name: Setup cargo-tarpaulin
|
||||
run: |
|
||||
curl -s https://api.github.com/repos/xd009642/tarpaulin/releases/latest | \
|
||||
grep "browser_download_url.*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
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- 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@v1
|
||||
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:
|
||||
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
|
||||
run: cargo build --locked --verbose
|
||||
- name: Run test fixtures
|
||||
shell: bash
|
||||
run: ./test-fixtures.sh
|
||||
working-directory: fixtures
|
||||
env:
|
||||
DEBUG: true
|
||||
|
||||
clippy:
|
||||
name: Lints
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
- name: Check the lints
|
||||
uses: actions-rs/cargo@v1
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
command: clippy
|
||||
args: --verbose -- -D warnings
|
||||
components: clippy
|
||||
- name: Check the lints
|
||||
run: cargo clippy --tests --verbose -- -D warnings
|
||||
|
||||
rustfmt:
|
||||
name: Formatting
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
override: true
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
- name: Check the formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
command: fmt
|
||||
args: -- --check --verbose
|
||||
components: rustfmt
|
||||
- name: Check the formatting
|
||||
run: cargo fmt -- --check --verbose
|
||||
|
||||
lychee:
|
||||
name: Links
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v4
|
||||
- name: Check the links
|
||||
uses: lycheeverse/lychee-action@v1
|
||||
with:
|
||||
|
|
|
@ -15,14 +15,14 @@ on:
|
|||
jobs:
|
||||
docker:
|
||||
name: Docker Build and Push
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
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@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
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@v1
|
||||
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@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
@ -64,16 +69,24 @@ jobs:
|
|||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
sbom: true
|
||||
provenance: true
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Scan the image
|
||||
uses: anchore/sbom-action@v0
|
||||
with:
|
||||
image: ghcr.io/${{ github.repository_owner }}/rustypaste/rustypaste
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
name: Shuttle.rs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build / Deploy
|
||||
runs-on: ubuntu-22.04
|
||||
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: Install cargo-binstall
|
||||
uses: taiki-e/install-action@cargo-binstall
|
||||
- name: Install cargo-shuttle
|
||||
run: cargo binstall -y cargo-shuttle
|
||||
- name: Prepare for deployment
|
||||
shell: bash
|
||||
run: sed -i 's|default = \["rustls"\]|default = \["rustls", "shuttle"\]|g' Cargo.toml
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
- name: Deploy
|
||||
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
|
||||
cargo shuttle deploy --allow-dirty --no-test
|
|
@ -5,4 +5,4 @@
|
|||
**/*.rs.bk
|
||||
|
||||
# Default upload directory
|
||||
/upload/
|
||||
**/upload/
|
||||
|
|
649
CHANGELOG.md
649
CHANGELOG.md
|
@ -1,21 +1,651 @@
|
|||
# Changelog
|
||||
|
||||
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.6.0] - 2021-11-07
|
||||
## [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
|
||||
|
||||
- Add delete endpoint (#136)
|
||||
|
||||
Now you can delete files from the server with sending a [`DELETE`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) request. To enable this, set the `delete_tokens` array in the configuration file or use the `DELETE_TOKEN` environment variable.
|
||||
|
||||
```toml
|
||||
[server]
|
||||
delete_tokens = [ "may_the_force_be_with_you" ]
|
||||
```
|
||||
|
||||
And then you can send a `DELETE` request as follows:
|
||||
|
||||
```sh
|
||||
$ curl -H "Authorization: may_the_force_be_with_you" -X DELETE "<server_address>/file.txt"
|
||||
|
||||
file deleted
|
||||
```
|
||||
|
||||
You can also use [`rpaste`](https://github.com/orhun/rustypaste-cli#delete-files-from-server) (the command line tool) to delete files:
|
||||
|
||||
```sh
|
||||
$ rpaste -d awesome.UA86.txt
|
||||
```
|
||||
|
||||
### Changed
|
||||
|
||||
- Update crates and rustls deps (#135)
|
||||
- Bump Shuttle to `0.25.0`
|
||||
|
||||
## [0.13.0] - 2023-08-26
|
||||
|
||||
### Added
|
||||
|
||||
- Support handling spaces in filenames (#107)
|
||||
|
||||
Now you can replace the whitespaces with either underscore or encoded space (`%20`) character in the filenames.
|
||||
|
||||
For example:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
handle_spaces = "replace"
|
||||
```
|
||||
|
||||
```sh
|
||||
$ curl -F "file=@test file.txt" <server_address>
|
||||
|
||||
<server_address>/test_file.txt
|
||||
```
|
||||
|
||||
Or you can use encoded spaces:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
handle_spaces = "encode"
|
||||
```
|
||||
|
||||
```sh
|
||||
$ curl -F "file=@test file.txt" <server_address>
|
||||
|
||||
<server_address>/test%20file.txt
|
||||
```
|
||||
|
||||
Please note that `random_url` should not be configured to use the original file names.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve random_url config handling (#122)
|
||||
|
||||
`[paste].random_url.enabled` is deprecated. You can now disable random URLs by commenting out `[paste].random_url`.
|
||||
|
||||
```toml
|
||||
# enabled
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
|
||||
# disabled
|
||||
# random_url = { type = "petname", words = 2, separator = "-" }
|
||||
```
|
||||
|
||||
- Replace unmaintained actions (#116)
|
||||
- Bump Shuttle to `0.24.0`
|
||||
- Bump dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't log invalid token in release builds (#112)
|
||||
|
||||
Before, invalid tokens were logged as follows:
|
||||
|
||||
```
|
||||
[2023-08-13T19:24:30Z WARN rustypaste::auth] authorization failure for a.b.c.d (header: invalid_token)
|
||||
```
|
||||
|
||||
Now, we print the token only in debug mode. In release mode, the log entry will look like this:
|
||||
|
||||
```
|
||||
[2023-08-13T19:24:30Z WARN rustypaste::auth] authorization failure for a.b.c.d
|
||||
```
|
||||
|
||||
## [0.12.1] - 2023-08-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Do not list expired files (#109)
|
||||
|
||||
## [0.12.0] - 2023-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- Add an endpoint for retrieving a list of files (#94)
|
||||
|
||||
Set the `expose_list` option to `true` in the configuration file for enabling this feature. It is disabled as default.
|
||||
|
||||
```toml
|
||||
[server]
|
||||
expose_list = true
|
||||
```
|
||||
|
||||
Then you can receive the list of files as JSON via `/list` endpoint:
|
||||
|
||||
```sh
|
||||
$ curl "http://<server_address>/list" | jq .
|
||||
|
||||
[
|
||||
{
|
||||
"file_name": "accepted-cicada.txt",
|
||||
"file_size": 241,
|
||||
"expires_at_utc": null
|
||||
},
|
||||
{
|
||||
"file_name": "evolving-ferret.txt",
|
||||
"file_size": 111,
|
||||
"expires_at_utc": "2023-08-07 10:51:14"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- Support multiple auth tokens (#84)
|
||||
|
||||
`auth_token` option is now deprecated and replaced with `auth_tokens` which supports an array of authentication tokens. For example:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
auth_tokens = [
|
||||
"super_secret_token1",
|
||||
"super_secret_token2",
|
||||
]
|
||||
```
|
||||
|
||||
- Add new line character to most prominent messages (#97)
|
||||
|
||||
This is a follow-up to #72 for making the terminal output better:
|
||||
|
||||
```sh
|
||||
$ curl http://localhost:8000/sweeping-tahr
|
||||
unauthorized
|
||||
```
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Shuttle to `0.23.0`
|
||||
- Bump dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Deploy the Shuttle service when a new tag is created
|
||||
|
||||
## [0.11.1] - 2023-07-01
|
||||
|
||||
This is a hotfix release for supporting the use of deprecated `[server].landing_page*` fields.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow using deprecated landing page fields
|
||||
|
||||
## [0.11.0] - 2023-07-01
|
||||
|
||||
### Added
|
||||
|
||||
- Add a new section for the landing page
|
||||
- Also, support a file for the landing page (#64)
|
||||
|
||||
Migration path:
|
||||
|
||||
Old:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
landing_page = "Landing page text."
|
||||
landing_page_file = "index.html"
|
||||
landing_page_content_type = "text/html; charset=utf-8"
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```toml
|
||||
[landing_page]
|
||||
text = "Landing page text."
|
||||
file = "index.html"
|
||||
content_type = "text/html; charset=utf-8"
|
||||
```
|
||||
|
||||
The configuration is backwards compatible but we recommend using the new `landing_page` section as shown above since the other fields are now deprecated.
|
||||
|
||||
- Add random suffix mode (#69)
|
||||
- Support appending a random suffix to the filename before the extension. For example, `foo.tar.gz` will result in `foo.eu7f92x1.tar.gz`
|
||||
|
||||
To enable, set `suffix_mode` to `true`:
|
||||
|
||||
```toml
|
||||
[paste]
|
||||
random_url = { enabled = true, type = "alphanumeric", length = 6, suffix_mode = true }
|
||||
```
|
||||
|
||||
- Honor X-Forward-\* headers (`X-Forwarded-For` / `X-Forwarded-Host` / `X-Forwarded-Proto`) (#61)
|
||||
|
||||
- This would be really useful to have for setups where the service is running behind a reverse-proxy or gateway and the possibility to adjust the logging output based on their availability, to have the real IP addresses of the clients available in the log.
|
||||
|
||||
- Add new line character to the 404 message (#72)
|
||||
|
||||
Terminal output will look better when the file is not found:
|
||||
|
||||
```sh
|
||||
$ curl http://localhost:8000/sweeping-tahr
|
||||
file is not found or expired :(
|
||||
```
|
||||
|
||||
- Add editorconfig for correctly formatting the test fixture files
|
||||
- Add pull request template
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Shuttle to `0.20.0`
|
||||
- List all the supported units in the documentation (#63)
|
||||
- Note that the Alpine Linux package is moved to the community
|
||||
|
||||
- <https://pkgs.alpinelinux.org/packages?name=rustypaste>
|
||||
|
||||
- Bump dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use the static folder for the Shuttle config (#70)
|
||||
- There was a regression in the previous release that has caused the static folder to be not present in Shuttle deployments. This shouldn't be an issue anymore and the deployment should be live.
|
||||
- Also, it is now possible to trigger a deployment manually via GitHub Actions.
|
||||
|
||||
Thanks to [@tessus](https://github.com/tessus) for his contributions to this release!
|
||||
|
||||
## [0.10.1] - 2023-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- Add a middleware for checking the content length
|
||||
- Before, the upload size was checked after full upload which was clearly wrong.
|
||||
- With this change, total amount of bytes to upload is checked via [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) header before the upload.
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Shuttle to `0.18.0`
|
||||
- Bump hotwatch to 0.5.0
|
||||
- Fixes [`RUSTSEC-2020-0016`](https://rustsec.org/advisories/RUSTSEC-2020-0016.html)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Do not drop the config watcher
|
||||
- Since `0.9.0`, the configuration watcher was dropped early which caused for it to not work and resulted in mysterious spikes in CPU usage.
|
||||
- With this version, this issue is fixed.
|
||||
|
||||
## [0.10.0] - 2023-05-31
|
||||
|
||||
### Added
|
||||
|
||||
- Support one shot URLs
|
||||
|
||||
With using the `oneshot_url` multipart field, you can now shorten an URL and make it disappear after viewed once:
|
||||
|
||||
```sh
|
||||
curl -F "oneshot_url=https://example.com" "<server_address>"
|
||||
```
|
||||
|
||||
- Allow configuring the content type for the landing page
|
||||
|
||||
`landing_page_content_type` is added as a configuration option for setting the [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
landing_page = ""
|
||||
landing_page_content_type = "text/plain; charset=utf-8"
|
||||
```
|
||||
|
||||
- Add information/example about using HTML forms
|
||||
|
||||
With utilizing the newly added option for the content type, you can now use HTML forms for the landing page:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
landing_page = "<html>"
|
||||
landing_page_content_type = "text/html; charset=utf-8"
|
||||
```
|
||||
|
||||
There is an example added to the repository: [html_form.toml](https://github.com/orhun/rustypaste/blob/1a8958966972f2afb04a12cb2f5537a1d971561c/examples/html_form.toml)
|
||||
|
||||
Also, there is an ongoing discussion about refactoring the usage of landing page fields in the configuration file. See [#52](https://github.com/orhun/rustypaste/issues/52)
|
||||
|
||||
- An informative log message is added for showing the server address at startup
|
||||
|
||||
## [0.9.1] - 2023-05-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump Shuttle to `0.17.0`
|
||||
- Tweak public instance settings
|
||||
- Increase the default expiry time to 24 hours
|
||||
- Increase the max content length to 20MB
|
||||
- Bump dependencies
|
||||
|
||||
## [0.9.0] - 2023-05-17
|
||||
|
||||
The public instance is now available at [https://rustypaste.shuttleapp.rs](https://rustypaste.shuttleapp.rs) 🚀
|
||||
|
||||
Read the blog post about `rustypaste` and Shuttle deployments: [https://blog.orhun.dev/blazingly-fast-file-sharing](https://blog.orhun.dev/blazingly-fast-file-sharing)
|
||||
|
||||
### Added
|
||||
|
||||
- Deploy on Shuttle.rs
|
||||
- Support setting a default expiry time
|
||||
|
||||
You can now specify a expiry time for uploaded files. For example, if you want all the files to expire after one hour:
|
||||
|
||||
```toml
|
||||
[paste]
|
||||
default_expiry = "1h"
|
||||
```
|
||||
|
||||
- Support overriding the server URL
|
||||
|
||||
If you are using `rustypaste` with a redirect or reverse proxy, it is now possible to set a different URL for the returned results:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
url = "https://rustypaste.shuttleapp.rs"
|
||||
```
|
||||
|
||||
- Add instructions for installing on Alpine Linux
|
||||
|
||||
`rustypaste` is now available in [testing](https://pkgs.alpinelinux.org/packages?name=rustypaste&branch=edge) repositories.
|
||||
|
||||
- Add new crate features
|
||||
|
||||
- `shuttle`: enable an entry point for deploying on Shuttle
|
||||
- `openssl`: use distro OpenSSL (binary size is reduced ~20% in release mode)
|
||||
- `rustls`: use [rustls](https://github.com/rustls/rustls) (enabled as default)
|
||||
|
||||
### Changed
|
||||
|
||||
- Make the default landing page fancier
|
||||
- Generate SBOM attestation for the Docker image
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump dependencies
|
||||
- Update the funding options
|
||||
- Consider donating if you liked `rustypaste`: [https://donate.orhun.dev](https://donate.orhun.dev) 💖
|
||||
|
||||
## [0.8.4] - 2023-01-31
|
||||
|
||||
### Added
|
||||
|
||||
- Allow downloading files via `?download=true` parameter
|
||||
- If you specify this for a file (e.g. `<server_address>/file?download=true`), `rustypaste` will override the MIME type to `application/octet-stream` and this will force your browser to download the file.
|
||||
- This is useful when e.g. you want to be able to share the link to a file that would play in the browser (like `.mp4`) but also share a link that will auto-download as well.
|
||||
|
||||
## [0.8.3] - 2023-01-30
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump dependencies
|
||||
- Switch to [Rust](https://hub.docker.com/_/rust) image for the Dockerfile
|
||||
- Remove unused `clap` dependency
|
||||
|
||||
## [0.8.2] - 2022-10-04
|
||||
|
||||
### Updated
|
||||
|
||||
- Don't expose version endpoint in default config
|
||||
- Set `expose_version` to `false` in the configuration file
|
||||
|
||||
## [0.8.1] - 2022-10-04
|
||||
|
||||
### Added
|
||||
|
||||
- Add `<server_address>/version` endpoint for retrieving the server version
|
||||
|
||||
```toml
|
||||
[server]
|
||||
expose_version=true
|
||||
```
|
||||
|
||||
If `expose_version` entry is not present in the configuration file, `/version` is not exposed. It is recommended to use this feature with authorization enabled.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Replace unmaintained `dotenv` crate with `dotenvy`
|
||||
- Fixes [RUSTSEC-2021-0141](https://rustsec.org/advisories/RUSTSEC-2021-0141.html)
|
||||
|
||||
## [0.8.0] - 2022-10-03
|
||||
|
||||
### Added
|
||||
|
||||
- Support adding a landing page
|
||||
|
||||
You can now specify a landing page text in the configuration file as follows:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
landing_page = """
|
||||
boo 👻
|
||||
======
|
||||
welcome!
|
||||
"""
|
||||
```
|
||||
|
||||
If the landing page entry is not present in the configuration file, visiting the index page will redirect to the repository.
|
||||
|
||||
### Updated
|
||||
|
||||
- Do not check for duplicate files by default
|
||||
- Set `duplicate_files` to `true` in the configuration file
|
||||
- It is an expensive operation to do on slower hardware and can take an unreasonable amount of time for bigger files
|
||||
- Enable [GitHub Sponsors](https://github.com/sponsors/orhun) for funding
|
||||
- Consider supporting me for my open-source work 💖
|
||||
|
||||
## [0.7.1] - 2022-05-21
|
||||
|
||||
### Added
|
||||
|
||||
- Aggressively test everything
|
||||
- Add the missing unit tests for the server endpoints (code coverage is increased to 84%)
|
||||
- Create a custom testing framework (written in Bash) for adding [test fixtures](https://github.com/orhun/rustypaste/tree/master/fixtures)
|
||||
|
||||
## [0.7.0] - 2022-03-26
|
||||
|
||||
### Added
|
||||
|
||||
- Support auto-deletion of expired files
|
||||
|
||||
`rustypaste` can now delete the expired files by itself. To enable this feature, add the following line to the `[paste]` section in the configuration file:
|
||||
|
||||
```toml
|
||||
# expired files will be cleaned up hourly
|
||||
delete_expired_files = { enabled = true, interval = "1h" }
|
||||
```
|
||||
|
||||
For users who want to have this feature disabled, there is an alternative [shell script](README.md#cleaning-up-expired-files) recommended in the documentation.
|
||||
|
||||
- Add systemd service files
|
||||
- [systemd files](./extra/systemd/) have been added to serve files from `/var/lib/rustypaste`, create `rustypaste` user automatically via `systemd-sysusers` and configure `AUTH_TOKEN` via `rustypaste.env`.
|
||||
- For the installation and usage, see the Arch Linux [PKGBUILD](https://github.com/archlinux/svntogit-community/blob/packages/rustypaste/trunk/PKGBUILD).
|
||||
|
||||
### Updated
|
||||
|
||||
- Upgrade Actix dependencies
|
||||
- `actix-web` is updated to [`4.0.*`](https://github.com/actix/actix-web/blob/master/actix-web/CHANGES.md#401---2022-02-25)
|
||||
- Strip the binaries during automated builds
|
||||
- Size of the Docker image is reduced by ~20%
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent invalid attempts of serving directories
|
||||
- This fixes an issue where requesting a directory was possible via e.g. `curl --path-as-is 0.0.0.0:8080/.`
|
||||
- This issue had no security impact (path traversal wasn't possible) since internal server error was returned.
|
||||
|
||||
## [0.6.5] - 2022-03-13
|
||||
|
||||
### Added
|
||||
|
||||
- Add instructions for installing [rustypaste](https://archlinux.org/packages/community/x86_64/rustypaste/) on Arch Linux
|
||||
- `pacman -S rustypaste` 🎉
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix a bug where the use of `CONFIG` environment variable causes a conflict between the configuration file path and `[config]` section
|
||||
|
||||
## [0.6.4] - 2022-03-11
|
||||
|
||||
### Added
|
||||
|
||||
- Support setting the refresh rate for hot-reloading the configuration file.
|
||||
|
||||
```toml
|
||||
[config]
|
||||
refresh_rate="1s"
|
||||
```
|
||||
|
||||
- Support setting the timeout for HTTP requests.
|
||||
|
||||
```toml
|
||||
[server]
|
||||
timeout="30s"
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
- Bump [regex crate](https://github.com/rust-lang/regex) to **1.5.5**
|
||||
- Fixes [CVE-2022-24713](https://github.com/advisories/GHSA-m5pq-gvj9-9vr8)
|
||||
|
||||
## [0.6.3] - 2022-02-24
|
||||
|
||||
### Added
|
||||
|
||||
- Support setting the authentication token in the configuration file.
|
||||
- This is an alternative (but not recommended) way of setting up authentication when the use of `AUTH_TOKEN` environment variable is not applicable.
|
||||
|
||||
```toml
|
||||
[server]
|
||||
auth_token="hunter2"
|
||||
```
|
||||
|
||||
## [0.6.2] - 2021-12-05
|
||||
|
||||
### Updated
|
||||
|
||||
- Improve the concurrency
|
||||
- Shrink the scope of non-suspendable types (`#[must_not_suspend]`) for dropping them before reaching a suspend point (`.await` call). This avoids possible deadlocks, delays, and situations where `Future`s not implementing `Send`.
|
||||
- Reference: https://rust-lang.github.io/rfcs/3014-must-not-suspend-lint.html
|
||||
|
||||
## [0.6.1] - 2021-11-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Gracefully handle the hot-reloading errors.
|
||||
- Errors that may occur while locking the [Mutex](https://doc.rust-lang.org/std/sync/struct.Mutex.html) are handled properly hence a single configuration change cannot take down the whole service due to [poisoning](https://doc.rust-lang.org/std/sync/struct.Mutex.html#poisoning).
|
||||
|
||||
## [0.6.0] - 2021-11-07
|
||||
|
||||
### Added
|
||||
|
||||
- Support pasting files from remote URLs (via `remote=` form field)
|
||||
|
||||
- `{server.max_content_length}` is used for download limit
|
||||
- See [README.md#paste-file-from-remote-url](https://github.com/orhun/rustypaste#paste-file-from-remote-url)
|
||||
|
||||
- Hot reload configuration file to apply configuration changes instantly without restarting the server
|
||||
|
||||
### Changed
|
||||
|
||||
- Library: Switch to Rust 2021 edition
|
||||
|
||||
### Security
|
||||
|
||||
- Prevent serving an already expired file
|
||||
|
||||
In the previous versions, it was possible to view an expired file by using the correct extension (timestamp). e.g. `paste.com/expired_file.txt.1630094518049` will serve the file normally although `paste.com/expired_file.txt` says that it is expired. This version fixes this vulnerability by regex-checking the requested file's extension.
|
||||
|
@ -23,7 +653,9 @@ In the previous versions, it was possible to view an expired file by using the c
|
|||
reference: [f078a9afa74f8608ee3f2a6e705159df15915c78](https://github.com/orhun/rustypaste/commit/f078a9afa74f8608ee3f2a6e705159df15915c78)
|
||||
|
||||
## [0.5.0] - 2021-10-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added an entry in the configuration file to disable "duplicate uploads":
|
||||
|
||||
```toml
|
||||
|
@ -35,13 +667,17 @@ duplicate_files = false
|
|||
Under the hood, it checks the SHA256 digest of the uploaded files.
|
||||
|
||||
## [0.4.1] - 2021-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Update README.md:
|
||||
- Mention the new standalone tool: [rustypaste-cli](https://github.com/orhun/rustypaste-cli)
|
||||
- Add [installation](https://github.com/orhun/rustypaste#installation) section.
|
||||
|
||||
## [0.4.0] - 2021-08-27
|
||||
|
||||
### Added
|
||||
|
||||
- Support [expiring links](README.md#expiration) (via `expire:` header)
|
||||
- Timestamps are used as extension for expiring files
|
||||
- Expired files can be cleaned up with [this command](README.md#cleaning-up-expired-files)
|
||||
|
@ -49,28 +685,39 @@ Under the hood, it checks the SHA256 digest of the uploaded files.
|
|||
- `{server.upload_path}/oneshot` is used for storage
|
||||
|
||||
## [0.3.1] - 2021-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Switch to [upload-release-action](https://github.com/svenstaro/upload-release-action) for uploading releases
|
||||
|
||||
## [0.3.0] - 2021-08-09
|
||||
|
||||
### Added
|
||||
|
||||
- Support overriding MIME types (config: `mime_override`)
|
||||
- Support blacklisting MIME types (config: `mime_blacklist`)
|
||||
|
||||
## [0.2.0] - 2021-08-04
|
||||
|
||||
### Added
|
||||
|
||||
- Support shortening URLs (via `url=` form field)
|
||||
- `{server.upload_path}/url` is used for storage
|
||||
|
||||
## [0.1.3] - 2021-07-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent sending empty file name and zero bytes
|
||||
- Prevent path traversal on upload directory ([#2](https://github.com/orhun/rustypaste/issues/2))
|
||||
- Check the content length while reading bytes for preventing OOM ([#1](https://github.com/orhun/rustypaste/issues/1))
|
||||
|
||||
## [0.1.2] - 2021-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Continuous Deployment workflow to publish Docker images
|
||||
|
||||
## [0.1.1] - 2021-07-27
|
||||
|
||||
Initial release.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
63
Cargo.toml
63
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypaste"
|
||||
version = "0.6.0"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
description = "A minimal file upload/pastebin service"
|
||||
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
|
||||
|
@ -12,47 +12,63 @@ keywords = ["paste", "pastebin", "upload"]
|
|||
categories = ["web-programming::http-server"]
|
||||
include = ["src/**/*", "Cargo.*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
|
||||
[features]
|
||||
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:tokio"]
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.3.2", features = ["rustls"] }
|
||||
actix-multipart = "0.3.0"
|
||||
actix-files = "0.5.0"
|
||||
env_logger = "0.9.0"
|
||||
log = "0.4.14"
|
||||
serde = "1.0.130"
|
||||
futures-util = "0.3.17"
|
||||
petname = "1.1.2"
|
||||
rand = "0.8.4"
|
||||
dotenv = "0.15.0"
|
||||
url = "2.2.2"
|
||||
mime = "0.3.16"
|
||||
regex = "1.5.4"
|
||||
actix-web = { version = "4.5.1" }
|
||||
actix-web-grants = { version = "4.0.3" }
|
||||
actix-multipart = "0.6.1"
|
||||
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.203"
|
||||
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.5.0"
|
||||
mime = "0.3.17"
|
||||
regex = "1.10.4"
|
||||
serde_regex = "1.1.0"
|
||||
lazy-regex = "2.2.1"
|
||||
lazy-regex = "3.1.0"
|
||||
humantime = "2.1.0"
|
||||
glob = "0.3.0"
|
||||
ring = "0.16.20"
|
||||
hotwatch = "0.4.5"
|
||||
humantime-serde = "1.1.1"
|
||||
glob = "0.3.1"
|
||||
ring = "0.17.8"
|
||||
hotwatch = "0.5.0"
|
||||
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.11.0"
|
||||
version = "0.14.0"
|
||||
default-features = false
|
||||
features = ["toml", "yaml"]
|
||||
|
||||
[dependencies.byte-unit]
|
||||
version = "4.0.13"
|
||||
version = "5.1.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.infer]
|
||||
version = "0.5.0"
|
||||
version = "0.15.0"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.1.1"
|
||||
actix-rt = "2.9.0"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
debug = true
|
||||
panic = "abort"
|
||||
|
||||
[profile.test]
|
||||
opt-level = 0
|
||||
|
@ -64,6 +80,7 @@ debug = false
|
|||
panic = "unwind"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[profile.bench]
|
||||
opt-level = 3
|
||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -1,24 +1,20 @@
|
|||
FROM ekidd/rust-musl-builder:latest as builder
|
||||
WORKDIR /home/rust/src
|
||||
FROM rust:1.72.0-alpine3.18 as builder
|
||||
WORKDIR /app
|
||||
RUN apk update
|
||||
RUN apk add --no-cache musl-dev
|
||||
COPY Cargo.toml Cargo.toml
|
||||
# https://github.com/emk/rust-musl-builder/issues/130
|
||||
RUN sed -i "s|edition = \"2021\"|edition = \"2018\"|" Cargo.toml
|
||||
RUN mkdir -p src/
|
||||
RUN echo "fn main() {println!(\"failed to build\")}" > src/main.rs
|
||||
RUN cargo build --release
|
||||
RUN rm -f target/release/deps/rustypaste*
|
||||
COPY . .
|
||||
# https://github.com/emk/rust-musl-builder/issues/130
|
||||
RUN sed -i "s|edition = \"2021\"|edition = \"2018\"|" Cargo.toml
|
||||
RUN cargo build --locked --release
|
||||
RUN mkdir -p build-out/
|
||||
RUN cp target/x86_64-unknown-linux-musl/release/rustypaste build-out/
|
||||
RUN cp target/release/rustypaste build-out/
|
||||
|
||||
FROM scratch
|
||||
WORKDIR /app
|
||||
COPY --from=builder \
|
||||
/home/rust/src/build-out/rustypaste \
|
||||
/home/rust/src/config.toml ./
|
||||
COPY --from=builder /app/build-out/rustypaste .
|
||||
ENV SERVER__ADDRESS=0.0.0.0:8000
|
||||
EXPOSE 8000
|
||||
USER 1000:1000
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 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
|
||||
|
|
206
README.md
206
README.md
|
@ -1,5 +1,13 @@
|
|||
<a href="https://github.com/orhun/rustypaste"><img src="img/rustypaste_logo.png" width="500"></a>
|
||||
|
||||
[![GitHub Release](https://img.shields.io/github/v/release/orhun/rustypaste?style=flat&labelColor=823213&color=2c2c2c&logo=GitHub&logoColor=white)](https://github.com/orhun/rustypaste/releases)
|
||||
[![Crate Release](https://img.shields.io/crates/v/rustypaste?style=flat&labelColor=823213&color=2c2c2c&logo=Rust&logoColor=white)](https://crates.io/crates/rustypaste/)
|
||||
[![Coverage](https://img.shields.io/codecov/c/gh/orhun/rustypaste?style=flat&labelColor=823213&color=2c2c2c&logo=Codecov&logoColor=white)](https://codecov.io/gh/orhun/rustypaste)
|
||||
[![Continuous Integration](https://img.shields.io/github/actions/workflow/status/orhun/rustypaste/ci.yml?branch=master&style=flat&labelColor=823213&color=2c2c2c&logo=GitHub%20Actions&logoColor=white)](https://github.com/orhun/rustypaste/actions?query=workflow%3A%22Continuous+Integration%22)
|
||||
[![Continuous Deployment](https://img.shields.io/github/actions/workflow/status/orhun/rustypaste/cd.yml?style=flat&labelColor=823213&color=2c2c2c&logo=GitHub%20Actions&logoColor=white&label=deploy)](https://github.com/orhun/rustypaste/actions?query=workflow%3A%22Continuous+Deployment%22)
|
||||
[![Docker Builds](https://img.shields.io/github/actions/workflow/status/orhun/rustypaste/docker.yml?style=flat&labelColor=823213&color=2c2c2c&label=docker&logo=Docker&logoColor=white)](https://hub.docker.com/r/orhunp/rustypaste)
|
||||
[![Documentation](https://img.shields.io/docsrs/rustypaste?style=flat&labelColor=823213&color=2c2c2c&logo=Rust&logoColor=white)](https://docs.rs/rustypaste/)
|
||||
|
||||
**Rustypaste** is a minimal file upload/pastebin service.
|
||||
|
||||
```sh
|
||||
|
@ -12,6 +20,47 @@ $ curl https://paste.site.com/safe-toad.txt
|
|||
some text
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [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)
|
||||
- [Testing](#testing)
|
||||
- [Unit tests](#unit-tests)
|
||||
- [Test Fixtures](#test-fixtures)
|
||||
- [Usage](#usage)
|
||||
- [CLI](#cli)
|
||||
- [Expiration](#expiration)
|
||||
- [One shot files](#one-shot-files)
|
||||
- [One shot URLs](#one-shot-urls)
|
||||
- [URL shortening](#url-shortening)
|
||||
- [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)
|
||||
- [Docker](#docker)
|
||||
- [Nginx](#nginx)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
</details>
|
||||
|
||||
## Features
|
||||
|
||||
- File upload & URL shortening & upload from URL
|
||||
|
@ -19,17 +68,24 @@ some text
|
|||
- random file names (optional)
|
||||
- pet name (e.g. `capital-mosquito.txt`)
|
||||
- alphanumeric string (e.g. `yB84D2Dv.txt`)
|
||||
- random suffix (e.g. `file.MRV5as.tar.gz`)
|
||||
- supports expiring links
|
||||
- supports one shot links (can only be viewed once)
|
||||
- auto-expiration of files (optional)
|
||||
- auto-deletion of expired files (optional)
|
||||
- supports one shot links/URLs (can only be viewed once)
|
||||
- guesses MIME types
|
||||
- supports overriding and blacklisting
|
||||
- supports forcing to download via `?download=true`
|
||||
- no duplicate uploads (optional)
|
||||
- listing/deleting files
|
||||
- custom landing page
|
||||
- Single binary
|
||||
- [binary releases](https://github.com/orhun/rustypaste/releases)
|
||||
- Simple configuration
|
||||
- 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
|
||||
|
@ -39,15 +95,42 @@ some text
|
|||
|
||||
## 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
|
||||
cargo install rustypaste
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
```sh
|
||||
pacman -S rustypaste
|
||||
```
|
||||
|
||||
### Alpine Linux
|
||||
|
||||
`rustypaste` is available for [Alpine Edge](https://pkgs.alpinelinux.org/packages?name=rustypaste&branch=edge). It can be installed via [apk](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) after enabling the [community repository](https://wiki.alpinelinux.org/wiki/Repositories).
|
||||
|
||||
```sh
|
||||
apk add rustypaste
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
pkg install rustypaste
|
||||
```
|
||||
|
||||
### Binary releases
|
||||
|
||||
See the available binaries on [releases](https://github.com/orhun/rustypaste/releases/) page.
|
||||
See the available binaries on the [releases](https://github.com/orhun/rustypaste/releases/) page.
|
||||
|
||||
### Build from source
|
||||
|
||||
|
@ -57,6 +140,34 @@ cd rustypaste/
|
|||
cargo build --release
|
||||
```
|
||||
|
||||
#### Feature flags
|
||||
|
||||
- `shuttle`: enable an entry point for deploying on Shuttle
|
||||
- `openssl`: use distro OpenSSL (binary size is reduced ~20% in release mode)
|
||||
- `rustls`: use [rustls](https://github.com/rustls/rustls) (enabled as default)
|
||||
|
||||
To enable a feature for build, pass `--features` flag to `cargo build` command.
|
||||
|
||||
For example, to reuse the OpenSSL present on a distro already:
|
||||
|
||||
```sh
|
||||
cargo build --release --no-default-features --features openssl
|
||||
```
|
||||
|
||||
#### Testing
|
||||
|
||||
##### Unit tests
|
||||
|
||||
```sh
|
||||
cargo test -- --test-threads 1
|
||||
```
|
||||
|
||||
##### Test Fixtures
|
||||
|
||||
```sh
|
||||
./fixtures/test-fixtures.sh
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The standalone command line tool (`rpaste`) is available [here](https://github.com/orhun/rustypaste-cli).
|
||||
|
@ -65,7 +176,7 @@ The standalone command line tool (`rpaste`) is available [here](https://github.c
|
|||
|
||||
```sh
|
||||
function rpaste() {
|
||||
curl -F "file=@$1" -H "Authorization: <auth_token>" "<server_address>"
|
||||
curl -F "file=@$1" -H "Authorization: <auth_token>" "<server_address>"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -85,18 +196,29 @@ $ rpaste -
|
|||
$ curl -F "file=@x.txt" -H "expire:10min" "<server_address>"
|
||||
```
|
||||
|
||||
(supported units: `ns`, `us`, `ms`, `sec`, `min`, `hours`, `days`, `weeks`, `months`, `years`)
|
||||
supported units:
|
||||
|
||||
#### One shot
|
||||
- `nsec`, `ns`
|
||||
- `usec`, `us`
|
||||
- `msec`, `ms`
|
||||
- `seconds`, `second`, `sec`, `s`
|
||||
- `minutes`, `minute`, `min`, `m`
|
||||
- `hours`, `hour`, `hr`, `h`
|
||||
- `days`, `day`, `d`
|
||||
- `weeks`, `week`, `w`
|
||||
- `months`, `month`, `M`
|
||||
- `years`, `year`, `y`
|
||||
|
||||
#### One shot files
|
||||
|
||||
```sh
|
||||
$ curl -F "oneshot=@x.txt" "<server_address>"
|
||||
```
|
||||
|
||||
#### Cleaning up expired files
|
||||
#### One shot URLs
|
||||
|
||||
```sh
|
||||
$ find upload/ -maxdepth 2 -type f -iname "*.[0-9]*" -exec rm -v {} \;
|
||||
$ curl -F "oneshot_url=https://example.com" "<server_address>"
|
||||
```
|
||||
|
||||
#### URL shortening
|
||||
|
@ -111,6 +233,39 @@ $ curl -F "url=https://example.com/some/long/url" "<server_address>"
|
|||
$ curl -F "remote=https://example.com/file.png" "<server_address>"
|
||||
```
|
||||
|
||||
#### Cleaning up expired files
|
||||
|
||||
Configure `[paste].delete_expired_files` to set an interval for deleting the expired files automatically.
|
||||
|
||||
On the other hand, following script can be used as [cron](https://en.wikipedia.org/wiki/Cron) for cleaning up the expired files manually:
|
||||
|
||||
```sh
|
||||
#!/bin/env sh
|
||||
now=$(date +%s)
|
||||
find upload/ -maxdepth 2 -type f -iname "*.[0-9]*" |
|
||||
while read -r filename; do
|
||||
[ "$(( ${filename##*.} / 1000 - "${now}" ))" -lt 0 ] && rm -v "${filename}"
|
||||
done
|
||||
```
|
||||
|
||||
#### Delete file from server
|
||||
|
||||
Set `delete_tokens` array in [config.toml](./config.toml) to activate the [`DELETE`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) endpoint and secure it with one (or more) auth token(s).
|
||||
|
||||
```sh
|
||||
$ 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:
|
||||
|
@ -132,8 +287,35 @@ $ echo "AUTH_TOKEN=$(openssl rand -base64 16)" > .env
|
|||
$ 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
|
||||
|
||||
Set `expose_list` to true in [config.toml](./config.toml) to be able to retrieve a JSON formatted list of files in your uploads directory. This will not include oneshot files, oneshot URLs, or URLs.
|
||||
|
||||
```sh
|
||||
$ curl "http://<server_address>/list"
|
||||
|
||||
[{"file_name":"accepted-cicada.txt","file_size":241,"expires_at_utc":null}]
|
||||
```
|
||||
|
||||
This route will require an `AUTH_TOKEN` if one is set.
|
||||
|
||||
#### HTML Form
|
||||
|
||||
It is possible to use an HTML form for uploading files. To do so, you need to update two fields in your `config.toml`:
|
||||
|
||||
- Set the `[landing_page].content_type` to `text/html; charset=utf-8`.
|
||||
- Update the `[landing_page].text` field with your HTML form or point `[landing_page].file` to your html file.
|
||||
|
||||
For an example, see [examples/html_form.toml](./examples/html_form.toml)
|
||||
|
||||
#### Docker
|
||||
|
||||
Following command can be used to run a container which is built from the [Dockerfile](./Dockerfile) in this repository:
|
||||
|
@ -184,19 +366,11 @@ http {
|
|||
}
|
||||
```
|
||||
|
||||
### Roadmap
|
||||
|
||||
_Nothing here yet! 🎉_
|
||||
|
||||
### Contributing
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Consider submitting your ideas via issues first. Also, see the [roadmap](#roadmap) and/or run the following command to see what is needed to be done:
|
||||
|
||||
```sh
|
||||
$ grep -nr "TODO:" src/
|
||||
```
|
||||
Consider submitting your ideas via [issues](https://github.com/orhun/rustypaste/issues/new) first and check out the [existing issues](https://github.com/orhun/rustypaste/issues).
|
||||
|
||||
#### License
|
||||
|
||||
|
|
|
@ -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`.
|
|
@ -0,0 +1,6 @@
|
|||
codecov:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 2%
|
77
config.toml
77
config.toml
|
@ -1,25 +1,70 @@
|
|||
[config]
|
||||
refresh_rate = "1s"
|
||||
|
||||
[server]
|
||||
address="127.0.0.1:8000"
|
||||
address = "127.0.0.1:8000"
|
||||
#url = "https://rustypaste.shuttleapp.rs"
|
||||
#workers=4
|
||||
max_content_length="10MB"
|
||||
upload_path="./upload"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
timeout = "30s"
|
||||
expose_version = false
|
||||
expose_list = false
|
||||
#auth_tokens = [
|
||||
# "super_secret_token1",
|
||||
# "super_secret_token2",
|
||||
#]
|
||||
#delete_tokens = [
|
||||
# "super_secret_token1",
|
||||
# "super_secret_token3",
|
||||
#]
|
||||
handle_spaces = "replace" # or "encode"
|
||||
|
||||
[landing_page]
|
||||
text = """
|
||||
┬─┐┬ ┬┌─┐┌┬┐┬ ┬┌─┐┌─┐┌─┐┌┬┐┌─┐
|
||||
├┬┘│ │└─┐ │ └┬┘├─┘├─┤└─┐ │ ├┤
|
||||
┴└─└─┘└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘
|
||||
|
||||
Submit files via HTTP POST here:
|
||||
curl -F 'file=@example.txt' <server>
|
||||
This will return the URL of the uploaded file.
|
||||
|
||||
The server administrator might remove any pastes that they do not personally
|
||||
want to host.
|
||||
|
||||
If you are the server administrator and want to change this page, just go
|
||||
into your config file and change it! If you change the expiry time, it is
|
||||
recommended that you do.
|
||||
|
||||
By default, pastes expire every hour. The server admin may or may not have
|
||||
changed this.
|
||||
|
||||
Check out the GitHub repository at https://github.com/orhun/rustypaste
|
||||
Command line tool is available at https://github.com/orhun/rustypaste-cli
|
||||
"""
|
||||
#file = "index.txt"
|
||||
content_type = "text/plain; charset=utf-8"
|
||||
|
||||
[paste]
|
||||
random_url = { enabled = true, type = "petname", words = 2, separator = "-" }
|
||||
#random_url = { enabled = true, type = "alphanumeric", length = 8 }
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
#random_url = { type = "alphanumeric", length = 8 }
|
||||
#random_url = { type = "alphanumeric", length = 6, suffix_mode = true }
|
||||
default_extension = "txt"
|
||||
mime_override = [
|
||||
{ mime = "image/jpeg", regex = "^.*\\.jpg$" },
|
||||
{ mime = "image/png", regex = "^.*\\.png$" },
|
||||
{ mime = "image/svg+xml", regex = "^.*\\.svg$" },
|
||||
{ mime = "video/webm", regex = "^.*\\.webm$" },
|
||||
{ mime = "video/x-matroska", regex = "^.*\\.mkv$" },
|
||||
{ mime = "application/octet-stream", regex = "^.*\\.bin$" },
|
||||
{ mime = "text/plain", regex = "^.*\\.(log|txt|diff)$" },
|
||||
{ mime = "image/jpeg", regex = "^.*\\.jpg$" },
|
||||
{ mime = "image/png", regex = "^.*\\.png$" },
|
||||
{ mime = "image/svg+xml", regex = "^.*\\.svg$" },
|
||||
{ mime = "video/webm", regex = "^.*\\.webm$" },
|
||||
{ mime = "video/x-matroska", regex = "^.*\\.mkv$" },
|
||||
{ mime = "application/octet-stream", regex = "^.*\\.bin$" },
|
||||
{ mime = "text/plain", regex = "^.*\\.(log|txt|diff|sh|rs|toml)$" },
|
||||
]
|
||||
mime_blacklist = [
|
||||
"application/x-dosexec",
|
||||
"application/java-archive",
|
||||
"application/java-vm"
|
||||
"application/x-dosexec",
|
||||
"application/java-archive",
|
||||
"application/java-vm",
|
||||
]
|
||||
duplicate_files = false
|
||||
duplicate_files = true
|
||||
# default_expiry = "1h"
|
||||
delete_expired_files = { enabled = true, interval = "1h" }
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
[config]
|
||||
refresh_rate = "1s"
|
||||
|
||||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
#url = "https://rustypaste.shuttleapp.rs"
|
||||
#workers=4
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
timeout = "30s"
|
||||
expose_version = false
|
||||
|
||||
[landing_page]
|
||||
file = "landing_page.html"
|
||||
content_type = "text/html; charset=utf-8"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
#random_url = { type = "alphanumeric", length = 8 }
|
||||
default_extension = "txt"
|
||||
mime_override = [
|
||||
{ mime = "image/jpeg", regex = "^.*\\.jpg$" },
|
||||
{ mime = "image/png", regex = "^.*\\.png$" },
|
||||
{ mime = "image/svg+xml", regex = "^.*\\.svg$" },
|
||||
{ mime = "video/webm", regex = "^.*\\.webm$" },
|
||||
{ mime = "video/x-matroska", regex = "^.*\\.mkv$" },
|
||||
{ mime = "application/octet-stream", regex = "^.*\\.bin$" },
|
||||
{ mime = "text/plain", regex = "^.*\\.(log|txt|diff|sh|rs|toml)$" },
|
||||
]
|
||||
mime_blacklist = [
|
||||
"application/x-dosexec",
|
||||
"application/java-archive",
|
||||
"application/java-vm",
|
||||
]
|
||||
duplicate_files = true
|
||||
# default_expiry = "1h"
|
||||
delete_expired_files = { enabled = true, interval = "1h" }
|
|
@ -0,0 +1,143 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>rustypaste</title>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
font-family: arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
pre {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
h2 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="file"] {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 18px;
|
||||
margin-right: 10px;
|
||||
padding: 10px;
|
||||
width: 400px;
|
||||
}
|
||||
input[type="submit"] {
|
||||
background-color: #333;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 18px;
|
||||
padding: 10px 20px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
input[type="submit"]:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
┬─┐┬ ┬┌─┐┌┬┐┬ ┬┌─┐┌─┐┌─┐┌┬┐┌─┐
|
||||
├┬┘│ │└─┐ │ └┬┘├─┘├─┤└─┐ │ ├┤
|
||||
┴└─└─┘└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘
|
||||
the server administrator might remove any pastes that they do not personally
|
||||
want to host.
|
||||
|
||||
by default, pastes expire every hour.
|
||||
</pre
|
||||
>
|
||||
<h2>share url</h2>
|
||||
<form action="/" method="post" enctype="multipart/form-data">
|
||||
<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="text" name="remote" />
|
||||
<input type="submit" value="share" />
|
||||
</form>
|
||||
|
||||
<h2>share file</h2>
|
||||
<form action="/" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<input type="submit" value="share" />
|
||||
</form>
|
||||
|
||||
<h2>share one-time file</h2>
|
||||
<form action="/" method="post" enctype="multipart/form-data">
|
||||
<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>
|
|
@ -0,0 +1,2 @@
|
|||
# To enable basic HTTP auth, set the AUTH_TOKEN
|
||||
AUTH_TOKEN=""
|
|
@ -0,0 +1,31 @@
|
|||
[Unit]
|
||||
Description=Rustypaste server
|
||||
After=network-online.target
|
||||
Wants=network-online.target systemd-networkd-wait-online.service
|
||||
|
||||
[Service]
|
||||
User=rustypaste
|
||||
Group=rustypaste
|
||||
ExecStart=/usr/bin/rustypaste
|
||||
ReadWritePaths=/var/lib/rustypaste
|
||||
ReadOnlyPaths=/etc/rustypaste
|
||||
|
||||
WorkingDirectory=/var/lib/rustypaste
|
||||
Environment="CONFIG=/etc/rustypaste/config.toml"
|
||||
EnvironmentFile=/etc/rustypaste/rustypaste.env
|
||||
|
||||
# Hardening options
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
NoNewPrivileges=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1 @@
|
|||
u rustypaste - "Minimal file upload/pastebin service" /var/lib/rustypaste
|
|
@ -0,0 +1 @@
|
|||
d /var/lib/rustypaste 0750 rustypaste rustypaste
|
|
@ -0,0 +1 @@
|
|||
RUST_LOG=error
|
|
@ -0,0 +1,54 @@
|
|||
## Fixtures
|
||||
|
||||
This directory contains the [test fixtures](https://en.wikipedia.org/wiki/Test_fixture) and a simple testing framework for `rustypaste`.
|
||||
|
||||
### Running fixtures
|
||||
|
||||
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`
|
||||
|
||||
Each fixture directory should contain the following files:
|
||||
|
||||
```
|
||||
test-file-upload/
|
||||
├── config.toml
|
||||
└── test.sh
|
||||
```
|
||||
|
||||
- `config.toml`: Contains the `rustypaste` configuration. See [the default configuration](../config.toml).
|
||||
- `test.sh`: Contains the helper functions for testing. The file format is the following:
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
setup() {
|
||||
# preparation
|
||||
}
|
||||
|
||||
run_test() {
|
||||
# assertions
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# cleanup
|
||||
}
|
||||
```
|
||||
|
||||
These functions are executed in the order defined above.
|
||||
|
||||
See the [test-file-upload](./test-file-upload/test.sh) fixture for an example.
|
||||
|
||||
### Debugging
|
||||
|
||||
Set the `DEBUG` environment variable to `true` while executing the runner script:
|
||||
|
||||
```sh
|
||||
$ DEBUG=true ./test-fixtures.sh
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
duplicate_content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$duplicate_content" > file
|
||||
date +%s > unique_file1
|
||||
sleep 1
|
||||
date +%s > unique_file2
|
||||
}
|
||||
|
||||
run_test() {
|
||||
first_file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$duplicate_content" = "$(cat upload/${first_file_url/http:\/\/localhost:8000\//})"
|
||||
|
||||
second_file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$first_file_url" = "$second_file_url"
|
||||
for url in "$first_file_url" "$second_file_url"; do
|
||||
test "$duplicate_content" = "$(curl -s $url)"
|
||||
done
|
||||
|
||||
first_file_url=$(curl -s -F "file=@unique_file1" localhost:8000)
|
||||
second_file_url=$(curl -s -F "file=@unique_file2" localhost:8000)
|
||||
test "$first_file_url" != "$second_file_url"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file unique_file1 unique_file2
|
||||
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,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" -H "expire:1s" localhost:8000)
|
||||
test "$content" = "$(cat upload/file.txt.*)"
|
||||
sleep 2
|
||||
|
||||
result="$(curl -s $file_url)"
|
||||
test "file is not found or expired :(" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
delete_tokens = ["may_the_force_be_with_you"]
|
||||
|
||||
[paste]
|
||||
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" localhost:8000)
|
||||
test "$file_url" = "http://localhost:8000/file.txt"
|
||||
test "file deleted" = "$(curl -s -H "Authorization: may_the_force_be_with_you" -X DELETE http://localhost:8000/file.txt)"
|
||||
test "file is not found or expired :(" = "$(curl -s -H "Authorization: may_the_force_be_with_you" -X DELETE http://localhost:8000/file.txt)"
|
||||
}
|
||||
|
||||
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,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,8 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
echo "$content" > .file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$file_url" = "http://localhost:8000/file.txt"
|
||||
test "$content" = "$(cat upload/file.txt)"
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
file_url2=$(curl -s -F "file=@.file" localhost:8000)
|
||||
test "$file_url2" = "http://localhost:8000/.file.txt"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file .file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[server]
|
||||
address="127.0.0.1:8000"
|
||||
max_content_length="10MB"
|
||||
upload_path="./upload"
|
||||
handle_spaces = "replace"
|
||||
|
||||
[paste]
|
||||
#random_url = { enabled = false, type = "alphanumeric", length = 6, suffix_mode = true }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data for space replacement"
|
||||
|
||||
setup() {
|
||||
echo "$content" > "test file with spaces.txt"
|
||||
}
|
||||
|
||||
run_test() {
|
||||
# Upload the file and get the URL.
|
||||
replaced_url=$(curl -s -F "file=@test file with spaces.txt" localhost:8000)
|
||||
|
||||
# Ensure the URL contains underscores instead of spaces.
|
||||
expected_url="http://localhost:8000/test_file_with_spaces.txt"
|
||||
test "$replaced_url" = "$expected_url"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm "test file with spaces.txt"
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
FIXTURE_DIR=$(readlink -f "$(dirname "$0")")
|
||||
PROJECT_DIR="$FIXTURE_DIR/.."
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
run_fixture() {
|
||||
cd "$FIXTURE_DIR/$1" || exit 1
|
||||
source "test.sh"
|
||||
NO_COLOR=1 CONFIG=config.toml "$PROJECT_DIR/target/debug/rustypaste" &
|
||||
SERVER_PID=$!
|
||||
trap 'kill -9 "$SERVER_PID" && wait "$SERVER_PID" 2> /dev/null' RETURN
|
||||
sleep 1
|
||||
( set -e;
|
||||
setup
|
||||
run_test
|
||||
teardown
|
||||
)
|
||||
result=$?
|
||||
return "$result"
|
||||
}
|
||||
|
||||
main() {
|
||||
find * -maxdepth 0 -type d -print0 | while IFS= read -r -d '' fixture; do
|
||||
run_fixture "$fixture"
|
||||
exit_status=$?
|
||||
if [ "$exit_status" -eq 0 ]; then
|
||||
echo -e "[${GREEN}ok${NC}] $fixture"
|
||||
else
|
||||
echo -e "[${RED}fail${NC}] $fixture"
|
||||
exit "$exit_status"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
[ "$DEBUG" == 'true' ] && set -x && export RUST_LOG=debug
|
||||
main
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
auth_token = "rustypasteisawesome"
|
||||
expose_list = true
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
auth_token="rustypasteisawesome"
|
||||
content="test data"
|
||||
file_count=3
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
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 "unauthorized" = "$(curl -s localhost:8000/list)"
|
||||
}
|
||||
|
||||
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]
|
||||
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,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="0nesh0t"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "oneshot=@file" localhost:8000)
|
||||
test "$content" = $(curl -s "$file_url")
|
||||
test "$content" = "$(cat upload/oneshot/file.txt.*)"
|
||||
|
||||
result="$(curl -s $file_url)"
|
||||
test "file is not found or expired :(" = "$result"
|
||||
}
|
||||
|
||||
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,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
url="https://orhun.dev/"
|
||||
|
||||
setup() {
|
||||
:;
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "oneshot_url=$url" localhost:8000)
|
||||
test "$url" = $(curl -Ls -w %{url_effective} -o /dev/null "$file_url")
|
||||
test "$url" = "$(cat upload/oneshot_url/oneshot_url.*)"
|
||||
|
||||
result="$(curl -s $file_url)"
|
||||
test "file is not found or expired :(" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10kb"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s --path-as-is localhost:8000/.)
|
||||
test "file is not found or expired :(" = "$result"
|
||||
|
||||
result=$(curl -s --write-out "%{http_code}" --path-as-is localhost:8000/../test.sh)
|
||||
test "404" = "$result"
|
||||
|
||||
result=$(curl -s -X POST -F "file=@file;filename=../." localhost:8000)
|
||||
test "$content" = "$(cat upload/file.txt)"
|
||||
}
|
||||
|
||||
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 = "6", suffix_mode = true }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" >file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
first_file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$first_file_url" != "http://localhost:8000/file.txt"
|
||||
test "$content" = "$(curl -s $first_file_url)"
|
||||
|
||||
second_file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$first_file_url" != "http://localhost:8000/file.txt"
|
||||
test "$content" = "$(curl -s $first_file_url)"
|
||||
|
||||
test "$first_file_url" != "$second_file_url"
|
||||
|
||||
test "$(cat upload/${first_file_url/http:\/\/localhost:8000\//})" \
|
||||
= "$(cat upload/${second_file_url/http:\/\/localhost:8000\//})"
|
||||
|
||||
[[ $(find upload/ -name "file.*.txt" -print -quit 2>/dev/null) ]] || exit 1
|
||||
}
|
||||
|
||||
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 = "8" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
first_file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$first_file_url" != "http://localhost:8000/file.txt"
|
||||
test "$content" = "$(curl -s $first_file_url)"
|
||||
|
||||
second_file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$first_file_url" != "http://localhost:8000/file.txt"
|
||||
test "$content" = "$(curl -s $first_file_url)"
|
||||
|
||||
test "$first_file_url" != "$second_file_url"
|
||||
|
||||
test "$(cat upload/${first_file_url/http:\/\/localhost:8000\//})" \
|
||||
= "$(cat upload/${second_file_url/http:\/\/localhost:8000\//})"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
timeout = "60s"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
url="https://raw.githubusercontent.com/orhun/rustypaste/master/img/rustypaste_logo.png"
|
||||
|
||||
setup() {
|
||||
:;
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "remote=$url" localhost:8000)
|
||||
curl -s "$file_url" -o uploaded_file > /dev/null
|
||||
curl -s "$url" -o remote_file > /dev/null
|
||||
test "$(sha256sum uploaded_file | awk '{print $1}')" = "$(sha256sum remote_file | awk '{print $1}')"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm uploaded_file remote_file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
auth_tokens = ["token1", "token2", "rustypasteisawesome", "token4"]
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
auth_tokens=("rustypasteisawesome" "token1" "token2" "token4")
|
||||
|
||||
content="topsecret"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "unauthorized" = "$result"
|
||||
|
||||
for auth_token in ${auth_tokens[@]}
|
||||
do
|
||||
result=$(curl -s -F "file=@file" -H "Authorization: $auth_token" localhost:8000)
|
||||
test "unauthorized" != "$result"
|
||||
test "$content" = "$(cat upload/file.txt)"
|
||||
test "$content" = "$(curl -s $result)"
|
||||
done
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
auth_token = "rustypasteisawesome"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
auth_token="rustypasteisawesome"
|
||||
content="topsecret"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "unauthorized" = "$result"
|
||||
|
||||
result=$(curl -s -F "file=@file" -H "Authorization: $auth_token" localhost:8000)
|
||||
test "unauthorized" != "$result"
|
||||
test "$content" = "$(cat upload/file.txt)"
|
||||
test "$content" = "$(curl -s $result)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
||||
delete_expired_files = { enabled = true, interval = "2s" }
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test content"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
}
|
||||
|
||||
run_test() {
|
||||
first_file_url=$(curl -s -F "file=@file" -H "expire:1s" localhost:8000)
|
||||
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 3
|
||||
test "file is not found or expired :(" = "$(curl -s $first_file_url)"
|
||||
test "$content" = "$(curl -s $second_file_url)"
|
||||
sleep 1
|
||||
test "file is not found or expired :(" = "$(curl -s $second_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 = "bin"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "$content" > file
|
||||
echo "<html></html>" > file.html
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||
test "$file_url" = "http://localhost:8000/file.bin"
|
||||
test "$content" = "$(cat upload/file.bin)"
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
|
||||
test "http://localhost:8000/file.html" = "$(curl -s -F file=@file.html localhost:8000)"
|
||||
}
|
||||
|
||||
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,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
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[landing_page]
|
||||
text = "awesome_landing"
|
||||
file = "page.txt"
|
||||
content_type = "text/plain; charset=utf-8"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
landing_page="awesome_landing_from_file"
|
||||
|
||||
setup() {
|
||||
echo $landing_page >page.txt
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s localhost:8000)
|
||||
test "$landing_page" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm page.txt
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[landing_page]
|
||||
text = "awesome_landing"
|
||||
content_type = "text/plain; charset=utf-8"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
landing_page="awesome_landing"
|
||||
|
||||
setup() {
|
||||
:;
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s localhost:8000)
|
||||
test "$landing_page" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
:;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
||||
mime_blacklist = ["text/html", "text/xml"]
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data"
|
||||
|
||||
setup() {
|
||||
echo "<html></html>" > file.html
|
||||
echo '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' > file.xml
|
||||
echo "$content" > file.txt
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm file.*
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
max_content_length = "10MB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
random_url = { type = "petname", words = 2, separator = "-" }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
||||
mime_override = [
|
||||
{ mime = "application/x-shockwave-flash", regex = "^.*\\.txt$" },
|
||||
{ mime = "image/gif", regex = "^.*\\.tar$" },
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test"
|
||||
|
||||
setup() {
|
||||
for ext in "txt" "tar" "png"; do
|
||||
echo "$content" > "file.$ext"
|
||||
done
|
||||
}
|
||||
|
||||
run_test() {
|
||||
file_url=$(curl -s -F "file=@file.txt" localhost:8000)
|
||||
test "application/x-shockwave-flash" = "$(curl -s --write-out %{content_type} $file_url | tail -n 1)"
|
||||
|
||||
file_url=$(curl -s -F "file=@file.tar" localhost:8000)
|
||||
test "image/gif" = "$(curl -s --write-out %{content_type} $file_url | tail -n 1)"
|
||||
|
||||
file_url=$(curl -s -F "file=@file.png" localhost:8000)
|
||||
test "image/png" = "$(curl -s --write-out %{content_type} $file_url | tail -n 1)"
|
||||
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 = "10KB"
|
||||
upload_path = "./upload"
|
||||
|
||||
[paste]
|
||||
default_extension = "txt"
|
||||
duplicate_files = false
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
setup() {
|
||||
touch emptyfile
|
||||
truncate -s 9KB smallfile
|
||||
# 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
|
||||
}
|
||||
|
||||
run_test() {
|
||||
result=$(curl -s -F "file=@emptyfile" localhost:8000)
|
||||
test "invalid file size" = "$result"
|
||||
|
||||
result=$(curl -s -F "file=@bigfile" localhost:8000)
|
||||
test "upload limit exceeded" = "$result"
|
||||
|
||||
result=$(curl -s -F "file=@normalfile" localhost:8000)
|
||||
test "upload limit exceeded" = "$result"
|
||||
|
||||
result=$(curl -s -F "file=@smallfile" localhost:8000)
|
||||
test "upload limit exceeded" != "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm emptyfile smallfile normalfile bigfile
|
||||
rm -r upload
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[server]
|
||||
address="127.0.0.1:8000"
|
||||
max_content_length="10MB"
|
||||
upload_path="./upload"
|
||||
handle_spaces = "encode"
|
||||
|
||||
[paste]
|
||||
#random_url = { enabled = false, type = "alphanumeric", length = 6, suffix_mode = true }
|
||||
default_extension = "txt"
|
||||
duplicate_files = true
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
content="test data for URL encoding"
|
||||
|
||||
setup() {
|
||||
echo "$content" > "test file with spaces.txt"
|
||||
}
|
||||
|
||||
run_test() {
|
||||
# Upload the file and get the URL.
|
||||
encoded_url=$(curl -s -F "file=@test file with spaces.txt" localhost:8000)
|
||||
|
||||
# Ensure the URL is encoded correctly.
|
||||
expected_url="http://localhost:8000/test%20file%20with%20spaces.txt"
|
||||
test "$encoded_url" = "$expected_url"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm "test file with spaces.txt"
|
||||
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
|
||||
|
||||
url="https://orhun.dev/"
|
||||
|
||||
setup() {
|
||||
:;
|
||||
}
|
||||
|
||||
run_test() {
|
||||
curl -s -F "url=$url" localhost:8000 > /dev/null
|
||||
test "$url" = "$(cat upload/url/url)"
|
||||
|
||||
result=$(curl -s -F "url=invalidurl" localhost:8000)
|
||||
test "relative URL without a base" = "$result"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -r upload
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
[config]
|
||||
refresh_rate = "1s"
|
||||
|
||||
[server]
|
||||
address = "127.0.0.1:8000"
|
||||
url = "https://rustypaste.shuttleapp.rs"
|
||||
#workers=4
|
||||
max_content_length = "20MB"
|
||||
upload_path = "./upload"
|
||||
timeout = "30s"
|
||||
expose_version = true
|
||||
landing_page = """
|
||||
┬─┐┬ ┬┌─┐┌┬┐┬ ┬┌─┐┌─┐┌─┐┌┬┐┌─┐
|
||||
├┬┘│ │└─┐ │ └┬┘├─┘├─┤└─┐ │ ├┤
|
||||
┴└─└─┘└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘
|
||||
|
||||
Submit files via HTTP POST here:
|
||||
|
||||
curl -F 'file=@example.txt' https://rustypaste.shuttleapp.rs
|
||||
|
||||
This will return the URL of the uploaded file.
|
||||
|
||||
Pastes expire every 24 hours. Uploaded files might not be persistent.
|
||||
|
||||
Check out the GitHub repository: https://github.com/orhun/rustypaste
|
||||
Command line tool is available : https://github.com/orhun/rustypaste-cli
|
||||
|
||||
If you liked this, consider supporting me: https://donate.orhun.dev <3
|
||||
|
||||
🦀
|
||||
"""
|
||||
landing_page_content_type = "text/plain; charset=utf-8"
|
||||
|
||||
[paste]
|
||||
# random_url = { type = "petname", words = 2, separator = "-" }
|
||||
random_url = { type = "alphanumeric", length = 6 }
|
||||
default_extension = "txt"
|
||||
mime_override = [
|
||||
{ mime = "image/jpeg", regex = "^.*\\.jpg$" },
|
||||
{ mime = "image/png", regex = "^.*\\.png$" },
|
||||
{ mime = "image/svg+xml", regex = "^.*\\.svg$" },
|
||||
{ mime = "video/webm", regex = "^.*\\.webm$" },
|
||||
{ mime = "video/x-matroska", regex = "^.*\\.mkv$" },
|
||||
{ mime = "application/octet-stream", regex = "^.*\\.bin$" },
|
||||
{ mime = "text/plain", regex = "^.*\\.(log|txt|diff|sh|kt|rs|toml)$" },
|
||||
]
|
||||
mime_blacklist = [
|
||||
"application/x-dosexec",
|
||||
"application/java-archive",
|
||||
"application/java-vm",
|
||||
]
|
||||
duplicate_files = true
|
||||
default_expiry = "24h"
|
||||
delete_expired_files = { enabled = true, interval = "1h" }
|
174
src/auth.rs
174
src/auth.rs
|
@ -1,44 +1,162 @@
|
|||
use crate::config::{Config, TokenType};
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::http::header::AUTHORIZATION;
|
||||
use actix_web::http::HeaderMap;
|
||||
use actix_web::{error, Error};
|
||||
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, token: Option<String>) -> Result<(), Error> {
|
||||
if let Some(token) = token {
|
||||
if !token.is_empty() {
|
||||
let auth_header = headers
|
||||
.get(AUTHORIZATION)
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
.map(|v| v.split_whitespace().last().unwrap_or_default());
|
||||
if auth_header.unwrap_or_default() != token {
|
||||
log::warn!(
|
||||
"authorization failure for {} (header: {})",
|
||||
host,
|
||||
auth_header.unwrap_or("none"),
|
||||
);
|
||||
return Err(error::ErrorUnauthorized("unauthorized"));
|
||||
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::HeaderValue;
|
||||
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(String::from("test_token"))).is_ok());
|
||||
assert!(check("", &headers, Some(String::from("invalid_token"))).is_err());
|
||||
assert!(check("", &headers, None).is_ok());
|
||||
assert!(check("", &HeaderMap::new(), None).is_ok());
|
||||
assert!(check("", &HeaderMap::new(), Some(String::from("token"))).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
225
src/config.rs
225
src/config.rs
|
@ -1,16 +1,33 @@
|
|||
use crate::mime::MimeMatcher;
|
||||
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;
|
||||
|
||||
/// Configuration values.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Config {
|
||||
/// Configuration settings.
|
||||
#[serde(rename = "config")]
|
||||
pub settings: Option<Settings>,
|
||||
/// Server configuration.
|
||||
pub server: ServerConfig,
|
||||
/// Paste configuration.
|
||||
pub paste: PasteConfig,
|
||||
/// Landing page configuration.
|
||||
pub landing_page: Option<LandingPageConfig>,
|
||||
}
|
||||
|
||||
/// General settings for configuration.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Settings {
|
||||
/// Refresh rate of the configuration file.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub refresh_rate: Duration,
|
||||
}
|
||||
|
||||
/// Server configuration.
|
||||
|
@ -18,37 +35,171 @@ pub struct Config {
|
|||
pub struct ServerConfig {
|
||||
/// The socket address to bind.
|
||||
pub address: String,
|
||||
/// URL that can be used to access the server externally.
|
||||
pub url: Option<String>,
|
||||
/// Number of workers to start.
|
||||
pub workers: Option<usize>,
|
||||
/// Maximum content length.
|
||||
pub max_content_length: Byte,
|
||||
/// Storage path.
|
||||
pub upload_path: PathBuf,
|
||||
/// Request timeout.
|
||||
#[serde(default, with = "humantime_serde")]
|
||||
pub timeout: Option<Duration>,
|
||||
/// Authentication token.
|
||||
#[deprecated(note = "use [server].auth_tokens instead")]
|
||||
pub auth_token: Option<String>,
|
||||
/// Authentication tokens.
|
||||
pub auth_tokens: Option<HashSet<String>>,
|
||||
/// Expose version.
|
||||
pub expose_version: Option<bool>,
|
||||
/// Landing page text.
|
||||
#[deprecated(note = "use the [landing_page] table")]
|
||||
pub landing_page: Option<String>,
|
||||
/// Landing page content-type.
|
||||
#[deprecated(note = "use the [landing_page] table")]
|
||||
pub landing_page_content_type: Option<String>,
|
||||
/// Handle spaces either via encoding or replacing.
|
||||
pub handle_spaces: Option<SpaceHandlingConfig>,
|
||||
/// Path of the JSON index.
|
||||
pub expose_list: Option<bool>,
|
||||
/// Authentication tokens for deleting.
|
||||
pub delete_tokens: Option<HashSet<String>>,
|
||||
}
|
||||
|
||||
/// Enum representing different strategies for handling spaces in filenames.
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SpaceHandlingConfig {
|
||||
/// Represents encoding spaces (e.g., using "%20").
|
||||
Encode,
|
||||
/// Represents replacing spaces with underscores.
|
||||
Replace,
|
||||
}
|
||||
|
||||
impl SpaceHandlingConfig {
|
||||
/// Processes the given filename based on the specified space handling strategy.
|
||||
pub fn process_filename(&self, file_name: &str) -> String {
|
||||
match self {
|
||||
Self::Encode => file_name.replace(' ', "%20"),
|
||||
Self::Replace => file_name.replace(' ', "_"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Landing page configuration.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct LandingPageConfig {
|
||||
/// Landing page text.
|
||||
pub text: Option<String>,
|
||||
/// Landing page file.
|
||||
pub file: Option<String>,
|
||||
/// Landing page content-type
|
||||
pub content_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Paste configuration.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PasteConfig {
|
||||
/// Random URL configuration.
|
||||
pub random_url: RandomURLConfig,
|
||||
pub random_url: Option<RandomURLConfig>,
|
||||
/// Default file extension.
|
||||
pub default_extension: String,
|
||||
/// Media type override options.
|
||||
#[serde(default)]
|
||||
pub mime_override: Vec<MimeMatcher>,
|
||||
/// Media type blacklist.
|
||||
#[serde(default)]
|
||||
pub mime_blacklist: Vec<String>,
|
||||
/// Allow duplicate uploads
|
||||
/// Allow duplicate uploads.
|
||||
pub duplicate_files: Option<bool>,
|
||||
/// Default expiry time.
|
||||
#[serde(default, with = "humantime_serde")]
|
||||
pub default_expiry: Option<Duration>,
|
||||
/// Delete expired files.
|
||||
pub delete_expired_files: Option<CleanupConfig>,
|
||||
}
|
||||
|
||||
/// Cleanup configuration.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CleanupConfig {
|
||||
/// Enable cleaning up.
|
||||
pub enabled: bool,
|
||||
/// Interval between clean-ups.
|
||||
#[serde(default, with = "humantime_serde")]
|
||||
pub interval: Duration,
|
||||
}
|
||||
|
||||
/// Type of access token.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum TokenType {
|
||||
/// Token for authentication.
|
||||
Auth,
|
||||
/// Token for DELETE endpoint.
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Parses the config file and returns the values.
|
||||
pub fn parse(path: &Path) -> Result<Config, ConfigError> {
|
||||
let mut config = config::Config::default();
|
||||
config
|
||||
.merge(config::File::from(path))?
|
||||
.merge(config::Environment::new().separator("__"))?;
|
||||
config.try_into()
|
||||
config::Config::builder()
|
||||
.add_source(config::File::from(path))
|
||||
.add_source(config::Environment::default().separator("__"))
|
||||
.build()?
|
||||
.try_deserialize()
|
||||
}
|
||||
|
||||
/// Retrieves all configured auth/delete tokens.
|
||||
pub fn get_tokens(&self, token_type: TokenType) -> Option<HashSet<String>> {
|
||||
let mut tokens = match token_type {
|
||||
TokenType::Auth => {
|
||||
let mut tokens: HashSet<_> = self.server.auth_tokens.clone().unwrap_or_default();
|
||||
|
||||
#[allow(deprecated)]
|
||||
if let Some(token) = &self.server.auth_token {
|
||||
tokens.insert(token.to_string());
|
||||
}
|
||||
if let Ok(env_token) = env::var(AUTH_TOKEN_ENV) {
|
||||
tokens.insert(env_token);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
TokenType::Delete => {
|
||||
let mut tokens: HashSet<_> = self.server.delete_tokens.clone().unwrap_or_default();
|
||||
|
||||
if let Ok(env_token) = env::var(DELETE_TOKEN_ENV) {
|
||||
tokens.insert(env_token);
|
||||
}
|
||||
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() {
|
||||
warn!("[server].auth_token is deprecated, please use [server].auth_tokens");
|
||||
}
|
||||
if self.server.landing_page.is_some() {
|
||||
warn!("[server].landing_page is deprecated, please use [landing_page].text");
|
||||
}
|
||||
if self.server.landing_page_content_type.is_some() {
|
||||
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() {
|
||||
warn!(
|
||||
"[paste].random_url.enabled is deprecated, disable it by commenting out [paste].random_url"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,4 +216,64 @@ mod tests {
|
|||
assert_eq!("0.0.1.1", config.server.address);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_parse_deprecated_config() -> Result<(), ConfigError> {
|
||||
let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml");
|
||||
env::set_var("SERVER__ADDRESS", "0.0.1.1");
|
||||
let mut config = Config::parse(&config_path)?;
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
enabled: Some(true),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
assert_eq!("0.0.1.1", config.server.address);
|
||||
config.warn_deprecation();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_space_handling() {
|
||||
let processed_filename =
|
||||
SpaceHandlingConfig::Replace.process_filename("file with spaces.txt");
|
||||
assert_eq!("file_with_spaces.txt", processed_filename);
|
||||
let encoded_filename = SpaceHandlingConfig::Encode.process_filename("file with spaces.txt");
|
||||
assert_eq!("file%20with%20spaces.txt", encoded_filename);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tokens() -> Result<(), ConfigError> {
|
||||
let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.toml");
|
||||
env::set_var("AUTH_TOKEN", "env_auth");
|
||||
env::set_var("DELETE_TOKEN", "env_delete");
|
||||
let mut config = Config::parse(&config_path)?;
|
||||
// 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(HashSet::from([
|
||||
"env_auth".to_string(),
|
||||
"may_the_force_be_with_you".to_string()
|
||||
])),
|
||||
config.get_tokens(TokenType::Auth)
|
||||
);
|
||||
assert_eq!(
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,21 +55,21 @@ impl Directory {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
|
||||
#[test]
|
||||
fn test_file_checksum() -> Result<(), ActixError> {
|
||||
assert_eq!(
|
||||
"rustypaste_logo.png",
|
||||
Some(OsString::from("rustypaste_logo.png").as_ref()),
|
||||
Directory::try_from(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("img")
|
||||
.as_path()
|
||||
)?
|
||||
.get_file("2073f6f567dcba3b468c568d29cf8ed2e9d3f0f7305b9ab1b5a22861f5922e61")
|
||||
.unwrap()
|
||||
.expect("cannot get file with checksum")
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
use crate::util;
|
||||
use actix_web::http::header::{
|
||||
ContentDisposition as ActixContentDisposition, DispositionParam, DispositionType,
|
||||
ContentDisposition as ActixContentDisposition, DispositionParam, DispositionType, HeaderMap,
|
||||
};
|
||||
use actix_web::http::HeaderMap;
|
||||
use actix_web::{error, Error as ActixError};
|
||||
use std::convert::TryFrom;
|
||||
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) -> Result<Option<u128>, ActixError> {
|
||||
if let Some(expire_time) = headers.get(EXPIRE).map(|v| v.to_str().ok()).flatten() {
|
||||
let timestamp = util::get_system_time()?;
|
||||
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()) {
|
||||
let expire_time =
|
||||
humantime::parse_duration(expire_time).map_err(error::ErrorInternalServerError)?;
|
||||
Ok(timestamp.checked_add(expire_time).map(|t| t.as_millis()))
|
||||
Ok(time.checked_add(expire_time).map(|t| t.as_millis()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
@ -30,12 +39,10 @@ pub struct ContentDisposition {
|
|||
inner: ActixContentDisposition,
|
||||
}
|
||||
|
||||
impl TryFrom<Option<ActixContentDisposition>> for ContentDisposition {
|
||||
type Error = ActixError;
|
||||
fn try_from(content_disposition: Option<ActixContentDisposition>) -> Result<Self, Self::Error> {
|
||||
match content_disposition {
|
||||
Some(inner) => Ok(Self { inner }),
|
||||
None => Err(error::ErrorBadRequest("content disposition does not exist")),
|
||||
impl From<ActixContentDisposition> for ContentDisposition {
|
||||
fn from(content_disposition: ActixContentDisposition) -> Self {
|
||||
Self {
|
||||
inner: content_disposition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,8 +64,7 @@ impl ContentDisposition {
|
|||
.parameters
|
||||
.iter()
|
||||
.find(|param| param.is_filename())
|
||||
.map(|param| param.as_filename())
|
||||
.flatten()
|
||||
.and_then(|param| param.as_filename())
|
||||
.filter(|file_name| !file_name.is_empty())
|
||||
.ok_or_else(|| error::ErrorBadRequest("file data not present"))
|
||||
}
|
||||
|
@ -67,31 +73,29 @@ impl ContentDisposition {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use actix_web::http::{HeaderName, HeaderValue};
|
||||
use crate::util;
|
||||
use actix_web::http::header::{HeaderName, HeaderValue};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_content_disposition() -> Result<(), ActixError> {
|
||||
assert!(ContentDisposition::try_from(None).is_err());
|
||||
|
||||
let actix_content_disposition = Some(ActixContentDisposition {
|
||||
let actix_content_disposition = ActixContentDisposition {
|
||||
disposition: DispositionType::FormData,
|
||||
parameters: vec![
|
||||
DispositionParam::Name(String::from("file")),
|
||||
DispositionParam::Filename(String::from("x.txt")),
|
||||
],
|
||||
});
|
||||
let content_disposition = ContentDisposition::try_from(actix_content_disposition)?;
|
||||
};
|
||||
let content_disposition = ContentDisposition::from(actix_content_disposition);
|
||||
assert!(content_disposition.has_form_field("file"));
|
||||
assert!(!content_disposition.has_form_field("test"));
|
||||
assert_eq!("x.txt", content_disposition.get_file_name()?);
|
||||
|
||||
let actix_content_disposition = Some(ActixContentDisposition {
|
||||
let actix_content_disposition = ActixContentDisposition {
|
||||
disposition: DispositionType::Attachment,
|
||||
parameters: vec![DispositionParam::Name(String::from("file"))],
|
||||
});
|
||||
let content_disposition = ContentDisposition::try_from(actix_content_disposition)?;
|
||||
};
|
||||
let content_disposition = ContentDisposition::from(actix_content_disposition);
|
||||
assert!(!content_disposition.has_form_field("file"));
|
||||
assert!(content_disposition.get_file_name().is_err());
|
||||
Ok(())
|
||||
|
@ -104,7 +108,8 @@ mod tests {
|
|||
HeaderName::from_static(EXPIRE),
|
||||
HeaderValue::from_static("5ms"),
|
||||
);
|
||||
let expiry_time = parse_expiry_date(&headers)?.unwrap();
|
||||
let time = util::get_system_time()?;
|
||||
let expiry_time = parse_expiry_date(&headers, time)?.unwrap_or_default();
|
||||
assert!(expiry_time > util::get_system_time()?.as_millis());
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
assert!(expiry_time < util::get_system_time()?.as_millis());
|
||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -27,3 +27,19 @@ pub mod mime;
|
|||
|
||||
/// Helper functions.
|
||||
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";
|
||||
|
||||
/// Environment variable for setting the authentication token.
|
||||
pub const AUTH_TOKEN_ENV: &str = "AUTH_TOKEN";
|
||||
|
||||
/// Environment variable for setting the deletion token.
|
||||
pub const DELETE_TOKEN_ENV: &str = "DELETE_TOKEN";
|
||||
|
|
227
src/main.rs
227
src/main.rs
|
@ -1,72 +1,193 @@
|
|||
use actix_web::client::ClientBuilder;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::web::Data;
|
||||
#[cfg(not(feature = "shuttle"))]
|
||||
use actix_web::{App, HttpServer};
|
||||
use hotwatch::{Event, Hotwatch};
|
||||
use rustypaste::config::Config;
|
||||
use awc::ClientBuilder;
|
||||
use hotwatch::notify::event::ModifyKind;
|
||||
use hotwatch::{Event, EventKind, Hotwatch};
|
||||
use rustypaste::config::{Config, ServerConfig};
|
||||
use rustypaste::middleware::ContentLengthLimiter;
|
||||
use rustypaste::paste::PasteType;
|
||||
use rustypaste::server;
|
||||
use rustypaste::util;
|
||||
use rustypaste::CONFIG_ENV;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Result as IoResult;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
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
|
||||
/// * initializes the logger
|
||||
/// * creates the necessary directories
|
||||
/// * spawns the threads
|
||||
fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig, Hotwatch)> {
|
||||
// Load the .env file.
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> IoResult<()> {
|
||||
// Initialize logger.
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
#[cfg(not(feature = "shuttle"))]
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
// Parse configuration.
|
||||
dotenv::dotenv().ok();
|
||||
let config_path =
|
||||
PathBuf::from(env::var("CONFIG").unwrap_or_else(|_| String::from("config.toml")));
|
||||
let config = Arc::new(Mutex::new(
|
||||
Config::parse(&config_path).expect("failed to parse config"),
|
||||
));
|
||||
let cloned_config = Arc::clone(&config);
|
||||
let server_config = config.lock().expect("cannot acquire config").server.clone();
|
||||
let config_path = match env::var(CONFIG_ENV).ok() {
|
||||
Some(path) => {
|
||||
env::remove_var(CONFIG_ENV);
|
||||
PathBuf::from(path)
|
||||
}
|
||||
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");
|
||||
trace!("{:#?}", config);
|
||||
config.warn_deprecation();
|
||||
let server_config = config.server.clone();
|
||||
let paste_config = RwLock::new(config.paste.clone());
|
||||
let (config_sender, config_receiver) = mpsc::channel::<Config>();
|
||||
|
||||
// Create necessary directories.
|
||||
fs::create_dir_all(&server_config.upload_path)?;
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::create_dir_all(paste_type.get_path(&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)?)?;
|
||||
}
|
||||
|
||||
// Set up a watcher for the configuration file changes.
|
||||
let mut hotwatch = Hotwatch::new_with_custom_delay(Duration::from_secs(1))
|
||||
.expect("failed to initialize configuration file watcher");
|
||||
let mut hotwatch = Hotwatch::new_with_custom_delay(
|
||||
config
|
||||
.settings
|
||||
.as_ref()
|
||||
.map(|v| v.refresh_rate)
|
||||
.unwrap_or_else(|| Duration::from_secs(1)),
|
||||
)
|
||||
.expect("failed to initialize configuration file watcher");
|
||||
|
||||
// Hot-reload the configuration file.
|
||||
hotwatch
|
||||
.watch(&config_path, move |event: Event| {
|
||||
if let Event::Write(path) = event {
|
||||
match Config::parse(&path) {
|
||||
Ok(config) => {
|
||||
*cloned_config.lock().expect("cannot acquire config") = config;
|
||||
log::info!("Configuration has been updated.");
|
||||
let config = Data::new(RwLock::new(config));
|
||||
let cloned_config = Data::clone(&config);
|
||||
let config_watcher = move |event: Event| {
|
||||
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
|
||||
(event.kind, event.paths.first())
|
||||
{
|
||||
match Config::parse(path) {
|
||||
Ok(config) => match cloned_config.write() {
|
||||
Ok(mut cloned_config) => {
|
||||
*cloned_config = config.clone();
|
||||
info!("Configuration has been updated.");
|
||||
if let Err(e) = config_sender.send(config) {
|
||||
error!("Failed to send config for the cleanup routine: {}", e)
|
||||
}
|
||||
cloned_config.warn_deprecation();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to update configuration: {}", e);
|
||||
error!("Failed to acquire config: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to update config: {}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("failed to watch {:?}", config_path));
|
||||
}
|
||||
};
|
||||
hotwatch
|
||||
.watch(&config_path, config_watcher)
|
||||
.unwrap_or_else(|_| panic!("failed to watch {config_path:?}"));
|
||||
|
||||
// Create a HTTP server.
|
||||
// Create a thread for cleaning up expired files.
|
||||
let upload_path = server_config.upload_path.clone();
|
||||
thread::spawn(move || loop {
|
||||
let mut enabled = false;
|
||||
if let Some(ref cleanup_config) = paste_config
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|v| v.delete_expired_files.clone())
|
||||
{
|
||||
if cleanup_config.enabled {
|
||||
debug!("Running cleanup...");
|
||||
for file in util::get_expired_files(&upload_path) {
|
||||
match fs::remove_file(&file) {
|
||||
Ok(()) => info!("Removed expired file: {:?}", file),
|
||||
Err(e) => error!("Cannot remove expired file: {}", e),
|
||||
}
|
||||
}
|
||||
thread::sleep(cleanup_config.interval);
|
||||
}
|
||||
enabled = cleanup_config.enabled;
|
||||
}
|
||||
if let Some(new_config) = if enabled {
|
||||
config_receiver.try_recv().ok()
|
||||
} else {
|
||||
config_receiver.recv().ok()
|
||||
} {
|
||||
match paste_config.write() {
|
||||
Ok(mut paste_config) => {
|
||||
*paste_config = new_config.paste;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to update config for the cleanup routine: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((config, server_config, hotwatch))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "shuttle"))]
|
||||
#[actix_web::main]
|
||||
async fn main() -> IoResult<()> {
|
||||
// Set up the application.
|
||||
let (config, server_config, _hotwatch) = setup(&PathBuf::new())?;
|
||||
|
||||
// Create an HTTP server.
|
||||
let mut http_server = HttpServer::new(move || {
|
||||
let http_client = ClientBuilder::default()
|
||||
.timeout(Duration::from_secs(30))
|
||||
let http_client = ClientBuilder::new()
|
||||
.timeout(
|
||||
server_config
|
||||
.timeout
|
||||
.unwrap_or_else(|| Duration::from_secs(30)),
|
||||
)
|
||||
.disable_redirects()
|
||||
.finish();
|
||||
App::new()
|
||||
.data(Arc::clone(&config))
|
||||
.data(http_client)
|
||||
.wrap(Logger::default())
|
||||
.app_data(Data::clone(&config))
|
||||
.app_data(Data::new(http_client))
|
||||
.wrap(Logger::new(
|
||||
"%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
|
||||
))
|
||||
.wrap(ContentLengthLimiter::new(server_config.max_content_length))
|
||||
.configure(server::configure_routes)
|
||||
})
|
||||
.bind(server_config.address)?;
|
||||
.bind(&server_config.address)?;
|
||||
|
||||
// Set worker count for the server.
|
||||
if let Some(workers) = server_config.workers {
|
||||
|
@ -74,5 +195,37 @@ async fn main() -> IoResult<()> {
|
|||
}
|
||||
|
||||
// Run the server.
|
||||
info!("Server is running at {}", server_config.address);
|
||||
http_server.run().await
|
||||
}
|
||||
|
||||
#[cfg(feature = "shuttle")]
|
||||
#[shuttle_runtime::main]
|
||||
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
|
||||
// Set up the application.
|
||||
let (config, server_config, _hotwatch) = setup(Path::new("shuttle"))?;
|
||||
|
||||
// Create the service.
|
||||
let service_config = move |cfg: &mut ServiceConfig| {
|
||||
let http_client = ClientBuilder::new()
|
||||
.timeout(
|
||||
server_config
|
||||
.timeout
|
||||
.unwrap_or_else(|| Duration::from_secs(30)),
|
||||
)
|
||||
.disable_redirects()
|
||||
.finish();
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
.app_data(Data::clone(&config))
|
||||
.app_data(Data::new(http_client))
|
||||
.wrap(Logger::new(
|
||||
"%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
|
||||
))
|
||||
.wrap(ContentLengthLimiter::new(server_config.max_content_length))
|
||||
.configure(server::configure_routes),
|
||||
);
|
||||
};
|
||||
|
||||
Ok(service_config.into())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
|
||||
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},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// Content length limiter middleware.
|
||||
#[derive(Debug)]
|
||||
pub struct ContentLengthLimiter {
|
||||
// Maximum amount of bytes to allow.
|
||||
max_bytes: Byte,
|
||||
}
|
||||
|
||||
impl ContentLengthLimiter {
|
||||
/// Constructs a new instance.
|
||||
pub fn new(max_bytes: Byte) -> Self {
|
||||
Self { max_bytes }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for ContentLengthLimiter
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Error = Error;
|
||||
type Transform = ContentLengthLimiterMiddleware<S>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(ContentLengthLimiterMiddleware {
|
||||
service: Rc::new(service),
|
||||
max_bytes: self.max_bytes,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Content length limiter middleware implementation.
|
||||
#[derive(Debug)]
|
||||
pub struct ContentLengthLimiterMiddleware<S> {
|
||||
service: Rc<S>,
|
||||
max_bytes: Byte,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for ContentLengthLimiterMiddleware<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<EitherBody<B>>;
|
||||
type Error = Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
forward_ready!(service);
|
||||
fn call(&self, mut request: ServiceRequest) -> Self::Future {
|
||||
let service = Rc::clone(&self.service);
|
||||
if let Some(content_length) = request
|
||||
.headers()
|
||||
.get(CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<Byte>().ok())
|
||||
{
|
||||
if content_length > self.max_bytes {
|
||||
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();
|
||||
while let Ok(Some(_)) = payload.try_next().await {}
|
||||
Ok(request.into_response(
|
||||
HttpResponseBuilder::new(StatusCode::PAYLOAD_TOO_LARGE)
|
||||
.body("upload limit exceeded")
|
||||
.map_into_right_body(),
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
Box::pin(async move {
|
||||
service
|
||||
.call(request)
|
||||
.await
|
||||
.map(ServiceResponse::map_into_left_body)
|
||||
})
|
||||
}
|
||||
}
|
16
src/mime.rs
16
src/mime.rs
|
@ -25,8 +25,7 @@ pub fn get_mime_type(
|
|||
let path = PathBuf::from(&file_name);
|
||||
let mut mime_type = file_extension_to_mime(
|
||||
path.extension()
|
||||
.map(|v| v.to_str())
|
||||
.flatten()
|
||||
.and_then(|v| v.to_str())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
for matcher in mime_matchers {
|
||||
|
@ -48,7 +47,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_match_mime_type() {
|
||||
fn test_match_mime_type() -> Result<(), FromStrError> {
|
||||
assert_eq!(
|
||||
mime::TEXT_PLAIN,
|
||||
get_mime_type(
|
||||
|
@ -57,8 +56,7 @@ mod tests {
|
|||
regex: Regex::new("^.*\\.test$").ok(),
|
||||
}],
|
||||
String::from("mime.test")
|
||||
)
|
||||
.unwrap()
|
||||
)?
|
||||
);
|
||||
assert_eq!(
|
||||
mime::IMAGE_PNG,
|
||||
|
@ -68,16 +66,16 @@ mod tests {
|
|||
regex: Regex::new("^.*\\.PNG$").ok(),
|
||||
}],
|
||||
String::from("image.PNG")
|
||||
)
|
||||
.unwrap()
|
||||
)?
|
||||
);
|
||||
assert_eq!(
|
||||
mime::APPLICATION_PDF,
|
||||
get_mime_type(&[], String::from("book.pdf")).unwrap()
|
||||
get_mime_type(&[], String::from("book.pdf"))?
|
||||
);
|
||||
assert_eq!(
|
||||
mime::APPLICATION_OCTET_STREAM,
|
||||
get_mime_type(&[], String::from("x.unknown")).unwrap()
|
||||
get_mime_type(&[], String::from("x.unknown"))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
319
src/paste.rs
319
src/paste.rs
|
@ -2,17 +2,18 @@ use crate::config::Config;
|
|||
use crate::file::Directory;
|
||||
use crate::header::ContentDisposition;
|
||||
use crate::util;
|
||||
use actix_web::client::Client;
|
||||
use actix_web::{error, Error};
|
||||
use awc::Client;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
use std::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
/// Type of the data to store.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PasteType {
|
||||
/// Any type of file.
|
||||
File,
|
||||
|
@ -22,6 +23,8 @@ pub enum PasteType {
|
|||
Oneshot,
|
||||
/// A file that only contains an URL.
|
||||
Url,
|
||||
/// A oneshot url.
|
||||
OneshotUrl,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ContentDisposition> for PasteType {
|
||||
|
@ -33,6 +36,8 @@ impl<'a> TryFrom<&'a ContentDisposition> for PasteType {
|
|||
Ok(Self::RemoteFile)
|
||||
} else if content_disposition.has_form_field("oneshot") {
|
||||
Ok(Self::Oneshot)
|
||||
} else if content_disposition.has_form_field("oneshot_url") {
|
||||
Ok(Self::OneshotUrl)
|
||||
} else if content_disposition.has_form_field("url") {
|
||||
Ok(Self::Url)
|
||||
} else {
|
||||
|
@ -48,16 +53,17 @@ impl PasteType {
|
|||
Self::File | Self::RemoteFile => String::new(),
|
||||
Self::Oneshot => String::from("oneshot"),
|
||||
Self::Url => String::from("url"),
|
||||
Self::OneshotUrl => String::from("oneshot_url"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,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
|
||||
|
@ -89,57 +96,92 @@ 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",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let file_name = match PathBuf::from(file_name)
|
||||
let mut file_name = match PathBuf::from(file_name)
|
||||
.file_name()
|
||||
.map(|v| v.to_str())
|
||||
.flatten()
|
||||
.and_then(|v| v.to_str())
|
||||
{
|
||||
Some("-") => String::from("stdin"),
|
||||
Some(".") => String::from("file"),
|
||||
Some(v) => v.to_string(),
|
||||
None => String::from("file"),
|
||||
};
|
||||
let mut path = self
|
||||
.type_
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(file_name);
|
||||
match path.clone().extension() {
|
||||
Some(extension) => {
|
||||
if let Some(file_name) = config.paste.random_url.generate() {
|
||||
path.set_file_name(file_name);
|
||||
path.set_extension(extension);
|
||||
if let Some(handle_spaces_config) = config.server.handle_spaces {
|
||||
file_name = handle_spaces_config.process_filename(&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;
|
||||
let mut file_name = match parts[0] {
|
||||
"" => {
|
||||
// Index shifts one to the right in the array for the rest of the string (the extension)
|
||||
dotfile = true;
|
||||
lower_bound = 2;
|
||||
// If the first array element is empty, it means the file started with a dot (e.g.: .foo)
|
||||
format!(".{}", parts[1])
|
||||
}
|
||||
_ => parts[0].to_string(),
|
||||
};
|
||||
let mut extension = if parts.len() > lower_bound {
|
||||
// To get the rest (the extension), we have to remove the first element of the array, which is the filename
|
||||
parts.remove(0);
|
||||
if dotfile {
|
||||
// If the filename starts with a dot, we have to remove another element, because the first element was empty
|
||||
parts.remove(0);
|
||||
}
|
||||
parts.join(".")
|
||||
} else {
|
||||
file_type
|
||||
.map(|t| t.extension())
|
||||
.unwrap_or(&config.paste.default_extension)
|
||||
.to_string()
|
||||
};
|
||||
if let Some(random_url) = &config.paste.random_url {
|
||||
if let Some(random_text) = random_url.generate() {
|
||||
if let Some(suffix_mode) = random_url.suffix_mode {
|
||||
if suffix_mode {
|
||||
extension = format!("{}.{}", random_text, extension);
|
||||
} else {
|
||||
file_name = random_text;
|
||||
}
|
||||
} else {
|
||||
file_name = random_text;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(file_name) = config.paste.random_url.generate() {
|
||||
path.set_file_name(file_name);
|
||||
}
|
||||
path.set_extension(
|
||||
file_type
|
||||
.map(|t| t.extension())
|
||||
.unwrap_or(&config.paste.default_extension),
|
||||
);
|
||||
}
|
||||
}
|
||||
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));
|
||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||
}
|
||||
let mut buffer = File::create(&path)?;
|
||||
buffer.write_all(&self.data)?;
|
||||
|
@ -158,7 +200,7 @@ impl Paste {
|
|||
&mut self,
|
||||
expiry_date: Option<u128>,
|
||||
client: &Client,
|
||||
config: &Config,
|
||||
config: &RwLock<Config>,
|
||||
) -> Result<String, Error> {
|
||||
let data = str::from_utf8(&self.data).map_err(error::ErrorBadRequest)?;
|
||||
let url = Url::parse(data).map_err(error::ErrorBadRequest)?;
|
||||
|
@ -167,14 +209,27 @@ impl Paste {
|
|||
.and_then(|segments| segments.last())
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.unwrap_or("file");
|
||||
let mut response = client.get(url.as_str()).send().await?;
|
||||
let mut response = client
|
||||
.get(url.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
let payload_limit = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
.server
|
||||
.max_content_length
|
||||
.get_bytes()
|
||||
.try_into()
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
let bytes = response.body().limit(payload_limit).await?.to_vec();
|
||||
let bytes = response
|
||||
.body()
|
||||
.limit(payload_limit)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.to_vec();
|
||||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let bytes_checksum = util::sha256_digest(&*bytes)?;
|
||||
self.data = bytes;
|
||||
if !config.paste.duplicate_files.unwrap_or(true) && expiry_date.is_none() {
|
||||
|
@ -189,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.
|
||||
|
@ -198,20 +253,21 @@ impl Paste {
|
|||
/// - If [`random_url.enabled`] is `true`, file name is set to a pet name or random string.
|
||||
///
|
||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||
#[allow(deprecated)]
|
||||
pub fn store_url(&self, expiry_date: Option<u128>, config: &Config) -> IoResult<String> {
|
||||
let data = str::from_utf8(&self.data)
|
||||
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
||||
let url = Url::parse(data).map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
|
||||
let file_name = config
|
||||
.paste
|
||||
.random_url
|
||||
.generate()
|
||||
.unwrap_or_else(|| PasteType::Url.get_dir());
|
||||
let mut path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(&file_name);
|
||||
let mut file_name = self.type_.get_dir();
|
||||
if let Some(random_url) = &config.paste.random_url {
|
||||
if let Some(random_text) = random_url.generate() {
|
||||
file_name = random_text;
|
||||
}
|
||||
}
|
||||
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));
|
||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||
}
|
||||
fs::write(&path, url.to_string())?;
|
||||
Ok(file_name)
|
||||
|
@ -223,78 +279,183 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::random::{RandomURLConfig, RandomURLType};
|
||||
use crate::util;
|
||||
use actix_web::client::Client;
|
||||
use actix_web::web::Data;
|
||||
use awc::ClientBuilder;
|
||||
use byte_unit::Byte;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[actix_rt::test]
|
||||
#[allow(deprecated)]
|
||||
async fn test_paste_data() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
config.paste.random_url = RandomURLConfig {
|
||||
enabled: true,
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
enabled: Some(true),
|
||||
words: Some(3),
|
||||
separator: Some(String::from("_")),
|
||||
type_: RandomURLType::PetName,
|
||||
..RandomURLConfig::default()
|
||||
};
|
||||
});
|
||||
let paste = Paste {
|
||||
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"),
|
||||
PathBuf::from(&file_name)
|
||||
.extension()
|
||||
.map(|v| v.to_str())
|
||||
.flatten()
|
||||
.and_then(|v| v.to_str())
|
||||
);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
config.paste.default_extension = String::from("bin");
|
||||
config.paste.random_url.enabled = false;
|
||||
config.paste.random_url = RandomURLConfig {
|
||||
enabled: true,
|
||||
length: Some(10),
|
||||
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("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."));
|
||||
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(".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."));
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
type_: RandomURLType::Alphanumeric,
|
||||
suffix_mode: Some(false),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let paste = Paste {
|
||||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
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)?;
|
||||
|
||||
config.paste.default_extension = String::from("txt");
|
||||
config.paste.random_url = None;
|
||||
let paste = Paste {
|
||||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("random", 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)?;
|
||||
|
||||
config.paste.default_extension = String::from("bin");
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(10),
|
||||
type_: RandomURLType::Alphanumeric,
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let paste = Paste {
|
||||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("random", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(
|
||||
Some("bin"),
|
||||
PathBuf::from(&file_name)
|
||||
.extension()
|
||||
.map(|v| v.to_str())
|
||||
.flatten()
|
||||
.and_then(|v| v.to_str())
|
||||
);
|
||||
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.enabled = false;
|
||||
config.paste.random_url = None;
|
||||
let paste = Paste {
|
||||
data: vec![116, 101, 115, 116],
|
||||
type_: PasteType::Oneshot,
|
||||
};
|
||||
let expiry_date = util::get_system_time().unwrap().as_millis() + 100;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
|
||||
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
|
||||
let file_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(format!("{}.{}", file_name, expiry_date));
|
||||
.expect("Bad upload path")
|
||||
.join(format!("{file_name}.{expiry_date}"));
|
||||
assert_eq!("test", fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
|
||||
config.paste.random_url.enabled = true;
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
enabled: Some(true),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let url = String::from("https://orhun.dev/");
|
||||
let paste = Paste {
|
||||
data: url.as_bytes().to_vec(),
|
||||
|
@ -303,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)?;
|
||||
|
@ -314,25 +476,36 @@ mod tests {
|
|||
};
|
||||
assert!(paste.store_url(None, &config).is_err());
|
||||
|
||||
config.server.max_content_length = Byte::from_str("30k").unwrap();
|
||||
config.server.max_content_length = Byte::from_str("30k").expect("cannot parse byte");
|
||||
let url = String::from("https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg");
|
||||
let mut paste = Paste {
|
||||
data: url.as_bytes().to_vec(),
|
||||
type_: PasteType::RemoteFile,
|
||||
};
|
||||
let client_data = Data::new(Client::default());
|
||||
let file_name = paste.store_remote_file(None, &client_data, &config).await?;
|
||||
let client_data = Data::new(
|
||||
ClientBuilder::new()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.finish(),
|
||||
);
|
||||
let file_name = paste
|
||||
.store_remote_file(None, &client_data, &RwLock::new(config.clone()))
|
||||
.await?;
|
||||
let file_path = PasteType::RemoteFile
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(&file_name);
|
||||
.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(())
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue