Compare commits
50 Commits
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | 9ceb0d4c21 | |
Orhun Parmaksız | 7afea1721d | |
Orhun Parmaksız | c04c39b975 | |
Orhun Parmaksız | 32f95ef938 | |
dependabot[bot] | 15e3619479 | |
Orhun Parmaksız | 2037530078 | |
Orhun Parmaksız | 44c07a3eb6 | |
Orhun Parmaksız | 64d783bd0d | |
Orhun Parmaksız | 1fd561f869 | |
dependabot[bot] | c6d6da6296 | |
Orhun Parmaksız | 54e2ddc91a | |
dependabot[bot] | 8e505c0da8 | |
Orhun Parmaksız | 0f0ba72305 | |
dependabot[bot] | 77e97573ef | |
dependabot[bot] | 40f5d909ca | |
dependabot[bot] | 567480a21e | |
dependabot[bot] | e8c342af46 | |
dependabot[bot] | 1442771a57 | |
Orhun Parmaksız | dadd88c240 | |
Orhun Parmaksız | e3b00453d9 | |
Helmut K. C. Tessarek | b2acb71d0d | |
Orhun Parmaksız | db24d911f6 | |
Helmut K. C. Tessarek | 4774de6652 | |
Helmut K. C. Tessarek | 4987cfe5e5 | |
Orhun Parmaksız | c60a614258 | |
Helmut K. C. Tessarek | fa5105deab | |
Jake Howard | dae00c42b5 | |
Orhun Parmaksız | 12f0e8f3a7 | |
Helmut K. C. Tessarek | db971e6434 | |
Helmut K. C. Tessarek | 8e6393c6f4 | |
Helmut K. C. Tessarek | e06c18279e | |
Helmut K. C. Tessarek | 48a3626c7d | |
dependabot[bot] | 8de2450931 | |
dependabot[bot] | 2003abe8bd | |
dependabot[bot] | 4703b26194 | |
dependabot[bot] | e383b6f426 | |
dependabot[bot] | fa7304a5d0 | |
dependabot[bot] | 90c0477edb | |
dependabot[bot] | 7f8428c75d | |
dependabot[bot] | 25a013266c | |
Orhun Parmaksız | 274bbd3307 | |
dependabot[bot] | c9ba166865 | |
dependabot[bot] | 4af7970f48 | |
dependabot[bot] | 87a0ba21ed | |
Orhun Parmaksız | c8dd41549d | |
Helmut K. C. Tessarek | ec9b55f3e2 | |
dependabot[bot] | f905648866 | |
dependabot[bot] | cf21df8a7c | |
dependabot[bot] | a806af9a41 | |
Orhun Parmaksız | f3962485df |
|
@ -1,3 +1,3 @@
|
|||
github: orhun
|
||||
patreon: orhunp
|
||||
custom: ["https://www.buymeacoffee.com/orhun"]
|
||||
buy_me_a_coffee: orhun
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
pull_request_rules:
|
||||
- name: Automatic merge for Dependabot pull requests
|
||||
conditions:
|
||||
- author=dependabot[bot]
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
|
@ -7,17 +7,11 @@ on:
|
|||
jobs:
|
||||
audit:
|
||||
name: Audit
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: Run cargo-audit
|
||||
uses: actions-rs/audit-check@v1
|
||||
uses: rustsec/audit-check@v1.4.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -30,19 +30,22 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup cargo-tarpaulin
|
||||
uses: taiki-e/install-action@cargo-tarpaulin
|
||||
- name: Run tests
|
||||
run: cargo tarpaulin --out xml --verbose -- --test-threads 1
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Generate code coverage
|
||||
run: cargo llvm-cov --lcov --output-path lcov.info -- --test-threads 1
|
||||
env:
|
||||
OUT_DIR: target
|
||||
- name: Upload reports to codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
file: cobertura.xml
|
||||
file: lcov.info
|
||||
flags: unit-tests
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
fixtures:
|
||||
strategy:
|
||||
|
|
|
@ -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@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
@ -68,7 +73,7 @@ jobs:
|
|||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
|
|
@ -6,9 +6,6 @@ on:
|
|||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
@ -24,12 +21,6 @@ jobs:
|
|||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
manifest:
|
||||
- 'Cargo.**'
|
||||
- name: Install cargo-binstall
|
||||
uses: taiki-e/install-action@cargo-binstall
|
||||
- name: Install cargo-shuttle
|
||||
|
@ -40,11 +31,7 @@ jobs:
|
|||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
- name: Deploy
|
||||
if: |
|
||||
${{ startsWith(github.event.ref, 'refs/tags/v') ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
steps.changes.outputs.manifest == 'true'
|
||||
}}
|
||||
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
|
||||
|
|
59
CHANGELOG.md
59
CHANGELOG.md
|
@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.15.0] - 2024-03-27
|
||||
|
||||
### Added
|
||||
|
||||
- Allow to override filename when using `random_url` by @tessus in [#233](https://github.com/orhun/rustypaste/pull/233)
|
||||
|
||||
Now you can use the `filename` header to override the name of the uploaded file.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
curl -F "file=@x.txt" -H "filename:override.txt" http://localhost:8000
|
||||
```
|
||||
|
||||
Even if `random_url` is set, the filename will be override.txt
|
||||
|
||||
[`rustypaste-cli`](https://github.com/orhun/rustypaste-cli) also has a new argument for overriding the file name:
|
||||
|
||||
```sh
|
||||
rpaste -n filename-on-server.txt awesome.txt
|
||||
```
|
||||
|
||||
- Use more specific HTTP status codes by @tessus in [#262](https://github.com/orhun/rustypaste/pull/262)
|
||||
|
||||
`rustypaste` now returns more appropriate status codes in the following 2 cases (instead of a generic 500 code):
|
||||
|
||||
- If the mime type is on the blacklist: `UnsupportedMediaType` (415)
|
||||
- If the file already exists: `Conflict` (409)
|
||||
|
||||
### Changed
|
||||
|
||||
- Do path joins more safely by @RealOrangeOne in [#247](https://github.com/orhun/rustypaste/pull/247)
|
||||
- Gracefully exit when there is no config file found by @orhun
|
||||
- Switch to cargo-llvm-cov for code coverage by @orhun in [#260](https://github.com/orhun/rustypaste/pull/260)
|
||||
- Replace unmaintained action by @tessus in [#266](https://github.com/orhun/rustypaste/pull/266)
|
||||
- Set up mergify by @orhun
|
||||
- Apply clippy suggestions by @orhun
|
||||
- Update funding options by @orhun
|
||||
- Update the copyright years by @orhun
|
||||
- Bump dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve logging for deleted file by @tessus in [#235](https://github.com/orhun/rustypaste/pull/235)
|
||||
- Fix deployment by @tessus in [#236](https://github.com/orhun/rustypaste/pull/236)
|
||||
- Return the correct file on multiple files with same name by @tessus in [#234](https://github.com/orhun/rustypaste/pull/234)
|
||||
- Update the hash of the example file by @tessus in [#254](https://github.com/orhun/rustypaste/pull/254)
|
||||
- Error on upload with the same filename by @tessus in [#258](https://github.com/orhun/rustypaste/pull/258)
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @RealOrangeOne made their first contribution in [#247](https://github.com/orhun/rustypaste/pull/247)
|
||||
|
||||
## [0.14.4] - 2023-12-20
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove excessive warning messages when auth tokens are not found (#210)
|
||||
|
||||
## [0.14.3] - 2023-12-12
|
||||
|
||||
### Changed
|
||||
|
|
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypaste"
|
||||
version = "0.14.3"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
description = "A minimal file upload/pastebin service"
|
||||
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
|
||||
|
@ -19,15 +19,15 @@ rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
|
|||
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.4.0" }
|
||||
actix-web = { version = "4.5.1" }
|
||||
actix-web-grants = { version = "4.0.3" }
|
||||
actix-multipart = "0.6.1"
|
||||
actix-files = "0.6.2"
|
||||
shuttle-actix-web = { version = "0.35.0", optional = true }
|
||||
shuttle-runtime = { version = "0.35.0", optional = true }
|
||||
awc = { version = "3.2.0" }
|
||||
serde = "1.0.193"
|
||||
futures-util = "0.3.29"
|
||||
actix-files = "0.6.5"
|
||||
shuttle-actix-web = { version = "0.42.0", optional = true }
|
||||
shuttle-runtime = { version = "0.42.0", optional = true }
|
||||
awc = { version = "3.4.0" }
|
||||
serde = "1.0.197"
|
||||
futures-util = "0.3.30"
|
||||
petname = { version = "1.1.3", default-features = false, features = [
|
||||
"std_rng",
|
||||
"default_dictionary",
|
||||
|
@ -36,26 +36,27 @@ rand = "0.8.5"
|
|||
dotenvy = "0.15.7"
|
||||
url = "2.5.0"
|
||||
mime = "0.3.17"
|
||||
regex = "1.10.2"
|
||||
regex = "1.10.4"
|
||||
serde_regex = "1.1.0"
|
||||
lazy-regex = "3.1.0"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1.1"
|
||||
glob = "0.3.1"
|
||||
ring = "0.17.7"
|
||||
ring = "0.17.8"
|
||||
hotwatch = "0.5.0"
|
||||
tokio = { version = "1.35.0", optional = true }
|
||||
tokio = { version = "1.37.0", optional = true }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
uts2ts = "0.4.1"
|
||||
path-clean = "1.0.1"
|
||||
|
||||
[dependencies.config]
|
||||
version = "0.13.4"
|
||||
version = "0.14.0"
|
||||
default-features = false
|
||||
features = ["toml", "yaml"]
|
||||
|
||||
[dependencies.byte-unit]
|
||||
version = "5.0.3"
|
||||
version = "5.1.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.infer]
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-2023 Orhun Parmaksız
|
||||
Copyright (c) 2021-2024 Orhun Parmaksız
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
20
README.md
20
README.md
|
@ -20,10 +20,6 @@ $ curl https://paste.site.com/safe-toad.txt
|
|||
some text
|
||||
```
|
||||
|
||||
The public instance is available at [https://rustypaste.shuttleapp.rs](https://rustypaste.shuttleapp.rs) 🚀
|
||||
|
||||
Here you can read the blog post about how it is deployed on Shuttle: [https://blog.orhun.dev/blazingly-fast-file-sharing](https://blog.orhun.dev/blazingly-fast-file-sharing)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
@ -52,6 +48,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
|||
- [Paste file from remote URL](#paste-file-from-remote-url)
|
||||
- [Cleaning up expired files](#cleaning-up-expired-files)
|
||||
- [Delete file from server](#delete-file-from-server)
|
||||
- [Override the filename when using `random_url`](#override-the-filename-when-using-random_url)
|
||||
- [Server](#server)
|
||||
- [List endpoint](#list-endpoint)
|
||||
- [HTML Form](#html-form)
|
||||
|
@ -98,6 +95,13 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
|
|||
|
||||
## Installation
|
||||
|
||||
<details>
|
||||
<summary>Packaging status</summary>
|
||||
|
||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustypaste.svg)](https://repology.org/project/rustypaste/versions)
|
||||
|
||||
</details>
|
||||
|
||||
### From crates.io
|
||||
|
||||
```sh
|
||||
|
@ -254,6 +258,14 @@ $ 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:
|
||||
|
|
|
@ -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,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
|
||||
}
|
|
@ -11,6 +11,7 @@ setup() {
|
|||
run_test() {
|
||||
test "this file type is not permitted" = "$(curl -s -F "file=@file.html" localhost:8000)"
|
||||
test "this file type is not permitted" = "$(curl -s -F "file=@file.xml" localhost:8000)"
|
||||
test "415" = "$(curl -s -F "file=@file.xml" -w "%{response_code}" -o /dev/null localhost:8000)"
|
||||
file_url=$(curl -s -F "file=@file.txt" localhost:8000)
|
||||
test "$content" = "$(curl -s $file_url)"
|
||||
}
|
||||
|
|
|
@ -33,11 +33,10 @@ pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<Token
|
|||
}
|
||||
} else if token_type == TokenType::Auth {
|
||||
// not configured `auth_tokens` means that the user is allowed to access the endpoints
|
||||
warn!("auth_tokens not configured, allowing the request without auth header");
|
||||
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 endpoints is not served because there are no delete_tokens set");
|
||||
warn!("delete endpoint is not served because there are no delete_tokens set");
|
||||
Err(error::ErrorNotFound(""))?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ use std::time::Duration;
|
|||
/// Custom HTTP header for expiry dates.
|
||||
pub const EXPIRE: &str = "expire";
|
||||
|
||||
/// Custom HTTP header to override filename.
|
||||
const FILENAME: &str = "filename";
|
||||
|
||||
/// Parses the expiry date from the [`custom HTTP header`](EXPIRE).
|
||||
pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u128>, ActixError> {
|
||||
if let Some(expire_time) = headers.get(EXPIRE).and_then(|v| v.to_str().ok()) {
|
||||
|
@ -18,6 +21,15 @@ pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses the filename from the header.
|
||||
pub fn parse_header_filename(headers: &HeaderMap) -> Result<Option<String>, ActixError> {
|
||||
if let Some(file_name) = headers.get(FILENAME).and_then(|v| v.to_str().ok()) {
|
||||
Ok(Some(file_name.to_string()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for Actix content disposition header.
|
||||
///
|
||||
/// Aims to parse the file data from multipart body.
|
||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -61,6 +61,13 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
}
|
||||
None => config_folder.join("config.toml"),
|
||||
};
|
||||
if !config_path.exists() {
|
||||
error!(
|
||||
"{} is not found, please provide a configuration file.",
|
||||
config_path.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
let config = Config::parse(&config_path).expect("failed to parse config");
|
||||
trace!("{:#?}", config);
|
||||
config.warn_deprecation();
|
||||
|
@ -71,7 +78,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
// Create necessary directories.
|
||||
fs::create_dir_all(&server_config.upload_path)?;
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||
fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?;
|
||||
fs::create_dir_all(paste_type.get_path(&server_config.upload_path)?)?;
|
||||
}
|
||||
|
||||
// Set up a watcher for the configuration file changes.
|
||||
|
@ -89,7 +96,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
|
|||
let cloned_config = Data::clone(&config);
|
||||
let config_watcher = move |event: Event| {
|
||||
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
|
||||
(event.kind, event.paths.get(0))
|
||||
(event.kind, event.paths.first())
|
||||
{
|
||||
match Config::parse(path) {
|
||||
Ok(config) => match cloned_config.write() {
|
||||
|
|
110
src/paste.rs
110
src/paste.rs
|
@ -58,12 +58,12 @@ impl PasteType {
|
|||
}
|
||||
|
||||
/// Returns the given path with [`directory`](Self::get_dir) adjoined.
|
||||
pub fn get_path(&self, path: &Path) -> PathBuf {
|
||||
pub fn get_path(&self, path: &Path) -> IoResult<PathBuf> {
|
||||
let dir = self.get_dir();
|
||||
if dir.is_empty() {
|
||||
path.to_path_buf()
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
path.join(dir)
|
||||
util::safe_path_join(path, Path::new(&dir))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,7 @@ impl Paste {
|
|||
/// - If `file_name` does not have an extension, it is replaced with [`default_extension`].
|
||||
/// - If `file_name` is "-", it is replaced with "stdin".
|
||||
/// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string.
|
||||
/// - If `header_filename` is set, it will override the filename.
|
||||
///
|
||||
/// [`default_extension`]: crate::config::PasteConfig::default_extension
|
||||
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
|
||||
|
@ -95,15 +96,15 @@ impl Paste {
|
|||
&self,
|
||||
file_name: &str,
|
||||
expiry_date: Option<u128>,
|
||||
header_filename: Option<String>,
|
||||
config: &Config,
|
||||
) -> IoResult<String> {
|
||||
) -> Result<String, Error> {
|
||||
let file_type = infer::get(&self.data);
|
||||
if let Some(file_type) = file_type {
|
||||
for mime_type in &config.paste.mime_blacklist {
|
||||
if mime_type == file_type.mime_type() {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::Other,
|
||||
String::from("this file type is not permitted"),
|
||||
return Err(error::ErrorUnsupportedMediaType(
|
||||
"this file type is not permitted",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -120,10 +121,9 @@ impl Paste {
|
|||
if let Some(handle_spaces_config) = config.server.handle_spaces {
|
||||
file_name = handle_spaces_config.process_filename(&file_name);
|
||||
}
|
||||
let mut path = self
|
||||
.type_
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(&file_name);
|
||||
|
||||
let mut path =
|
||||
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
|
||||
let mut parts: Vec<&str> = file_name.split('.').collect();
|
||||
let mut dotfile = false;
|
||||
let mut lower_bound = 1;
|
||||
|
@ -166,11 +166,20 @@ impl Paste {
|
|||
}
|
||||
path.set_file_name(file_name);
|
||||
path.set_extension(extension);
|
||||
if let Some(header_filename) = header_filename {
|
||||
file_name = header_filename;
|
||||
path.set_file_name(file_name);
|
||||
}
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|v| v.to_string_lossy())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let file_path = util::glob_match_file(path.clone())
|
||||
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
|
||||
if file_path.is_file() && file_path.exists() {
|
||||
return Err(error::ErrorConflict("file already exists\n"));
|
||||
}
|
||||
if let Some(timestamp) = expiry_date {
|
||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||
}
|
||||
|
@ -235,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.
|
||||
|
@ -255,10 +264,8 @@ impl Paste {
|
|||
file_name = random_text;
|
||||
}
|
||||
}
|
||||
let mut path = self
|
||||
.type_
|
||||
.get_path(&config.server.upload_path)
|
||||
.join(&file_name);
|
||||
let mut path =
|
||||
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
|
||||
if let Some(timestamp) = expiry_date {
|
||||
path.set_file_name(format!("{file_name}.{timestamp}"));
|
||||
}
|
||||
|
@ -295,7 +302,7 @@ mod tests {
|
|||
data: vec![65, 66, 67],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("test.txt", None, &config)?;
|
||||
let file_name = paste.store_file("test.txt", None, None, &config)?;
|
||||
assert_eq!("ABC", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(
|
||||
Some("txt"),
|
||||
|
@ -315,7 +322,7 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
assert!(file_name.starts_with("foo."));
|
||||
|
@ -331,7 +338,7 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(".foo.tar.gz", None, &config)?;
|
||||
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
assert!(file_name.starts_with(".foo."));
|
||||
|
@ -347,7 +354,7 @@ mod tests {
|
|||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
|
||||
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert!(file_name.ends_with(".tar.gz"));
|
||||
fs::remove_file(file_name)?;
|
||||
|
@ -358,7 +365,7 @@ mod tests {
|
|||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(".foo", None, &config)?;
|
||||
let file_name = paste.store_file(".foo", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(".foo.txt", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
|
@ -373,7 +380,7 @@ mod tests {
|
|||
data: vec![120, 121, 122],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file("random", None, &config)?;
|
||||
let file_name = paste.store_file("random", None, None, &config)?;
|
||||
assert_eq!("xyz", fs::read_to_string(&file_name)?);
|
||||
assert_eq!(
|
||||
Some("bin"),
|
||||
|
@ -383,8 +390,52 @@ mod tests {
|
|||
);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
type_: RandomURLType::Alphanumeric,
|
||||
suffix_mode: Some(true),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let paste = Paste {
|
||||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header.txt".to_string()),
|
||||
&config,
|
||||
)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert_eq!("fn_from_header.txt", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
config.paste.random_url = Some(RandomURLConfig {
|
||||
length: Some(4),
|
||||
type_: RandomURLType::Alphanumeric,
|
||||
suffix_mode: Some(true),
|
||||
..RandomURLConfig::default()
|
||||
});
|
||||
let paste = Paste {
|
||||
data: vec![116, 101, 115, 115, 117, 115],
|
||||
type_: PasteType::File,
|
||||
};
|
||||
let file_name = paste.store_file(
|
||||
"filename.txt",
|
||||
None,
|
||||
Some("fn_from_header".to_string()),
|
||||
&config,
|
||||
)?;
|
||||
assert_eq!("tessus", fs::read_to_string(&file_name)?);
|
||||
assert_eq!("fn_from_header", file_name);
|
||||
fs::remove_file(file_name)?;
|
||||
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?;
|
||||
fs::create_dir_all(
|
||||
paste_type
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path"),
|
||||
)?;
|
||||
}
|
||||
|
||||
config.paste.random_url = None;
|
||||
|
@ -393,9 +444,10 @@ mod tests {
|
|||
type_: PasteType::Oneshot,
|
||||
};
|
||||
let expiry_date = util::get_system_time()?.as_millis() + 100;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
|
||||
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
|
||||
let file_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(format!("{file_name}.{expiry_date}"));
|
||||
assert_eq!("test", fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
|
@ -412,6 +464,7 @@ mod tests {
|
|||
let file_name = paste.store_url(None, &config)?;
|
||||
let file_path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(&file_name);
|
||||
assert_eq!(url, fs::read_to_string(&file_path)?);
|
||||
fs::remove_file(file_path)?;
|
||||
|
@ -439,15 +492,20 @@ mod tests {
|
|||
.await?;
|
||||
let file_path = PasteType::RemoteFile
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path")
|
||||
.join(file_name);
|
||||
assert_eq!(
|
||||
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
|
||||
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
||||
util::sha256_digest(&*paste.data)?
|
||||
);
|
||||
fs::remove_file(file_path)?;
|
||||
|
||||
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
|
||||
fs::remove_dir(paste_type.get_path(&config.server.upload_path))?;
|
||||
fs::remove_dir(
|
||||
paste_type
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path"),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
136
src/server.rs
136
src/server.rs
|
@ -4,7 +4,7 @@ use crate::file::Directory;
|
|||
use crate::header::{self, ContentDisposition};
|
||||
use crate::mime as mime_util;
|
||||
use crate::paste::{Paste, PasteType};
|
||||
use crate::util;
|
||||
use crate::util::{self, safe_path_join};
|
||||
use actix_files::NamedFile;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::StatusCode;
|
||||
|
@ -89,12 +89,11 @@ async fn serve(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let path = config.server.upload_path.join(&*file);
|
||||
let mut path = util::glob_match_file(path)?;
|
||||
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
||||
let mut paste_type = PasteType::File;
|
||||
if !path.exists() || path.is_dir() {
|
||||
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
|
||||
let alt_path = type_.get_path(&config.server.upload_path).join(&*file);
|
||||
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
|
||||
let alt_path = util::glob_match_file(alt_path)?;
|
||||
if alt_path.exists()
|
||||
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
|
||||
|
@ -159,13 +158,12 @@ async fn delete(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let path = config.server.upload_path.join(&*file);
|
||||
let path = util::glob_match_file(path)?;
|
||||
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
|
||||
if !path.is_file() || !path.exists() {
|
||||
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
||||
}
|
||||
match fs::remove_file(path) {
|
||||
Ok(_) => info!("deleted file: {:?}", file),
|
||||
Ok(_) => info!("deleted file: {:?}", file.to_string()),
|
||||
Err(e) => {
|
||||
error!("cannot delete file: {}", e);
|
||||
return Err(error::ErrorInternalServerError("cannot delete file"));
|
||||
|
@ -225,6 +223,7 @@ async fn upload(
|
|||
}
|
||||
let mut urls: Vec<String> = Vec::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
let header_filename = header::parse_header_filename(request.headers())?;
|
||||
let mut field = item?;
|
||||
let content = ContentDisposition::from(field.content_disposition().clone());
|
||||
if let Ok(paste_type) = PasteType::try_from(&content) {
|
||||
|
@ -274,7 +273,12 @@ async fn upload(
|
|||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
paste.store_file(content.get_file_name()?, expiry_date, &config)?
|
||||
paste.store_file(
|
||||
content.get_file_name()?,
|
||||
expiry_date,
|
||||
header_filename,
|
||||
&config,
|
||||
)?
|
||||
}
|
||||
PasteType::RemoteFile => {
|
||||
paste
|
||||
|
@ -853,6 +857,108 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_upload_file_override_filename() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new(RwLock::new(config)))
|
||||
.app_data(Data::new(Client::default()))
|
||||
.configure(configure_routes),
|
||||
)
|
||||
.await;
|
||||
|
||||
let file_name = "test_file.txt";
|
||||
let header_filename = "fn_from_header.txt";
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
get_multipart_request(×tamp, "file", file_name)
|
||||
.insert_header((
|
||||
header::HeaderName::from_static("filename"),
|
||||
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||
))
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(
|
||||
response.into_body(),
|
||||
&format!("http://localhost:8080/{header_filename}\n"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{header_filename}"))
|
||||
.to_request();
|
||||
let response = test::call_service(&app, serve_request).await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(response.into_body(), ×tamp).await?;
|
||||
|
||||
fs::remove_file(header_filename)?;
|
||||
let serve_request = TestRequest::get()
|
||||
.uri(&format!("/{header_filename}"))
|
||||
.to_request();
|
||||
let response = test::call_service(&app, serve_request).await;
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_upload_same_filename() -> Result<(), Error> {
|
||||
let mut config = Config::default();
|
||||
config.server.upload_path = env::current_dir()?;
|
||||
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.app_data(Data::new(RwLock::new(config)))
|
||||
.app_data(Data::new(Client::default()))
|
||||
.configure(configure_routes),
|
||||
)
|
||||
.await;
|
||||
|
||||
let file_name = "test_file.txt";
|
||||
let header_filename = "fn_from_header.txt";
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
get_multipart_request(×tamp, "file", file_name)
|
||||
.insert_header((
|
||||
header::HeaderName::from_static("filename"),
|
||||
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||
))
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
assert_body(
|
||||
response.into_body(),
|
||||
&format!("http://localhost:8080/{header_filename}\n"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let timestamp = util::get_system_time()?.as_secs().to_string();
|
||||
let response = test::call_service(
|
||||
&app,
|
||||
get_multipart_request(×tamp, "file", file_name)
|
||||
.insert_header((
|
||||
header::HeaderName::from_static("filename"),
|
||||
header::HeaderValue::from_static("fn_from_header.txt"),
|
||||
))
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::CONFLICT, response.status());
|
||||
assert_body(response.into_body(), "file already exists\n").await?;
|
||||
|
||||
fs::remove_file(header_filename)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[allow(deprecated)]
|
||||
async fn test_upload_duplicate_file() -> Result<(), Error> {
|
||||
|
@ -1003,7 +1109,7 @@ mod tests {
|
|||
let body = response.into_body();
|
||||
let body_bytes = actix_web::body::to_bytes(body).await?;
|
||||
assert_eq!(
|
||||
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
|
||||
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
|
||||
util::sha256_digest(&*body_bytes)?
|
||||
);
|
||||
|
||||
|
@ -1031,7 +1137,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let url_upload_path = PasteType::Url.get_path(&config.server.upload_path);
|
||||
let url_upload_path = PasteType::Url
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&url_upload_path)?;
|
||||
|
||||
let response = test::call_service(
|
||||
|
@ -1069,7 +1177,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let oneshot_upload_path = PasteType::Oneshot.get_path(&config.server.upload_path);
|
||||
let oneshot_upload_path = PasteType::Oneshot
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&oneshot_upload_path)?;
|
||||
|
||||
let file_name = "oneshot.txt";
|
||||
|
@ -1129,7 +1239,9 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let url_upload_path = PasteType::OneshotUrl.get_path(&config.server.upload_path);
|
||||
let url_upload_path = PasteType::OneshotUrl
|
||||
.get_path(&config.server.upload_path)
|
||||
.expect("Bad upload path");
|
||||
fs::create_dir_all(&url_upload_path)?;
|
||||
|
||||
let response = test::call_service(
|
||||
|
|
47
src/util.rs
47
src/util.rs
|
@ -2,6 +2,7 @@ use crate::paste::PasteType;
|
|||
use actix_web::{error, Error as ActixError};
|
||||
use glob::glob;
|
||||
use lazy_regex::{lazy_regex, Lazy, Regex};
|
||||
use path_clean::PathClean;
|
||||
use ring::digest::{Context, SHA256};
|
||||
use std::fmt::Write;
|
||||
use std::io::{BufReader, Read};
|
||||
|
@ -37,7 +38,7 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
|
|||
);
|
||||
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
|
||||
.map_err(error::ErrorInternalServerError)?
|
||||
.next()
|
||||
.last()
|
||||
{
|
||||
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
|
||||
if let Some(extension) = glob_path
|
||||
|
@ -64,7 +65,8 @@ pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
|
|||
PasteType::OneshotUrl,
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok())
|
||||
.filter_map(|v| v.get_path(base_path).ok())
|
||||
.filter_map(|v| glob(&v.join("*.[0-9]*").to_string_lossy()).ok())
|
||||
.flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>())
|
||||
.filter(|path| {
|
||||
if let Some(extension) = path
|
||||
|
@ -108,6 +110,27 @@ pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
|
|||
})?)
|
||||
}
|
||||
|
||||
/// Joins the paths whilst ensuring the path doesn't drastically change.
|
||||
/// `base` is assumed to be a trusted value.
|
||||
pub fn safe_path_join<B: AsRef<Path>, P: AsRef<Path>>(base: B, part: P) -> IoResult<PathBuf> {
|
||||
let new_path = base.as_ref().join(part).clean();
|
||||
|
||||
let cleaned_base = base.as_ref().clean();
|
||||
|
||||
if !new_path.starts_with(cleaned_base) {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} is outside of {}",
|
||||
new_path.display(),
|
||||
base.as_ref().display()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -170,4 +193,24 @@ mod tests {
|
|||
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(¤t_dir));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_safe_join_path() {
|
||||
assert_eq!(safe_path_join("/foo", "bar").ok(), Some("/foo/bar".into()));
|
||||
assert_eq!(safe_path_join("/", "bar").ok(), Some("/bar".into()));
|
||||
assert_eq!(safe_path_join("/", "././bar").ok(), Some("/bar".into()));
|
||||
assert_eq!(
|
||||
safe_path_join("/foo/bar", "baz/").ok(),
|
||||
Some("/foo/bar/baz/".into())
|
||||
);
|
||||
assert_eq!(
|
||||
safe_path_join("/foo/bar/../", "baz").ok(),
|
||||
Some("/foo/baz".into())
|
||||
);
|
||||
|
||||
assert!(safe_path_join("/foo", "/foobar").is_err());
|
||||
assert!(safe_path_join("/foo", "/bar").is_err());
|
||||
assert!(safe_path_join("/foo/bar", "..").is_err());
|
||||
assert!(safe_path_join("/foo/bar", "../").is_err());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue