Compare commits

...

113 Commits

Author SHA1 Message Date
dependabot[bot] 9ceb0d4c21
chore(deps): bump docker/setup-qemu-action from 2 to 3 (#283)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 19:27:39 +03:00
Orhun Parmaksız 7afea1721d
chore(docker): enable arm64 builds (#280)
This reverts commit d57ed1125f.
2024-05-11 23:40:06 +03:00
Orhun Parmaksız c04c39b975
docs(readme): remove blog post link 2024-05-05 14:01:07 +03:00
Orhun Parmaksız 32f95ef938
docs(readme): remove public instance link 2024-05-05 13:59:03 +03:00
dependabot[bot] 15e3619479
chore(deps): bump tokio from 1.36.0 to 1.37.0 (#268)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-04-04 00:09:29 +03:00
Orhun Parmaksız 2037530078
refactor(ci): provide codecov token via env
https://github.com/codecov/codecov-action/issues/1292
2024-04-04 00:03:57 +03:00
Orhun Parmaksız 44c07a3eb6
chore(release): prepare for v0.15.0 2024-03-27 23:03:10 +03:00
Orhun Parmaksız 64d783bd0d
chore(deps): bump all dependencies 2024-03-27 22:49:10 +03:00
Orhun Parmaksız 1fd561f869
docs(readme): add packaging status badge 2024-03-27 17:39:35 +03:00
dependabot[bot] c6d6da6296
chore(deps): bump codecov/codecov-action from 3 to 4 (#237)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:35:54 +03:00
Orhun Parmaksız 54e2ddc91a
chore(deps): bump serde from 1.0.196 to 1.0.197 2024-03-27 17:35:12 +03:00
dependabot[bot] 8e505c0da8
chore(deps): bump regex from 1.10.3 to 1.10.4 (#267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:34:04 +03:00
Orhun Parmaksız 0f0ba72305
chore(deps): bump shuttle dependencies 2024-03-27 17:33:23 +03:00
dependabot[bot] 77e97573ef
chore(deps): bump tokio from 1.35.1 to 1.36.0 (#242)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:27:22 +03:00
dependabot[bot] 40f5d909ca
chore(deps): bump config from 0.13.4 to 0.14.0 (#240)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:27:07 +03:00
dependabot[bot] 567480a21e
chore(deps): bump actix-files from 0.6.2 to 0.6.5 (#225)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:25:43 +03:00
dependabot[bot] e8c342af46
chore(deps): bump ring from 0.17.7 to 0.17.8 (#248)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:25:13 +03:00
dependabot[bot] 1442771a57
chore(deps): bump awc from 3.3.0 to 3.4.0 (#243)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:24:56 +03:00
Orhun Parmaksız dadd88c240
chore(license): update the copyright years 2024-03-24 23:45:04 +03:00
Orhun Parmaksız e3b00453d9
chore(github): update funding options 2024-03-24 23:44:34 +03:00
Helmut K. C. Tessarek b2acb71d0d
refactor(ci): replace unmaintained action (#266) 2024-03-23 11:47:23 +01:00
Orhun Parmaksız db24d911f6
chore(deps): set up mergify 2024-03-14 23:02:13 +03:00
Helmut K. C. Tessarek 4774de6652
refactor(server): use more specific HTTP status codes (#262)
* refactor(server): use more specific http status codes

* fix: clippy error - oops missed that one

* test(fixtures): add check for status code
2024-03-11 14:00:45 +01:00
Helmut K. C. Tessarek 4987cfe5e5
fix(upload): error on upload with the same filename (#258) 2024-03-08 14:47:13 +01:00
Orhun Parmaksız c60a614258
chore(ci): switch to cargo-llvm-cov for code coverage (#260) 2024-03-08 14:17:42 +01:00
Helmut K. C. Tessarek fa5105deab
test(upload): update the hash of the example file (#254)
changed at source
2024-03-06 05:57:59 +01:00
Jake Howard dae00c42b5
feat(server): do path joins more safely (#247)
* Do path joins more safely

* Improve path cleaning and tests

* Lower-case error message

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

* Correct handle potential errors in `get_path`

* Use `expect` in tests, rather than `unwrap`

* Correctly handle invalid upload path without panic

* Correctly handle filesystem create errors

* Use result rather than option to allow easier error handling

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-03-06 05:55:59 +01:00
Orhun Parmaksız 12f0e8f3a7
chore(server): gracefully exit when there is no config file found 2024-03-01 22:17:17 +03:00
Helmut K. C. Tessarek db971e6434
feat(server): allow to override filename when using random_url (#233)
* feat(server): allow to override filename when using random_url

* docs(README): remove line from features

* refactor(header): make const private
2024-02-12 13:06:12 +01:00
Helmut K. C. Tessarek 8e6393c6f4
fix(server): return the correct file on multiple files with same name (#234)
* fix(server): file not found, even though on server and not expired

* test: rename test
2024-02-09 21:38:33 +01:00
Helmut K. C. Tessarek e06c18279e
ci(shuttle): fix deployment (#236)
* ci(shuttle): fix deployment

* chore: add dummy file to trigger pipeline

* chore: remove dummy file to trigger pipeline

* ci(shuttle): remove dorny/paths-filter
2024-02-06 18:31:46 +01:00
Helmut K. C. Tessarek 48a3626c7d
fix(server): improve logging for deleted file (#235) 2024-01-31 21:25:04 +01:00
dependabot[bot] 8de2450931
chore(deps): bump serde from 1.0.195 to 1.0.196 (#232)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.195 to 1.0.196.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.195...v1.0.196)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 15:04:10 +01:00
dependabot[bot] 2003abe8bd
chore(deps): bump dorny/paths-filter from 2 to 3 (#231)
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 2 to 3.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/v2...v3)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-25 16:57:35 +01:00
dependabot[bot] 4703b26194
chore(deps): bump byte-unit from 5.1.3 to 5.1.4 (#229)
Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 5.1.3 to 5.1.4.
- [Commits](https://github.com/magiclen/byte-unit/compare/v5.1.3...v5.1.4)

---
updated-dependencies:
- dependency-name: byte-unit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 13:48:07 +01:00
dependabot[bot] e383b6f426
chore(deps): bump regex from 1.10.2 to 1.10.3 (#227)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.2...1.10.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 16:11:30 +01:00
dependabot[bot] fa7304a5d0
chore(deps): bump actions/cache from 3 to 4 (#226)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 16:59:37 +01:00
dependabot[bot] 90c0477edb
chore(deps): bump serde from 1.0.194 to 1.0.195 (#221)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.194 to 1.0.195.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.194...v1.0.195)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 12:35:54 +01:00
dependabot[bot] 7f8428c75d
chore(deps): bump byte-unit from 5.1.2 to 5.1.3 (#220)
Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 5.1.2 to 5.1.3.
- [Commits](https://github.com/magiclen/byte-unit/compare/v5.1.2...v5.1.3)

---
updated-dependencies:
- dependency-name: byte-unit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 12:35:35 +01:00
dependabot[bot] 25a013266c
chore(deps): bump serde from 1.0.193 to 1.0.194 (#219)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.193 to 1.0.194.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.193...v1.0.194)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 12:41:04 +01:00
Orhun Parmaksız 274bbd3307
fix(lints): apply clippy suggestions 2023-12-31 14:06:46 +03:00
dependabot[bot] c9ba166865
chore(deps): bump awc from 3.2.0 to 3.3.0 (#215)
Bumps [awc](https://github.com/actix/actix-web) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/actix/actix-web/releases)
- [Changelog](https://github.com/actix/actix-web/blob/master/CHANGES.md)
- [Commits](https://github.com/actix/actix-web/compare/awc-v3.2.0...awc-v3.3.0)

---
updated-dependencies:
- dependency-name: awc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 15:23:17 +01:00
dependabot[bot] 4af7970f48
chore(deps): bump futures-util from 0.3.29 to 0.3.30 (#214)
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.29 to 0.3.30.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.29...0.3.30)

---
updated-dependencies:
- dependency-name: futures-util
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 15:18:58 +01:00
dependabot[bot] 87a0ba21ed
chore(deps): bump actix-web from 4.4.0 to 4.4.1 (#213)
Bumps [actix-web](https://github.com/actix/actix-web) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/actix/actix-web/releases)
- [Changelog](https://github.com/actix/actix-web/blob/master/CHANGES.md)
- [Commits](https://github.com/actix/actix-web/compare/web-v4.4.0...web-v4.4.1)

---
updated-dependencies:
- dependency-name: actix-web
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 15:18:34 +01:00
Orhun Parmaksız c8dd41549d
chore(release): prepare for v0.14.4 2023-12-20 22:52:27 +03:00
Helmut K. C. Tessarek ec9b55f3e2
fix(auth): remove excessive warning and typo (#210) 2023-12-20 18:47:51 +01:00
dependabot[bot] f905648866
chore(deps): bump tokio from 1.35.0 to 1.35.1 (#208)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.35.0 to 1.35.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.35.0...tokio-1.35.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-20 12:57:53 +01:00
dependabot[bot] cf21df8a7c
chore(deps): bump byte-unit from 5.0.4 to 5.1.2 (#207)
Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 5.0.4 to 5.1.2.
- [Commits](https://github.com/magiclen/byte-unit/compare/v5.0.4...v5.1.2)

---
updated-dependencies:
- dependency-name: byte-unit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 13:46:56 +01:00
dependabot[bot] a806af9a41
chore(deps): bump byte-unit from 5.0.3 to 5.0.4 (#206)
Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 5.0.3 to 5.0.4.
- [Commits](https://github.com/magiclen/byte-unit/compare/v5.0.3...v5.0.4)

---
updated-dependencies:
- dependency-name: byte-unit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 15:35:55 +01:00
Orhun Parmaksız f3962485df
chore(deps): bump shuttle dependencies 2023-12-13 20:40:50 +03:00
Orhun Parmaksız d6e1968191
chore(changelog): update entries for v0.14.3 2023-12-12 21:31:26 +03:00
Orhun Parmaksız e34f747761
chore(release): prepare for v0.14.3 2023-12-12 21:30:08 +03:00
Artem Medvedev e21f99ac4a
refactor(server)!: cleanup authorization boilerplate (#199)
* refactor!: use `actix-web-grants` to protect endpoints

* fix: filter out blank strings

* doc: add documentation for a function

* fix: don't return body for not exposed endpoints

* test: add fixtures

* test: fix naming

* test: remove extra step in teardown
2023-12-12 19:21:29 +01:00
Artem Medvedev b74e9ceeaf
docs(readme): explain behavior of unset `auth_tokens` and `delete_tokens` (#202)
* doc(readme): explain behavior of unset `auth_tokens` & `delete_tokens`

Just a clarification on how this works in the case of uninstalled tokens, for greater clarity and security

* docs(readme): update styling

* docs(readme): update grammar

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-12-11 19:19:18 +01:00
Artem Medvedev c5c9c6d233
chore(fixtures): run fixtures on `macos` & `ubuntu` (#201)
* ci(fixtures): run fixtures on `macos` & `ubuntu`

In order to check the fixtures works on both OS

* fix: install coreutils for macos

* doc: add mention coreutils on `macOS`

* ci: fix

* ci: fix

* refactor(fixture): rename matrix.os.version to matrix.os.runner

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-12-11 19:16:25 +01:00
dependabot[bot] d7c67e0350
chore(deps): bump tokio from 1.34.0 to 1.35.0 (#203)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.34.0 to 1.35.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.34.0...tokio-1.35.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 13:46:46 +01:00
Artem Medvedev adbd67a182
test(fixtures): support `fixtures` on `macOS` (#200)
* test: support fixtures for macOS

* chore: clarify the comment

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-12-10 16:04:28 +01:00
Orhun Parmaksız 3e34206884
chore(deps): bump shuttle dependencies to 0.35.0 2023-12-09 22:41:20 +03:00
dependabot[bot] 1e9a3b4a71
chore(deps): bump ring from 0.17.6 to 0.17.7 (#196)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.6 to 0.17.7.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 19:07:16 +01:00
Orhun Parmaksız d4b02eca91
fix(shuttle): fix Shuttle entrypoint 2023-12-05 15:22:10 +03:00
Orhun Parmaksız 0f377fc44c
chore(release): prepare for v0.14.2 2023-12-05 15:21:06 +03:00
Orhun Parmaksız 62948abdc0
docs(release): add release instructions 2023-12-05 15:17:55 +03:00
Orhun Parmaksız bb33f437ba
chore(deps): upgrade transitive dependencies 2023-12-05 15:14:08 +03:00
Orhun Parmaksız ef08b9e838
refactor(tracing): use macros from tracing crate 2023-12-05 15:12:48 +03:00
dependabot[bot] a291307bec
chore(deps): bump byte-unit from 4.0.19 to 5.0.3 (#192)
* chore(deps): bump byte-unit from 4.0.19 to 5.0.3

Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 4.0.19 to 5.0.3.
- [Commits](https://github.com/magiclen/byte-unit/compare/v4.0.19...v5.0.3)

---
updated-dependencies:
- dependency-name: byte-unit
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): update codebase accordingly to the new version of byte-unit

* fix(fixtures): use more precise byte comparison

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-12-05 13:08:22 +01:00
Orhun Parmaksız 3c3a4b58ac
chore(example): add auth token handling to HTML form example (#183) 2023-12-03 21:37:00 +03:00
Orhun Parmaksız fd75750a43
chore(deps): bump shuttle dependencies to 0.34.1 2023-11-30 00:47:36 +03:00
Orhun Parmaksız 339963f2bd
refactor(ci): fix multiline if statement 2023-11-30 00:46:54 +03:00
Orhun Parmaksız ed9f6aa586
chore(shuttle): deploy when Shuttle dependency is updated 2023-11-30 00:44:47 +03:00
dependabot[bot] 7c9f443943
chore(deps): bump ring from 0.17.5 to 0.17.6 (#195)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.5 to 0.17.6.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 22:36:59 +01:00
dependabot[bot] 50fad6e333
chore(deps): bump lazy-regex from 3.0.2 to 3.1.0 (#182)
Bumps [lazy-regex](https://github.com/Canop/lazy-regex) from 3.0.2 to 3.1.0.
- [Changelog](https://github.com/Canop/lazy-regex/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Canop/lazy-regex/commits)

---
updated-dependencies:
- dependency-name: lazy-regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-24 23:05:34 +01:00
dependabot[bot] cc6815d783
chore(deps): bump config from 0.13.3 to 0.13.4 (#188)
Bumps [config](https://github.com/mehcode/config-rs) from 0.13.3 to 0.13.4.
- [Changelog](https://github.com/mehcode/config-rs/blob/v0.13.4/CHANGELOG.md)
- [Commits](https://github.com/mehcode/config-rs/compare/0.13.3...v0.13.4)

---
updated-dependencies:
- dependency-name: config
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-24 23:05:17 +01:00
Orhun Parmaksız 92a4360752
chore(deps): bump shuttle dependencies to 0.34.0 2023-11-25 01:03:37 +03:00
dependabot[bot] 89459e6be7
chore(deps): bump url from 2.4.1 to 2.5.0 (#189)
Bumps [url](https://github.com/servo/rust-url) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.4.1...v2.5.0)

---
updated-dependencies:
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-23 19:38:17 +01:00
dependabot[bot] 616e5846cc
chore(deps): bump serde from 1.0.192 to 1.0.193 (#187)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.192 to 1.0.193.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.192...v1.0.193)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 23:11:01 +01:00
Orhun Parmaksız 54a906ddf7
chore(deps): bump shuttle dependencies to 0.33.0 2023-11-18 13:02:01 +01:00
dependabot[bot] 4bd3082566
chore(deps): bump tokio from 1.33.0 to 1.34.0 (#181)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.33.0 to 1.34.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.33.0...tokio-1.34.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-15 18:20:18 +01:00
dependabot[bot] 0e7aafaf0f
chore(deps): bump tracing-subscriber from 0.3.17 to 0.3.18 (#184)
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.17 to 0.3.18.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.17...tracing-subscriber-0.3.18)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-14 15:00:22 +01:00
Orhun Parmaksız b0c61cdb09
bump shuttle dependencies to 0.32.0 2023-11-09 15:35:13 +03:00
dependabot[bot] 12b1c713a6
chore(deps): bump serde from 1.0.190 to 1.0.192 (#178)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.190 to 1.0.192.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.190...v1.0.192)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-07 22:24:14 +01:00
Orhun Parmaksız c6d32d6df9
docs(readme): update table of contents 2023-11-03 10:32:32 +01:00
DtxdF 9a73b3e9c5
docs(readme): add instructions for FreeBSD (#177)
* docs(readme): add instructions for FreeBSD

* docs(readme): add appjail to features section
2023-11-03 10:32:09 +01:00
Orhun Parmaksız 79662d64ab
chore(release): prepare for v0.14.1 2023-11-03 00:05:00 +01:00
Orhun Parmaksız eee3355107
fix(lib): fix typos 2023-11-02 23:58:46 +01:00
Orhun Parmaksız 87c0b11c60
chore(deps): upgrade transitive dependencies 2023-11-02 23:58:02 +01:00
Orhun Parmaksız 25be13d9bf
bump shuttle dependencies to 0.31.0 2023-11-02 16:10:51 +01:00
dependabot[bot] d943ad5bc6
chore(deps): bump futures-util from 0.3.28 to 0.3.29 (#174)
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.28 to 0.3.29.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.28...0.3.29)

---
updated-dependencies:
- dependency-name: futures-util
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-27 15:19:41 +02:00
dependabot[bot] f0e0b247eb
chore(deps): bump serde from 1.0.189 to 1.0.190 (#173)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.189 to 1.0.190.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.189...v1.0.190)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 14:18:07 +02:00
Orhun Parmaksız 30400cab0d
chore(deps): bump shuttle dependencies to 0.30.1 2023-10-25 16:36:42 +02:00
dependabot[bot] 2477b7428e
chore(deps): bump tracing from 0.1.39 to 0.1.40 (#168)
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.39 to 0.1.40.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.39...tracing-0.1.40)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-23 19:12:51 +02:00
Jean SIMARD 70541bb842
feat(log): switch to `tracing` for logging (#163)
* feat(log): use 'tracing-subscriber' instead of 'env_logger'

resolves #161

* feat: use 'tracing' instead of 'log'

* feat: make 'INFO' the default log level
2023-10-20 23:30:16 +02:00
dependabot[bot] cd9ace952c
chore(deps): bump ring from 0.17.4 to 0.17.5 (#167)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.4 to 0.17.5.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 14:25:37 +02:00
Orhun Parmaksız 77bb4ca299
chore(deps): bump regex to 1.10.2 2023-10-18 22:18:31 +03:00
dependabot[bot] 5d0e38ad15
chore(deps): bump regex from 1.10.0 to 1.10.1 (#165)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.0...1.10.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 11:25:28 +02:00
dependabot[bot] 4cb6548851
chore(deps): bump ring from 0.17.3 to 0.17.4 (#164)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.3 to 0.17.4.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 11:24:58 +02:00
dependabot[bot] db97b32955
chore(deps): bump serde from 1.0.188 to 1.0.189 (#162)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.188 to 1.0.189.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.188...v1.0.189)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 14:37:49 +02:00
dependabot[bot] 25171ad828
chore(deps): bump ring from 0.17.2 to 0.17.3 (#160)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.2 to 0.17.3.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 16:30:26 +02:00
dependabot[bot] d3394d0d30
chore(deps): bump regex from 1.9.6 to 1.10.0 (#159)
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.6 to 1.10.0.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.6...1.10.0)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 16:30:12 +02:00
Orhun Parmaksız 48cdd09298
chore(deps): bump shuttle dependencies to 0.29.0 2023-10-09 16:26:51 +03:00
dependabot[bot] b2cdc072ca
chore(deps): bump ring from 0.17.0 to 0.17.2 (#157)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.0 to 0.17.2.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 15:24:10 +02:00
dependabot[bot] 6be7b850a1
chore(deps): bump tokio from 1.32.0 to 1.33.0 (#155)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.32.0 to 1.33.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.32.0...tokio-1.33.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 15:23:35 +02:00
Orhun Parmaksız c60b548cc7
fix(lints): apply clippy suggestions 2023-10-07 14:40:05 +03:00
dependabot[bot] 8d4063c051
chore(deps): bump ring from 0.16.20 to 0.17.0 (#151)
Bumps [ring](https://github.com/briansmith/ring) from 0.16.20 to 0.17.0.
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-07 13:09:46 +02:00
Orhun Parmaksız 0eebb78694
chore(deps): bump shuttle dependencies to 0.28.0 2023-10-04 22:12:27 +03:00
dependabot[bot] 9e6dfeeaaa
chore(deps): bump regex from 1.9.5 to 1.9.6 (#152)
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.5 to 1.9.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.5...1.9.6)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 14:29:50 +02:00
Orhun Parmaksız 86759450b9
chore(deps): bump shuttle dependencies to 0.27.0 2023-09-22 15:01:20 +03:00
Orhun Parmaksız 613f1cd73d
fix(ci): use the correct arguments for cargo-tarpaulin 2023-09-19 13:58:21 +03:00
dependabot[bot] ab3e4844a9
chore(deps): bump docker/build-push-action from 4 to 5 (#143)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 12:57:16 +02:00
Orhun Parmaksız 3f13beccaf
chore(deps): bump shuttle dependencies to 0.26.0 2023-09-19 13:56:46 +03:00
dependabot[bot] 6a1b911652
chore(deps): bump docker/setup-buildx-action from 2 to 3 (#141)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 21:23:45 +02:00
dependabot[bot] fb0c075fc2
chore(deps): bump docker/login-action from 2 to 3 (#140)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 21:23:10 +02:00
dependabot[bot] 1fb0858f01
chore(deps): bump lazy-regex from 3.0.1 to 3.0.2 (#144)
Bumps [lazy-regex](https://github.com/Canop/lazy-regex) from 3.0.1 to 3.0.2.
- [Changelog](https://github.com/Canop/lazy-regex/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Canop/lazy-regex/commits)

---
updated-dependencies:
- dependency-name: lazy-regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 21:21:32 +02:00
dependabot[bot] ca68abd2a6
chore(deps): bump docker/metadata-action from 4 to 5 (#142)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 21:20:56 +02:00
43 changed files with 2016 additions and 1089 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
github: orhun
patreon: orhunp
custom: ["https://www.buymeacoffee.com/orhun"]
buy_me_a_coffee: orhun

7
.github/mergify.yml vendored Normal file
View File

@ -0,0 +1,7 @@
pull_request_rules:
- name: Automatic merge for Dependabot pull requests
conditions:
- author=dependabot[bot]
actions:
merge:
method: squash

View File

@ -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 }}

View File

@ -30,30 +30,40 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Setup cargo-tarpaulin
run: |
curl -s https://api.github.com/repos/xd009642/tarpaulin/releases/latest | \
grep "browser_download_url.*x86_64-unknown-linux-musl.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -qi -
tar -xzf cargo-tarpaulin-*.tar.gz
mv cargo-tarpaulin ~/.cargo/bin/
- name: Run tests
run: cargo tarpaulin --out Xml --verbose -- --test-threads 1
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate code coverage
run: cargo llvm-cov --lcov --output-path lcov.info -- --test-threads 1
env:
OUT_DIR: target
- name: Upload reports to codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
name: code-coverage-report
file: cobertura.xml
file: lcov.info
flags: unit-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
fixtures:
name: Test fixtures
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
os:
- name: Linux
runner: ubuntu-22.04
- name: macOS
runner: macos-12
name: Test fixtures [${{ matrix.os.name }}]
runs-on: ${{ matrix.os.runner }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install coreutils for MacOS
if: matrix.os.name == 'macOS'
run: brew install coreutils
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build the project

View File

@ -22,7 +22,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
orhunp/rustypaste
@ -35,12 +35,17 @@ jobs:
type=raw,value=latest
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
@ -49,14 +54,14 @@ jobs:
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: orhunp
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -64,11 +69,11 @@ jobs:
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}

View File

@ -6,12 +6,8 @@ on:
- master
tags:
- "v*.*.*"
pull_request:
branches:
- master
workflow_dispatch:
jobs:
build:
name: Build / Deploy
@ -35,7 +31,7 @@ jobs:
- name: Build
run: cargo build --locked --verbose
- name: Deploy
if: ${{ startsWith(github.event.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
run: |
cargo shuttle login --api-key ${{ secrets.SHUTTLE_TOKEN }}
cargo shuttle project restart

View File

@ -5,6 +5,94 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.15.0] - 2024-03-27
### Added
- Allow to override filename when using `random_url` by @tessus in [#233](https://github.com/orhun/rustypaste/pull/233)
Now you can use the `filename` header to override the name of the uploaded file.
For example:
```sh
curl -F "file=@x.txt" -H "filename:override.txt" http://localhost:8000
```
Even if `random_url` is set, the filename will be override.txt
[`rustypaste-cli`](https://github.com/orhun/rustypaste-cli) also has a new argument for overriding the file name:
```sh
rpaste -n filename-on-server.txt awesome.txt
```
- Use more specific HTTP status codes by @tessus in [#262](https://github.com/orhun/rustypaste/pull/262)
`rustypaste` now returns more appropriate status codes in the following 2 cases (instead of a generic 500 code):
- If the mime type is on the blacklist: `UnsupportedMediaType` (415)
- If the file already exists: `Conflict` (409)
### Changed
- Do path joins more safely by @RealOrangeOne in [#247](https://github.com/orhun/rustypaste/pull/247)
- Gracefully exit when there is no config file found by @orhun
- Switch to cargo-llvm-cov for code coverage by @orhun in [#260](https://github.com/orhun/rustypaste/pull/260)
- Replace unmaintained action by @tessus in [#266](https://github.com/orhun/rustypaste/pull/266)
- Set up mergify by @orhun
- Apply clippy suggestions by @orhun
- Update funding options by @orhun
- Update the copyright years by @orhun
- Bump dependencies
### Fixed
- Improve logging for deleted file by @tessus in [#235](https://github.com/orhun/rustypaste/pull/235)
- Fix deployment by @tessus in [#236](https://github.com/orhun/rustypaste/pull/236)
- Return the correct file on multiple files with same name by @tessus in [#234](https://github.com/orhun/rustypaste/pull/234)
- Update the hash of the example file by @tessus in [#254](https://github.com/orhun/rustypaste/pull/254)
- Error on upload with the same filename by @tessus in [#258](https://github.com/orhun/rustypaste/pull/258)
### New Contributors
- @RealOrangeOne made their first contribution in [#247](https://github.com/orhun/rustypaste/pull/247)
## [0.14.4] - 2023-12-20
### Removed
- Remove excessive warning messages when auth tokens are not found (#210)
## [0.14.3] - 2023-12-12
### Changed
- Return `404` for not exposed endpoints instead of `403`
- Disallow blank `delete_tokens` and `auth_tokens`
- Bump dependencies
## [0.14.2] - 2023-12-05
### Added
- Add installation instructions for FreeBSD (#177)
- Add auth token handling to HTML form example (#183)
- Add release instructions
### Changed
- Bump Shuttle to `0.34.1`
- Bump dependencies
## [0.14.1] - 2023-11-02
### Changed
- Switch to `tracing` for logging (#163)
- Bump Shuttle to `0.31.0`
- Bump dependencies
## [0.14.0] - 2023-09-05
### Added

1836
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "rustypaste"
version = "0.14.0"
version = "0.15.0"
edition = "2021"
description = "A minimal file upload/pastebin service"
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
@ -16,51 +16,47 @@ include = ["src/**/*", "Cargo.*", "LICENSE", "README.md", "CHANGELOG.md"]
default = ["rustls"]
openssl = ["actix-web/openssl", "awc/openssl"]
rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
shuttle = [
"dep:shuttle-actix-web",
"dep:shuttle-runtime",
"dep:shuttle-static-folder",
"dep:tokio",
]
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
[dependencies]
actix-web = { version = "4.4.0" }
actix-web = { version = "4.5.1" }
actix-web-grants = { version = "4.0.3" }
actix-multipart = "0.6.1"
actix-files = "0.6.2"
awc = { version = "3.2.0" }
env_logger = "0.10.0"
log = "0.4.20"
serde = "1.0.188"
futures-util = "0.3.28"
actix-files = "0.6.5"
shuttle-actix-web = { version = "0.42.0", optional = true }
shuttle-runtime = { version = "0.42.0", optional = true }
awc = { version = "3.4.0" }
serde = "1.0.197"
futures-util = "0.3.30"
petname = { version = "1.1.3", default-features = false, features = [
"std_rng",
"default_dictionary",
] }
rand = "0.8.5"
dotenvy = "0.15.7"
url = "2.4.1"
url = "2.5.0"
mime = "0.3.17"
regex = "1.9.5"
regex = "1.10.4"
serde_regex = "1.1.0"
lazy-regex = "3.0.1"
lazy-regex = "3.1.0"
humantime = "2.1.0"
humantime-serde = "1.1.1"
glob = "0.3.1"
ring = "0.16.20"
ring = "0.17.8"
hotwatch = "0.5.0"
shuttle-actix-web = { version = "0.25.0", optional = true }
shuttle-runtime = { version = "0.25.0", optional = true }
shuttle-static-folder = { version = "0.25.0", optional = true }
tokio = { version = "1.32.0", optional = true }
tokio = { version = "1.37.0", optional = true }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uts2ts = "0.4.1"
path-clean = "1.0.1"
[dependencies.config]
version = "0.13.3"
version = "0.14.0"
default-features = false
features = ["toml", "yaml"]
[dependencies.byte-unit]
version = "4.0.19"
version = "5.1.4"
features = ["serde"]
[dependencies.infer]

View File

@ -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

View File

@ -20,10 +20,6 @@ $ curl https://paste.site.com/safe-toad.txt
some text
```
The public instance is available at [https://rustypaste.shuttleapp.rs](https://rustypaste.shuttleapp.rs) 🚀
Here you can read the blog post about how it is deployed on Shuttle: [https://blog.orhun.dev/blazingly-fast-file-sharing](https://blog.orhun.dev/blazingly-fast-file-sharing)
</details>
<details>
@ -36,6 +32,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
- [From crates.io](#from-cratesio)
- [Arch Linux](#arch-linux)
- [Alpine Linux](#alpine-linux)
- [FreeBSD](#freebsd)
- [Binary releases](#binary-releases)
- [Build from source](#build-from-source)
- [Feature flags](#feature-flags)
@ -51,6 +48,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
- [Paste file from remote URL](#paste-file-from-remote-url)
- [Cleaning up expired files](#cleaning-up-expired-files)
- [Delete file from server](#delete-file-from-server)
- [Override the filename when using `random_url`](#override-the-filename-when-using-random_url)
- [Server](#server)
- [List endpoint](#list-endpoint)
- [HTML Form](#html-form)
@ -87,6 +85,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
- supports hot reloading
- Easy to deploy
- [docker images](https://hub.docker.com/r/orhunp/rustypaste)
- [appjail images](https://github.com/AppJail-makejails/rustypaste)
- No database
- filesystem is used
- Self-hosted
@ -96,6 +95,13 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
## Installation
<details>
<summary>Packaging status</summary>
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustypaste.svg)](https://repology.org/project/rustypaste/versions)
</details>
### From crates.io
```sh
@ -116,6 +122,12 @@ pacman -S rustypaste
apk add rustypaste
```
### FreeBSD
```sh
pkg install rustypaste
```
### Binary releases
See the available binaries on the [releases](https://github.com/orhun/rustypaste/releases/) page.
@ -244,6 +256,16 @@ Set `delete_tokens` array in [config.toml](./config.toml) to activate the [`DELE
$ curl -H "Authorization: <auth_token>" -X DELETE "<server_address>/file.txt"
```
> The `DELETE` endpoint will not be exposed and will return `404` error if `delete_tokens` are not set.
#### Override the filename when using `random_url`
The generation of a random filename can be overridden by sending a header called `filename`:
```sh
curl -F "file=@x.txt" -H "filename: <file_name>" "<server_address>"
```
### Server
To start the server:
@ -267,6 +289,10 @@ $ rustypaste
You can also set multiple auth tokens via the array field `[server].auth_tokens` in your `config.toml`.
> If neither `AUTH_TOKEN` nor `[server].auth_tokens` are set, the server will not require any authentication.
>
> Exception is the `DELETE` endpoint, which requires at least one token to be set. See [deleting files from server](#delete-file-from-server) for more information.
See [config.toml](./config.toml) for configuration options.
#### List endpoint

7
RELEASE.md Normal file
View File

@ -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`.

View File

@ -26,7 +26,7 @@
form {
margin: 20px 0;
}
input[type="url"],
input[type="text"],
input[type="file"] {
border: none;
border-radius: 5px;
@ -67,13 +67,13 @@ by default, pastes expire every hour.
>
<h2>share url</h2>
<form action="/" method="post" enctype="multipart/form-data">
<input type="url" name="url" />
<input type="text" name="url" />
<input type="submit" value="share" />
</form>
<h2>share file from url</h2>
<form action="/" method="post" enctype="multipart/form-data">
<input type="url" name="remote" />
<input type="text" name="remote" />
<input type="submit" value="share" />
</form>
@ -88,5 +88,56 @@ by default, pastes expire every hour.
<input type="file" name="oneshot" />
<input type="submit" value="share" />
</form>
<h2>share file with auth token</h2>
<div id="shareForm">
<input type="file" id="file" name="file" /><br>
<input type="text" id="authToken" placeholder="Auth Token" />
<input type="submit" value="share" onclick="shareFileWithAuth()" />
</form>
</body>
<script>
function shareFileWithAuth() {
const fileInput = document.getElementById("file");
const file = fileInput.files[0];
if (!file) {
alert("Please select a file");
return;
}
const authTokenInput = document.getElementById("authToken");
const authToken = authTokenInput.value;
if (!authToken) {
alert("Please provide an Auth Token");
return;
}
const formData = new FormData();
formData.append("file", file);
fetch("/", {
method: "POST",
headers: {
Authorization: authToken,
},
body: formData,
})
.then((response) => {
if (!response.ok) {
alert("Failed to upload");
throw new Error("Network response was not ok");
}
return response.text();
})
.then((data) => {
window.open(data, "_blank");
})
.catch((error) => {
console.error("There was an error uploading the file:", error);
});
}
</script>
</html>

View File

@ -7,6 +7,9 @@ This directory contains the [test fixtures](https://en.wikipedia.org/wiki/Test_f
1. Build the project in debug mode: `cargo build`
2. Execute the runner script in this directory: `./test-fixtures.sh`
On `macOS` you need to have [coreutils](https://www.gnu.org/software/coreutils/) installed to run the script.
The simplest way is to install it via [Homebrew](https://brew.sh/): `brew install coreutils`
### Adding new fixtures
Create an appropriately named directory for the test fixture you want to add. e.g. `test-file-upload`

View File

@ -9,7 +9,7 @@ setup() {
run_test() {
file_url=$(curl -s -F "file=@file" -H "expire:1s" localhost:8000)
test "$content" = "$(cat upload/file.txt.*)"
sleep 2s
sleep 2
result="$(curl -s $file_url)"
test "file is not found or expired :(" = "$result"

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -10,7 +10,7 @@ setup() {
run_test() {
seq $file_count | xargs -I -- curl -s -F "file=@file" -H "Authorization: $auth_token" localhost:8000 >/dev/null
test "$file_count" = "$(curl -s -H "Authorization: $auth_token" localhost:8000/list | grep -o 'file_name' | wc -l)"
test $file_count = $(curl -s -H "Authorization: $auth_token" localhost:8000/list | grep -o 'file_name' | wc -l)
test "unauthorized" = "$(curl -s localhost:8000/list)"
}

View File

@ -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" }

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -11,10 +11,10 @@ run_test() {
second_file_url=$(curl -s -F "file=@file" -H "expire:4s" localhost:8000)
test "$content" = "$(curl -s $first_file_url)"
test "$content" = "$(curl -s $second_file_url)"
sleep 3s
sleep 3
test "file is not found or expired :(" = "$(curl -s $first_file_url)"
test "$content" = "$(curl -s $second_file_url)"
sleep 1s
sleep 1
test "file is not found or expired :(" = "$(curl -s $second_file_url)"
}

View File

@ -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

View File

@ -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
}

View File

@ -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)"
}

View File

@ -1,6 +1,6 @@
[server]
address = "127.0.0.1:8000"
max_content_length = "10kb"
max_content_length = "10KB"
upload_path = "./upload"
[paste]

View File

@ -3,7 +3,8 @@
setup() {
touch emptyfile
truncate -s 9KB smallfile
fallocate -l 10000 normalfile
# On Linux, `fallocate -l 10000 normalfile` can be used for a better precision.
dd if=/dev/random of=normalfile count=10000 bs=1024 status=none
truncate -s 11KB bigfile
}

View File

@ -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

View File

@ -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
}

View File

@ -1,55 +1,162 @@
use actix_web::http::header::{HeaderMap, AUTHORIZATION};
use actix_web::{error, Error};
use crate::config::{Config, TokenType};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::header::AUTHORIZATION;
use actix_web::http::Method;
use actix_web::middleware::ErrorHandlerResponse;
use actix_web::{error, web, Error};
use std::collections::HashSet;
use std::sync::RwLock;
/// Checks the authorization header for the specified token.
/// Extracts the tokens from the authorization header by token type.
///
/// `Authorization: (type) <token>`
pub fn check(host: &str, headers: &HeaderMap, tokens: Option<Vec<String>>) -> Result<(), Error> {
if let Some(tokens) = tokens {
let auth_header = headers
.get(AUTHORIZATION)
.map(|v| v.to_str().unwrap_or_default())
.map(|v| v.split_whitespace().last().unwrap_or_default());
if !tokens.iter().any(|v| v == auth_header.unwrap_or_default()) {
#[cfg(debug_assertions)]
log::warn!(
"authorization failure for {host} (token: {})",
auth_header.unwrap_or("none"),
);
#[cfg(not(debug_assertions))]
log::warn!("authorization failure for {host}");
return Err(error::ErrorUnauthorized("unauthorized\n"));
pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<TokenType>, Error> {
let config = req
.app_data::<web::Data<RwLock<Config>>>()
.map(|cfg| cfg.read())
.and_then(Result::ok)
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?;
let mut user_tokens = HashSet::with_capacity(2);
let auth_header = req
.headers()
.get(AUTHORIZATION)
.map(|v| v.to_str().unwrap_or_default())
.map(|v| v.split_whitespace().last().unwrap_or_default());
for token_type in [TokenType::Auth, TokenType::Delete] {
let maybe_tokens = config.get_tokens(token_type);
if let Some(configured_tokens) = maybe_tokens {
if configured_tokens.contains(auth_header.unwrap_or_default()) {
user_tokens.insert(token_type);
}
} else if token_type == TokenType::Auth {
// not configured `auth_tokens` means that the user is allowed to access the endpoints
user_tokens.insert(token_type);
} else if token_type == TokenType::Delete && req.method() == Method::DELETE {
// explicitly disable `DELETE` methods if no `delete_tokens` are set
warn!("delete endpoint is not served because there are no delete_tokens set");
Err(error::ErrorNotFound(""))?;
}
}
Ok(())
Ok(user_tokens)
}
/// Returns `HttpResponse` with unauthorized (`401`) error and `unauthorized\n` as body.
pub(crate) fn unauthorized_error() -> actix_web::HttpResponse {
error::ErrorUnauthorized("unauthorized\n").into()
}
/// Log all unauthorized requests.
pub(crate) fn handle_unauthorized_error<B>(
res: ServiceResponse<B>,
) -> actix_web::Result<ErrorHandlerResponse<B>> {
let connection = res.request().connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");
#[cfg(debug_assertions)]
{
let auth_header = res
.request()
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.unwrap_or("none");
warn!("authorization failure for {host} (token: {auth_header})",);
}
#[cfg(not(debug_assertions))]
warn!("authorization failure for {host}");
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::http::header::HeaderValue;
use actix_web::test::TestRequest;
use actix_web::web::Data;
use actix_web::HttpResponse;
use awc::http::StatusCode;
#[actix_web::test]
async fn test_extract_tokens() -> Result<(), Error> {
let mut config = Config::default();
// request without configured auth-tokens
let request = TestRequest::default()
.app_data(Data::new(RwLock::new(config.clone())))
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
.to_srv_request();
let tokens = extract_tokens(&request).await?;
assert_eq!(HashSet::from([TokenType::Auth]), tokens);
// request with configured auth-tokens
config.server.auth_tokens = Some(["test_token".to_string()].into());
let request = TestRequest::default()
.app_data(Data::new(RwLock::new(config.clone())))
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
.to_srv_request();
let tokens = extract_tokens(&request).await?;
assert_eq!(HashSet::from([TokenType::Auth]), tokens);
// request with configured auth-tokens but wrong token in request
config.server.auth_tokens = Some(["test_token".to_string()].into());
let request = TestRequest::default()
.app_data(Data::new(RwLock::new(config.clone())))
.insert_header((
AUTHORIZATION,
HeaderValue::from_static("basic invalid_token"),
))
.to_srv_request();
let tokens = extract_tokens(&request).await?;
assert_eq!(HashSet::new(), tokens);
// DELETE request without configured delete-tokens
let request = TestRequest::default()
.method(Method::DELETE)
.app_data(Data::new(RwLock::new(config.clone())))
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
.to_srv_request();
let res = extract_tokens(&request).await;
assert!(res.is_err());
assert_eq!(
Some(StatusCode::NOT_FOUND),
res.err()
.as_ref()
.map(Error::error_response)
.as_ref()
.map(HttpResponse::status)
);
// DELETE request with configured delete-tokens
config.server.delete_tokens = Some(["delete_token".to_string()].into());
let request = TestRequest::default()
.method(Method::DELETE)
.app_data(Data::new(RwLock::new(config.clone())))
.insert_header((
AUTHORIZATION,
HeaderValue::from_static("basic delete_token"),
))
.to_srv_request();
let tokens = extract_tokens(&request).await?;
assert_eq!(HashSet::from([TokenType::Delete]), tokens);
// DELETE request with configured delete-tokens but wrong token in request
let request = TestRequest::default()
.method(Method::DELETE)
.app_data(Data::new(RwLock::new(config.clone())))
.insert_header((
AUTHORIZATION,
HeaderValue::from_static("basic invalid_token"),
))
.to_srv_request();
let tokens = extract_tokens(&request).await?;
assert_eq!(HashSet::new(), tokens);
#[test]
fn test_check_auth() -> Result<(), Error> {
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, HeaderValue::from_static("basic test_token"));
assert!(check("", &headers, Some(vec!["test_token".to_string()])).is_ok());
assert!(check("", &headers, Some(vec!["invalid_token".to_string()])).is_err());
assert!(check(
"",
&headers,
Some(vec!["invalid1".to_string(), "test_token".to_string()])
)
.is_ok());
assert!(check(
"",
&headers,
Some(vec!["invalid1".to_string(), "invalid2".to_string()])
)
.is_err());
assert!(check("", &headers, None).is_ok());
assert!(check("", &HeaderMap::new(), None).is_ok());
assert!(check("", &HeaderMap::new(), Some(vec!["token".to_string()])).is_err());
Ok(())
}
}

View File

@ -3,6 +3,7 @@ use crate::random::RandomURLConfig;
use crate::{AUTH_TOKEN_ENV, DELETE_TOKEN_ENV};
use byte_unit::Byte;
use config::{self, ConfigError};
use std::collections::HashSet;
use std::env;
use std::path::{Path, PathBuf};
use std::time::Duration;
@ -49,7 +50,7 @@ pub struct ServerConfig {
#[deprecated(note = "use [server].auth_tokens instead")]
pub auth_token: Option<String>,
/// Authentication tokens.
pub auth_tokens: Option<Vec<String>>,
pub auth_tokens: Option<HashSet<String>>,
/// Expose version.
pub expose_version: Option<bool>,
/// Landing page text.
@ -63,7 +64,7 @@ pub struct ServerConfig {
/// Path of the JSON index.
pub expose_list: Option<bool>,
/// Authentication tokens for deleting.
pub delete_tokens: Option<Vec<String>>,
pub delete_tokens: Option<HashSet<String>>,
}
/// Enum representing different strategies for handling spaces in filenames.
@ -130,6 +131,7 @@ pub struct CleanupConfig {
}
/// Type of access token.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum TokenType {
/// Token for authentication.
Auth,
@ -148,48 +150,52 @@ impl Config {
}
/// Retrieves all configured auth/delete tokens.
pub fn get_tokens(&self, token_type: TokenType) -> Option<Vec<String>> {
pub fn get_tokens(&self, token_type: TokenType) -> Option<HashSet<String>> {
let mut tokens = match token_type {
TokenType::Auth => {
let mut tokens = self.server.auth_tokens.clone().unwrap_or_default();
let mut tokens: HashSet<_> = self.server.auth_tokens.clone().unwrap_or_default();
#[allow(deprecated)]
if let Some(token) = &self.server.auth_token {
tokens.insert(0, token.to_string());
tokens.insert(token.to_string());
}
if let Ok(env_token) = env::var(AUTH_TOKEN_ENV) {
tokens.insert(0, env_token);
tokens.insert(env_token);
}
tokens
}
TokenType::Delete => {
let mut tokens = self.server.delete_tokens.clone().unwrap_or_default();
let mut tokens: HashSet<_> = self.server.delete_tokens.clone().unwrap_or_default();
if let Ok(env_token) = env::var(DELETE_TOKEN_ENV) {
tokens.insert(0, env_token);
tokens.insert(env_token);
}
tokens
}
};
tokens.retain(|v| !v.is_empty());
(!tokens.is_empty()).then_some(tokens)
// filter out blank tokens
tokens.retain(|v| !v.trim().is_empty());
Some(tokens).filter(|v| !v.is_empty())
}
/// Print deprecation warnings.
#[allow(deprecated)]
pub fn warn_deprecation(&self) {
if self.server.auth_token.is_some() {
log::warn!("[server].auth_token is deprecated, please use [server].auth_tokens");
warn!("[server].auth_token is deprecated, please use [server].auth_tokens");
}
if self.server.landing_page.is_some() {
log::warn!("[server].landing_page is deprecated, please use [landing_page].text");
warn!("[server].landing_page is deprecated, please use [landing_page].text");
}
if self.server.landing_page_content_type.is_some() {
log::warn!(
warn!(
"[server].landing_page_content_type is deprecated, please use [landing_page].content_type"
);
}
if let Some(random_url) = &self.paste.random_url {
if random_url.enabled.is_some() {
log::warn!(
warn!(
"[paste].random_url.enabled is deprecated, disable it by commenting out [paste].random_url"
);
}
@ -241,24 +247,33 @@ mod tests {
env::set_var("AUTH_TOKEN", "env_auth");
env::set_var("DELETE_TOKEN", "env_delete");
let mut config = Config::parse(&config_path)?;
config.server.auth_tokens = Some(vec!["may_the_force_be_with_you".to_string()]);
config.server.delete_tokens = Some(vec!["i_am_your_father".to_string()]);
// empty tokens will be filtered
config.server.auth_tokens =
Some(["may_the_force_be_with_you".to_string(), "".to_string()].into());
config.server.delete_tokens = Some(["i_am_your_father".to_string(), "".to_string()].into());
assert_eq!(
Some(vec![
Some(HashSet::from([
"env_auth".to_string(),
"may_the_force_be_with_you".to_string()
]),
])),
config.get_tokens(TokenType::Auth)
);
assert_eq!(
Some(vec![
Some(HashSet::from([
"env_delete".to_string(),
"i_am_your_father".to_string()
]),
])),
config.get_tokens(TokenType::Delete)
);
env::remove_var("AUTH_TOKEN");
env::remove_var("DELETE_TOKEN");
// `get_tokens` returns `None` if no tokens are configured
config.server.auth_tokens = Some([" ".to_string()].into());
config.server.delete_tokens = Some(HashSet::new());
assert_eq!(None, config.get_tokens(TokenType::Auth));
assert_eq!(None, config.get_tokens(TokenType::Delete));
Ok(())
}
}

View File

@ -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.

View File

@ -31,6 +31,10 @@ pub mod util;
/// Custom middleware implementation.
pub mod middleware;
// Use macros from tracing crate.
#[macro_use]
extern crate tracing;
/// Environment variable for setting the configuration file path.
pub const CONFIG_ENV: &str = "CONFIG";

View File

@ -18,12 +18,20 @@ use std::path::{Path, PathBuf};
use std::sync::{mpsc, RwLock};
use std::thread;
use std::time::Duration;
#[cfg(not(feature = "shuttle"))]
use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter,
};
#[cfg(feature = "shuttle")]
use {
actix_web::web::{self, ServiceConfig},
shuttle_actix_web::ShuttleActixWeb,
};
// Use macros from tracing crate.
#[macro_use]
extern crate tracing;
/// Sets up the application.
///
/// * loads the configuration
@ -36,7 +44,14 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
// Initialize logger.
#[cfg(not(feature = "shuttle"))]
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
tracing_subscriber::registry()
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.with(tracing_subscriber::fmt::layer())
.init();
// Parse configuration.
let config_path = match env::var(CONFIG_ENV).ok() {
@ -46,8 +61,15 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
}
None => config_folder.join("config.toml"),
};
if !config_path.exists() {
error!(
"{} is not found, please provide a configuration file.",
config_path.display()
);
std::process::exit(1);
}
let config = Config::parse(&config_path).expect("failed to parse config");
log::trace!("{:#?}", config);
trace!("{:#?}", config);
config.warn_deprecation();
let server_config = config.server.clone();
let paste_config = RwLock::new(config.paste.clone());
@ -56,7 +78,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
// Create necessary directories.
fs::create_dir_all(&server_config.upload_path)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?;
fs::create_dir_all(paste_type.get_path(&server_config.upload_path)?)?;
}
// Set up a watcher for the configuration file changes.
@ -74,24 +96,24 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
let cloned_config = Data::clone(&config);
let config_watcher = move |event: Event| {
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
(event.kind, event.paths.get(0))
(event.kind, event.paths.first())
{
match Config::parse(path) {
Ok(config) => match cloned_config.write() {
Ok(mut cloned_config) => {
*cloned_config = config.clone();
log::info!("Configuration has been updated.");
info!("Configuration has been updated.");
if let Err(e) = config_sender.send(config) {
log::error!("Failed to send config for the cleanup routine: {}", e)
error!("Failed to send config for the cleanup routine: {}", e)
}
cloned_config.warn_deprecation();
}
Err(e) => {
log::error!("Failed to acquire config: {}", e);
error!("Failed to acquire config: {}", e);
}
},
Err(e) => {
log::error!("Failed to update config: {}", e);
error!("Failed to update config: {}", e);
}
}
}
@ -110,11 +132,11 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
.and_then(|v| v.delete_expired_files.clone())
{
if cleanup_config.enabled {
log::debug!("Running cleanup...");
debug!("Running cleanup...");
for file in util::get_expired_files(&upload_path) {
match fs::remove_file(&file) {
Ok(()) => log::info!("Removed expired file: {:?}", file),
Err(e) => log::error!("Cannot remove expired file: {}", e),
Ok(()) => info!("Removed expired file: {:?}", file),
Err(e) => error!("Cannot remove expired file: {}", e),
}
}
thread::sleep(cleanup_config.interval);
@ -131,7 +153,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
*paste_config = new_config.paste;
}
Err(e) => {
log::error!("Failed to update config for the cleanup routine: {}", e);
error!("Failed to update config for the cleanup routine: {}", e);
}
}
}
@ -162,9 +184,7 @@ async fn main() -> IoResult<()> {
.wrap(Logger::new(
"%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
))
.wrap(ContentLengthLimiter::new(
server_config.max_content_length.get_bytes(),
))
.wrap(ContentLengthLimiter::new(server_config.max_content_length))
.configure(server::configure_routes)
})
.bind(&server_config.address)?;
@ -175,17 +195,15 @@ async fn main() -> IoResult<()> {
}
// Run the server.
log::info!("Server is running at {}", server_config.address);
info!("Server is running at {}", server_config.address);
http_server.run().await
}
#[cfg(feature = "shuttle")]
#[shuttle_runtime::main]
async fn actix_web(
#[shuttle_static_folder::StaticFolder(folder = "shuttle")] static_folder: PathBuf,
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
async fn actix_web() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
// Set up the application.
let (config, server_config, _hotwatch) = setup(&static_folder)?;
let (config, server_config, _hotwatch) = setup(Path::new("shuttle"))?;
// Create the service.
let service_config = move |cfg: &mut ServiceConfig| {
@ -204,9 +222,7 @@ async fn actix_web(
.wrap(Logger::new(
"%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
))
.wrap(ContentLengthLimiter::new(
server_config.max_content_length.get_bytes(),
))
.wrap(ContentLengthLimiter::new(server_config.max_content_length))
.configure(server::configure_routes),
);
};

View File

@ -3,6 +3,7 @@ use actix_web::http::header::CONTENT_LENGTH;
use actix_web::http::StatusCode;
use actix_web::{body::EitherBody, Error};
use actix_web::{HttpMessage, HttpResponseBuilder};
use byte_unit::Byte;
use futures_util::{Future, TryStreamExt};
use std::{
future::{ready, Ready},
@ -14,12 +15,12 @@ use std::{
#[derive(Debug)]
pub struct ContentLengthLimiter {
// Maximum amount of bytes to allow.
max_bytes: u128,
max_bytes: Byte,
}
impl ContentLengthLimiter {
/// Contructs a new instance.
pub fn new(max_bytes: u128) -> Self {
/// Constructs a new instance.
pub fn new(max_bytes: Byte) -> Self {
Self { max_bytes }
}
}
@ -47,7 +48,7 @@ where
#[derive(Debug)]
pub struct ContentLengthLimiterMiddleware<S> {
service: Rc<S>,
max_bytes: u128,
max_bytes: Byte,
}
impl<S, B> Service<ServiceRequest> for ContentLengthLimiterMiddleware<S>
@ -66,10 +67,13 @@ where
.headers()
.get(CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u128>().ok())
.and_then(|v| v.parse::<Byte>().ok())
{
if content_length > self.max_bytes {
log::warn!("Upload rejected due to exceeded limit.");
warn!(
"Upload rejected due to exceeded limit. ({:-#} > {:-#})",
content_length, self.max_bytes
);
return Box::pin(async move {
// drain the body due to https://github.com/actix/actix-web/issues/2695
let mut payload = request.take_payload();

View File

@ -58,12 +58,12 @@ impl PasteType {
}
/// Returns the given path with [`directory`](Self::get_dir) adjoined.
pub fn get_path(&self, path: &Path) -> PathBuf {
pub fn get_path(&self, path: &Path) -> IoResult<PathBuf> {
let dir = self.get_dir();
if dir.is_empty() {
path.to_path_buf()
Ok(path.to_path_buf())
} else {
path.join(dir)
util::safe_path_join(path, Path::new(&dir))
}
}
@ -88,6 +88,7 @@ impl Paste {
/// - If `file_name` does not have an extension, it is replaced with [`default_extension`].
/// - If `file_name` is "-", it is replaced with "stdin".
/// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string.
/// - If `header_filename` is set, it will override the filename.
///
/// [`default_extension`]: crate::config::PasteConfig::default_extension
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
@ -95,15 +96,15 @@ impl Paste {
&self,
file_name: &str,
expiry_date: Option<u128>,
header_filename: Option<String>,
config: &Config,
) -> IoResult<String> {
) -> Result<String, Error> {
let file_type = infer::get(&self.data);
if let Some(file_type) = file_type {
for mime_type in &config.paste.mime_blacklist {
if mime_type == file_type.mime_type() {
return Err(IoError::new(
IoErrorKind::Other,
String::from("this file type is not permitted"),
return Err(error::ErrorUnsupportedMediaType(
"this file type is not permitted",
));
}
}
@ -120,10 +121,9 @@ impl Paste {
if let Some(handle_spaces_config) = config.server.handle_spaces {
file_name = handle_spaces_config.process_filename(&file_name);
}
let mut path = self
.type_
.get_path(&config.server.upload_path)
.join(&file_name);
let mut path =
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
let mut parts: Vec<&str> = file_name.split('.').collect();
let mut dotfile = false;
let mut lower_bound = 1;
@ -166,11 +166,20 @@ impl Paste {
}
path.set_file_name(file_name);
path.set_extension(extension);
if let Some(header_filename) = header_filename {
file_name = header_filename;
path.set_file_name(file_name);
}
let file_name = path
.file_name()
.map(|v| v.to_string_lossy())
.unwrap_or_default()
.to_string();
let file_path = util::glob_match_file(path.clone())
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
if file_path.is_file() && file_path.exists() {
return Err(error::ErrorConflict("file already exists\n"));
}
if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}"));
}
@ -210,7 +219,6 @@ impl Paste {
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.server
.max_content_length
.get_bytes()
.try_into()
.map_err(error::ErrorInternalServerError)?;
let bytes = response
@ -236,7 +244,7 @@ impl Paste {
.to_string());
}
}
Ok(self.store_file(file_name, expiry_date, &config)?)
self.store_file(file_name, expiry_date, None, &config)
}
/// Writes an URL to a file in upload directory.
@ -256,10 +264,8 @@ impl Paste {
file_name = random_text;
}
}
let mut path = self
.type_
.get_path(&config.server.upload_path)
.join(&file_name);
let mut path =
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}"));
}
@ -277,6 +283,7 @@ mod tests {
use awc::ClientBuilder;
use byte_unit::Byte;
use std::env;
use std::str::FromStr;
use std::time::Duration;
#[actix_rt::test]
@ -295,7 +302,7 @@ mod tests {
data: vec![65, 66, 67],
type_: PasteType::File,
};
let file_name = paste.store_file("test.txt", None, &config)?;
let file_name = paste.store_file("test.txt", None, None, &config)?;
assert_eq!("ABC", fs::read_to_string(&file_name)?);
assert_eq!(
Some("txt"),
@ -315,7 +322,7 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert!(file_name.ends_with(".tar.gz"));
assert!(file_name.starts_with("foo."));
@ -331,7 +338,7 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file(".foo.tar.gz", None, &config)?;
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert!(file_name.ends_with(".tar.gz"));
assert!(file_name.starts_with(".foo."));
@ -347,7 +354,7 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert!(file_name.ends_with(".tar.gz"));
fs::remove_file(file_name)?;
@ -358,7 +365,7 @@ mod tests {
data: vec![120, 121, 122],
type_: PasteType::File,
};
let file_name = paste.store_file(".foo", None, &config)?;
let file_name = paste.store_file(".foo", None, None, &config)?;
assert_eq!("xyz", fs::read_to_string(&file_name)?);
assert_eq!(".foo.txt", file_name);
fs::remove_file(file_name)?;
@ -373,7 +380,7 @@ mod tests {
data: vec![120, 121, 122],
type_: PasteType::File,
};
let file_name = paste.store_file("random", None, &config)?;
let file_name = paste.store_file("random", None, None, &config)?;
assert_eq!("xyz", fs::read_to_string(&file_name)?);
assert_eq!(
Some("bin"),
@ -383,8 +390,52 @@ mod tests {
);
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig {
length: Some(4),
type_: RandomURLType::Alphanumeric,
suffix_mode: Some(true),
..RandomURLConfig::default()
});
let paste = Paste {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file(
"filename.txt",
None,
Some("fn_from_header.txt".to_string()),
&config,
)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert_eq!("fn_from_header.txt", file_name);
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig {
length: Some(4),
type_: RandomURLType::Alphanumeric,
suffix_mode: Some(true),
..RandomURLConfig::default()
});
let paste = Paste {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file(
"filename.txt",
None,
Some("fn_from_header".to_string()),
&config,
)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert_eq!("fn_from_header", file_name);
fs::remove_file(file_name)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?;
fs::create_dir_all(
paste_type
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)?;
}
config.paste.random_url = None;
@ -393,9 +444,10 @@ mod tests {
type_: PasteType::Oneshot,
};
let expiry_date = util::get_system_time()?.as_millis() + 100;
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
let file_path = PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(format!("{file_name}.{expiry_date}"));
assert_eq!("test", fs::read_to_string(&file_path)?);
fs::remove_file(file_path)?;
@ -412,6 +464,7 @@ mod tests {
let file_name = paste.store_url(None, &config)?;
let file_path = PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(&file_name);
assert_eq!(url, fs::read_to_string(&file_path)?);
fs::remove_file(file_path)?;
@ -439,15 +492,20 @@ mod tests {
.await?;
let file_path = PasteType::RemoteFile
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(file_name);
assert_eq!(
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
util::sha256_digest(&*paste.data)?
);
fs::remove_file(file_path)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
fs::remove_dir(paste_type.get_path(&config.server.upload_path))?;
fs::remove_dir(
paste_type
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)?;
}
Ok(())

View File

@ -1,15 +1,18 @@
use crate::auth;
use crate::auth::{extract_tokens, handle_unauthorized_error, unauthorized_error};
use crate::config::{Config, LandingPageConfig, TokenType};
use crate::file::Directory;
use crate::header::{self, ContentDisposition};
use crate::mime as mime_util;
use crate::paste::{Paste, PasteType};
use crate::util;
use crate::util::{self, safe_path_join};
use actix_files::NamedFile;
use actix_multipart::Multipart;
use actix_web::http::StatusCode;
use actix_web::middleware::ErrorHandlers;
use actix_web::{delete, error, get, post, web, Error, HttpRequest, HttpResponse};
use actix_web_grants::GrantsMiddleware;
use awc::Client;
use byte_unit::Byte;
use byte_unit::{Byte, UnitType};
use futures_util::stream::StreamExt;
use mime::TEXT_PLAIN_UTF_8;
use serde::{Deserialize, Serialize};
@ -86,12 +89,11 @@ async fn serve(
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let path = config.server.upload_path.join(&*file);
let mut path = util::glob_match_file(path)?;
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
let mut paste_type = PasteType::File;
if !path.exists() || path.is_dir() {
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
let alt_path = type_.get_path(&config.server.upload_path).join(&*file);
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
let alt_path = util::glob_match_file(alt_path)?;
if alt_path.exists()
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
@ -148,31 +150,22 @@ async fn serve(
/// Remove a file from the upload directory.
#[delete("/{file}")]
#[actix_web_grants::protect("TokenType::Delete", ty = TokenType, error = unauthorized_error)]
async fn delete(
request: HttpRequest,
file: web::Path<String>,
config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> {
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let path = config.server.upload_path.join(&*file);
let path = util::glob_match_file(path)?;
let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");
let tokens = config.get_tokens(TokenType::Delete);
if tokens.is_none() {
log::warn!("delete endpoint is not served because there are no delete_tokens set");
return Err(error::ErrorForbidden("endpoint is not exposed\n"));
}
auth::check(host, request.headers(), tokens)?;
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
if !path.is_file() || !path.exists() {
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
}
match fs::remove_file(path) {
Ok(_) => log::info!("deleted file: {:?}", file),
Ok(_) => info!("deleted file: {:?}", file.to_string()),
Err(e) => {
log::error!("cannot delete file: {}", e);
error!("cannot delete file: {}", e);
return Err(error::ErrorInternalServerError("cannot delete file"));
}
}
@ -181,27 +174,23 @@ async fn delete(
/// Expose version endpoint
#[get("/version")]
async fn version(
request: HttpRequest,
config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> {
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");
let tokens = config.get_tokens(TokenType::Auth);
auth::check(host, request.headers(), tokens)?;
if !config.server.expose_version.unwrap_or(false) {
log::warn!("server is not configured to expose version endpoint");
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
warn!("server is not configured to expose version endpoint");
Err(error::ErrorNotFound(""))?;
}
let version = env!("CARGO_PKG_VERSION");
Ok(HttpResponse::Ok().body(version.to_owned() + "\n"))
}
/// Handles file upload by processing `multipart/form-data`.
#[post("/")]
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn upload(
request: HttpRequest,
mut payload: Multipart,
@ -210,13 +199,6 @@ async fn upload(
) -> Result<HttpResponse, Error> {
let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");
{
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let tokens = config.get_tokens(TokenType::Auth);
auth::check(host, request.headers(), tokens)?;
}
let server_url = match config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
@ -241,6 +223,7 @@ async fn upload(
}
let mut urls: Vec<String> = Vec::new();
while let Some(item) = payload.next().await {
let header_filename = header::parse_header_filename(request.headers())?;
let mut field = item?;
let content = ContentDisposition::from(field.content_disposition().clone());
if let Ok(paste_type) = PasteType::try_from(&content) {
@ -249,7 +232,7 @@ async fn upload(
bytes.append(&mut chunk?.to_vec());
}
if bytes.is_empty() {
log::warn!("{} sent zero bytes", host);
warn!("{} sent zero bytes", host);
return Err(error::ErrorBadRequest("invalid file size"));
}
if paste_type != PasteType::Oneshot
@ -290,7 +273,12 @@ async fn upload(
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
paste.store_file(content.get_file_name()?, expiry_date, &config)?
paste.store_file(
content.get_file_name()?,
expiry_date,
header_filename,
&config,
)?
}
PasteType::RemoteFile => {
paste
@ -304,10 +292,12 @@ async fn upload(
paste.store_url(expiry_date, &config)?
}
};
log::info!(
info!(
"{} ({}) is uploaded from {}",
file_name,
Byte::from_bytes(paste.data.len() as u128).get_appropriate_unit(false),
Byte::from_u128(paste.data.len() as u128)
.unwrap_or_default()
.get_appropriate_unit(UnitType::Decimal),
host
);
let config = config
@ -318,7 +308,7 @@ async fn upload(
}
urls.push(format!("{}/{}\n", server_url, file_name));
} else {
log::warn!("{} sent an invalid form field", host);
warn!("{} sent an invalid form field", host);
return Err(error::ErrorBadRequest("invalid form field"));
}
}
@ -338,21 +328,15 @@ pub struct ListItem {
/// Returns the list of files.
#[get("/list")]
async fn list(
request: HttpRequest,
config: web::Data<RwLock<Config>>,
) -> Result<HttpResponse, Error> {
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
.clone();
let connection = request.connection_info().clone();
let host = connection.realip_remote_addr().unwrap_or("unknown host");
let tokens = config.get_tokens(TokenType::Auth);
auth::check(host, request.headers(), tokens)?;
if !config.server.expose_list.unwrap_or(false) {
log::warn!("server is not configured to expose list endpoint");
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
warn!("server is not configured to expose list endpoint");
Err(error::ErrorNotFound(""))?;
}
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
.filter_map(|entry| {
@ -365,7 +349,7 @@ async fn list(
metadata
}
Err(e) => {
log::error!("failed to read metadata: {e}");
error!("failed to read metadata: {e}");
return None;
}
};
@ -398,13 +382,20 @@ async fn list(
/// Configures the server routes.
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(index)
.service(version)
.service(list)
.service(serve)
.service(upload)
.service(delete)
.route("", web::head().to(HttpResponse::MethodNotAllowed));
cfg.service(
web::scope("")
.service(index)
.service(version)
.service(list)
.service(serve)
.service(upload)
.service(delete)
.route("", web::head().to(HttpResponse::MethodNotAllowed))
.wrap(GrantsMiddleware::with_extractor(extract_tokens))
.wrap(
ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized_error),
),
);
}
#[cfg(test)]
@ -564,7 +555,7 @@ mod tests {
#[actix_web::test]
async fn test_version_without_auth() -> Result<(), Error> {
let mut config = Config::default();
config.server.auth_tokens = Some(vec!["test".to_string()]);
config.server.auth_tokens = Some(["test".to_string()].into());
let app = test::init_service(
App::new()
.app_data(Data::new(RwLock::new(config)))
@ -598,8 +589,8 @@ mod tests {
.uri("/version")
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(StatusCode::FORBIDDEN, response.status());
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
assert_eq!(StatusCode::NOT_FOUND, response.status());
assert_body(response.into_body(), "").await?;
Ok(())
}
@ -719,7 +710,7 @@ mod tests {
#[actix_web::test]
async fn test_auth() -> Result<(), Error> {
let mut config = Config::default();
config.server.auth_tokens = Some(vec!["test".to_string()]);
config.server.auth_tokens = Some(["test".to_string()].into());
let app = test::init_service(
App::new()
@ -743,7 +734,7 @@ mod tests {
App::new()
.app_data(Data::new(RwLock::new(Config::default())))
.app_data(Data::new(Client::default()))
.wrap(ContentLengthLimiter::new(1))
.wrap(ContentLengthLimiter::new(Byte::from_u64(1)))
.configure(configure_routes),
)
.await;
@ -762,7 +753,7 @@ mod tests {
#[actix_web::test]
async fn test_delete_file() -> Result<(), Error> {
let mut config = Config::default();
config.server.delete_tokens = Some(vec!["test".to_string()]);
config.server.delete_tokens = Some(["test".to_string()].into());
config.server.upload_path = env::current_dir()?;
let app = test::init_service(
@ -816,8 +807,8 @@ mod tests {
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(StatusCode::FORBIDDEN, response.status());
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
assert_eq!(StatusCode::NOT_FOUND, response.status());
assert_body(response.into_body(), "").await?;
Ok(())
}
@ -866,6 +857,108 @@ mod tests {
Ok(())
}
#[actix_web::test]
async fn test_upload_file_override_filename() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = env::current_dir()?;
let app = test::init_service(
App::new()
.app_data(Data::new(RwLock::new(config)))
.app_data(Data::new(Client::default()))
.configure(configure_routes),
)
.await;
let file_name = "test_file.txt";
let header_filename = "fn_from_header.txt";
let timestamp = util::get_system_time()?.as_secs().to_string();
let response = test::call_service(
&app,
get_multipart_request(&timestamp, "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(), &timestamp).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(&timestamp, "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(&timestamp, "file", file_name)
.insert_header((
header::HeaderName::from_static("filename"),
header::HeaderValue::from_static("fn_from_header.txt"),
))
.to_request(),
)
.await;
assert_eq!(StatusCode::CONFLICT, response.status());
assert_body(response.into_body(), "file already exists\n").await?;
fs::remove_file(header_filename)?;
Ok(())
}
#[actix_web::test]
#[allow(deprecated)]
async fn test_upload_duplicate_file() -> Result<(), Error> {
@ -975,7 +1068,7 @@ mod tests {
async fn test_upload_remote_file() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = env::current_dir()?;
config.server.max_content_length = Byte::from_bytes(30000);
config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default();
let app = test::init_service(
App::new()
@ -1016,7 +1109,7 @@ mod tests {
let body = response.into_body();
let body_bytes = actix_web::body::to_bytes(body).await?;
assert_eq!(
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
util::sha256_digest(&*body_bytes)?
);
@ -1044,7 +1137,9 @@ mod tests {
)
.await;
let url_upload_path = PasteType::Url.get_path(&config.server.upload_path);
let url_upload_path = PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&url_upload_path)?;
let response = test::call_service(
@ -1082,7 +1177,9 @@ mod tests {
)
.await;
let oneshot_upload_path = PasteType::Oneshot.get_path(&config.server.upload_path);
let oneshot_upload_path = PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&oneshot_upload_path)?;
let file_name = "oneshot.txt";
@ -1142,7 +1239,9 @@ mod tests {
)
.await;
let url_upload_path = PasteType::OneshotUrl.get_path(&config.server.upload_path);
let url_upload_path = PasteType::OneshotUrl
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&url_upload_path)?;
let response = test::call_service(

View File

@ -2,8 +2,11 @@ use crate::paste::PasteType;
use actix_web::{error, Error as ActixError};
use glob::glob;
use lazy_regex::{lazy_regex, Lazy, Regex};
use path_clean::PathClean;
use ring::digest::{Context, SHA256};
use std::fmt::Write;
use std::io::{BufReader, Read};
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
@ -35,7 +38,7 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
);
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
.map_err(error::ErrorInternalServerError)?
.next()
.last()
{
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
if let Some(extension) = glob_path
@ -62,7 +65,8 @@ pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
PasteType::OneshotUrl,
]
.into_iter()
.filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok())
.filter_map(|v| v.get_path(base_path).ok())
.filter_map(|v| glob(&v.join("*.[0-9]*").to_string_lossy()).ok())
.flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>())
.filter(|path| {
if let Some(extension) = path
@ -99,8 +103,32 @@ pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
.iter()
.collect::<Vec<&u8>>()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>())
.try_fold::<String, _, IoResult<String>>(String::new(), |mut output, b| {
write!(output, "{b:02x}")
.map_err(|e| IoError::new(IoErrorKind::Other, e.to_string()))?;
Ok(output)
})?)
}
/// Joins the paths whilst ensuring the path doesn't drastically change.
/// `base` is assumed to be a trusted value.
pub fn safe_path_join<B: AsRef<Path>, P: AsRef<Path>>(base: B, part: P) -> IoResult<PathBuf> {
let new_path = base.as_ref().join(part).clean();
let cleaned_base = base.as_ref().clean();
if !new_path.starts_with(cleaned_base) {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!(
"{} is outside of {}",
new_path.display(),
base.as_ref().display()
),
));
}
Ok(new_path)
}
#[cfg(test)]
@ -165,4 +193,24 @@ mod tests {
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(&current_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());
}
}