480 Commits

Author SHA1 Message Date
Alexander Neumann
ad130de021 Generate CHANGELOG.md for 0.14.0 2025-05-31 22:21:45 +02:00
Alexander Neumann
2aaa048aba Move changelog files for 0.14.0 2025-05-31 22:21:33 +02:00
Alexander Neumann
b6ec6f45cc Update VERSION files for 0.14.0 2025-05-31 22:20:43 +02:00
rawtaz
2a77536ce5 Merge pull request #348 from restic/clarify-umask-for-group-accessible
Some checks failed
test / Linux Go 1.23.x (push) Has been cancelled
test / Linux (race) Go 1.24.x (push) Has been cancelled
test / Linux Go 1.24.x (push) Has been cancelled
test / lint (push) Has been cancelled
test / Analyze results (push) Has been cancelled
Improve description of group-accessible option
2025-05-27 20:58:05 +02:00
Michael Eischer
0adcfa2619 Improve description of group-accessible option 2025-05-27 19:43:24 +02:00
Michael Eischer
9f8bb0c87c Merge pull request #347 from restic/polish-changelogs
Some checks failed
test / Linux (race) Go 1.24.x (push) Has been cancelled
test / Linux Go 1.23.x (push) Has been cancelled
test / Linux Go 1.24.x (push) Has been cancelled
test / lint (push) Has been cancelled
test / Analyze results (push) Has been cancelled
Polish changelogs
2025-05-21 19:01:27 +02:00
Leo R. Lundgren
5faeedf050 Polish changelogs 2025-05-20 21:56:28 +02:00
Michael Eischer
7294612990 Merge pull request #346 from MichaelEischer/zip-for-windows
Some checks are pending
test / lint (push) Waiting to run
test / Analyze results (push) Blocked by required conditions
test / Linux Go 1.23.x (push) Waiting to run
test / Linux (race) Go 1.24.x (push) Waiting to run
test / Linux Go 1.24.x (push) Waiting to run
Build zip files for windows release binaries
2025-05-20 19:09:48 +02:00
Michael Eischer
25066228ee add changelog for windows zip binaries 2025-05-15 21:03:05 +02:00
Michael Eischer
72a7319fae limit build parallelism 2025-05-15 20:58:23 +02:00
Michael Eischer
df5330773f also generate zip files for windows 2025-05-15 20:58:23 +02:00
Michael Eischer
2bb4d251e2 autoformat goreleaser yaml 2025-05-15 20:58:23 +02:00
Michael Eischer
f018e99109 Merge pull request #340 from MichaelEischer/limit-htpasswd-perms
Some checks failed
test / Linux Go 1.23.x (push) Has been cancelled
test / Linux (race) Go 1.24.x (push) Has been cancelled
test / Linux Go 1.24.x (push) Has been cancelled
test / lint (push) Has been cancelled
test / Analyze results (push) Has been cancelled
Limit htpasswd perms
2025-05-15 20:20:16 +02:00
Michael Eischer
95538fe956 restrict umask of htpasswd file 2025-05-15 19:56:53 +02:00
Michael Eischer
4e6193ceee Merge pull request #339 from MichaelEischer/polish-changelogs
Some checks failed
test / Linux Go 1.23.x (push) Has been cancelled
test / Linux (race) Go 1.24.x (push) Has been cancelled
test / Linux Go 1.24.x (push) Has been cancelled
test / lint (push) Has been cancelled
test / Analyze results (push) Has been cancelled
Polish changelogs
2025-05-12 19:00:34 +02:00
Michael Eischer
4c368ae1fb polish changelogs 2025-05-12 18:50:47 +02:00
Michael Eischer
0ed9de379e Merge pull request #341 from restic/dependabot/go_modules/github.com/prometheus/client_golang-1.22.0
Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0
2025-05-12 18:47:15 +02:00
Michael Eischer
451c4831f9 Merge pull request #345 from restic/dependabot/go_modules/golang.org/x/crypto-0.38.0
Bump golang.org/x/crypto from 0.37.0 to 0.38.0
2025-05-12 18:47:00 +02:00
dependabot[bot]
1610cf6cef Bump golang.org/x/crypto from 0.37.0 to 0.38.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 19:51:08 +00:00
dependabot[bot]
3d35116b3c Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.1...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 20:42:16 +00:00
dependabot[bot]
eee73d3bc1 Bump golang.org/x/crypto from 0.33.0 to 0.37.0 (#337)
Some checks failed
test / Linux Go 1.23.x (push) Has been cancelled
test / Linux (race) Go 1.24.x (push) Has been cancelled
test / Linux Go 1.24.x (push) Has been cancelled
test / lint (push) Has been cancelled
test / Analyze results (push) Has been cancelled
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.37.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.37.0
  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>
2025-04-14 19:16:53 +00:00
Michael Eischer
df7b13e18f Merge pull request #338 from MichaelEischer/bump-go1.23
Bump minimum Go version to 1.23
2025-04-14 21:13:59 +02:00
Michael Eischer
2d3e02017b CI: add Go 1.24 and reduce diff to config used by restic 2025-04-14 21:11:57 +02:00
DarkSpir
2b6f0b39fc Hardened tls cipher suits and added option for tls min version (#315)
* handlers.go: Added parameter for TLS min version

rest-server/main.go: Added parameter handling for TLS min version

rest-server/main.go: Added crypto.tls, implemented and configured tlsConfig object

* tls min version parameter documentation

* Added changelog documentation

* README.md: Fixed typo

main.go: Added error for unknown TLS min versions

main.go: Changed CurvePreferences in TLS config to Go default

main.go: Removed handling for TLS min versions 1.0 and 1.1

Signed-off-by: darkspir <forgejo.darkspir@teemitmil.ch>

* main.go: Improved TLSMinVer parameter documentation

* README.md: Improved --tls-min-ver parameter documentation

* main.go: Changed --tls-min-ver parameter documentation again

* main.go: Added allowed versions in Error Unsupported TLS min version

* update rest-server help output in readme

---------

Signed-off-by: darkspir <forgejo.darkspir@teemitmil.ch>
Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-04-14 19:09:57 +00:00
Michael Eischer
dbf5253ac2 Merge pull request #307 from akmet/master
Add support for proxy-based authentication
2025-04-14 21:01:11 +02:00
Michael Eischer
fa15677855 Merge pull request #334 from stemid/patch-1
Support passwords with spaces in docker container
2025-04-14 20:52:01 +02:00
Michael Eischer
19aa0845c0 create_user/delete_user: quote user and filename 2025-04-14 20:48:11 +02:00
Michael Eischer
04b52b0cee document how to use group-accessible repos with systemd (#327) 2025-04-14 20:46:20 +02:00
dependabot[bot]
f2d406ff2e Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.1 (#331)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  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>
2025-04-14 20:45:54 +02:00
Michael Eischer
8ad7cfa60a bump minimum go version to 1.23 2025-04-14 20:44:28 +02:00
Stefan Midjich
4f17744d6c Support passwords with spaces
You must quote $2 in order to support passwords with spaces.
2025-03-13 14:40:59 +01:00
Michael Eischer
68ae5d1c0b check that proxy header is ignored if not enabled 2025-02-17 22:36:03 +01:00
Michael Eischer
0dfc772cdb document proxy auth in readme 2025-02-17 22:32:49 +01:00
akmet
b0a9a0452e Add support for proxy-based authentication 2025-02-17 22:25:25 +01:00
Massimo Lusetti
f053e33486 Add group-accessible-repos option (#308)
Some checks failed
test / Go ${{ matrix.go }} (1.22.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.23.x) (push) Has been cancelled
test / lint (push) Has been cancelled
test / Analyze results (push) Has been cancelled
* Add group-accessible-repos option

The group-accessible-repos option will let filesystem group id
be able to access files and dir within the restic repo
Default stick with old behaviour to be owner restricted
While here make dirMode and fileMode within Options struct
private

---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
2025-02-17 22:17:54 +01:00
Michael Eischer
10a06dcbf1 Merge pull request #325 from restic/dependabot/go_modules/golang.org/x/crypto-0.33.0
Bump golang.org/x/crypto from 0.32.0 to 0.33.0
2025-02-17 21:46:52 +01:00
dependabot[bot]
b05b44cb2c Bump golang.org/x/crypto from 0.27.0 to 0.33.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.33.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 20:44:14 +00:00
Michael Eischer
a976a2145b Merge pull request #326 from restic/dependabot/go_modules/github.com/spf13/cobra-1.9.1
Bump github.com/spf13/cobra from 1.8.1 to 1.9.1
2025-02-17 21:43:11 +01:00
dependabot[bot]
751d2841f3 Bump github.com/spf13/cobra from 1.8.1 to 1.9.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 20:38:50 +00:00
Michael Eischer
5938f9aacd Merge pull request #323 from MichaelEischer/update-issue-templates
Sync issue and pr templates with restic
2025-02-17 21:38:35 +01:00
Michael Eischer
376392a89c Merge pull request #322 from MichaelEischer/bump-go-version
Bump minimum go version to 1.22
2025-02-17 21:37:02 +01:00
Michael Eischer
13f461740d sync issue and pr template with restic 2025-02-07 23:47:46 +01:00
Michael Eischer
e9e6529345 update dependencies 2025-02-07 22:44:58 +01:00
Michael Eischer
a0110bb902 add changelog 2025-02-07 22:44:58 +01:00
Michael Eischer
82c5a314f9 CI: sync pipeline with restic 2025-02-07 22:44:58 +01:00
Michael Eischer
9fc5066fc4 fix most linter errors 2025-02-07 22:34:40 +01:00
Michael Eischer
3e0737a8bd sync golangci config with restic 2025-02-07 22:34:40 +01:00
Michael Eischer
9195526406 bump minimum go version to 1.22 2025-02-02 22:32:12 +01:00
Michael Eischer
2513a698f3 Merge pull request #302 from restic/dependabot/go_modules/golang.org/x/crypto-0.27.0
Some checks failed
test / Go ${{ matrix.go }} (1.18.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.19.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.20.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.21.x) (push) Has been cancelled
test / lint (push) Has been cancelled
Bump golang.org/x/crypto from 0.25.0 to 0.27.0
2024-12-01 17:59:35 +01:00
dependabot[bot]
bdfb047edf Bump golang.org/x/crypto from 0.25.0 to 0.27.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.27.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 19:18:23 +00:00
Michael Eischer
e35c6e39d9 Merge pull request #295 from restic/append-only-status
Some checks failed
test / Go ${{ matrix.go }} (1.18.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.19.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.20.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.21.x) (push) Has been cancelled
test / lint (push) Has been cancelled
Output status of append only mode on startup
2024-07-29 19:36:59 +02:00
Leo R. Lundgren
da5bb66030 Output status of append only mode on startup 2024-07-29 01:20:10 +02:00
Alexander Neumann
664d997006 Fix goreleaser config
Some checks failed
test / Go ${{ matrix.go }} (1.18.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.19.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.20.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.21.x) (push) Has been cancelled
test / lint (push) Has been cancelled
2024-07-26 14:55:06 +02:00
Alexander Neumann
5d7b581db6 Generate CHANGELOG.md for 0.13.0 2024-07-26 14:47:50 +02:00
Alexander Neumann
37b4327012 Move changelog files for 0.13.0 2024-07-26 14:47:23 +02:00
Alexander Neumann
238eceb2a1 Update VERSION files for 0.13.0 2024-07-26 14:47:04 +02:00
Alexander Neumann
9fb75a71cb Udpate goreleaser config 2024-07-26 12:03:27 +02:00
rawtaz
38f29da143 Merge pull request #294 from restic/polish-changelogs
Some checks failed
test / Go ${{ matrix.go }} (1.18.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.19.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.20.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.21.x) (push) Has been cancelled
test / lint (push) Has been cancelled
doc: Polish unreleased changelogs
2024-07-24 21:03:55 +02:00
Leo R. Lundgren
eb9f8cfa1f doc: Polish unreleased changelogs 2024-07-24 21:01:16 +02:00
Michael Eischer
e8a9fbc88f Merge pull request #290 from MichaelEischer/bump-dependencies
Some checks failed
test / Go ${{ matrix.go }} (1.18.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.19.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.20.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.21.x) (push) Has been cancelled
test / lint (push) Has been cancelled
Bump dependencies and replace deprecated goreleaser option
2024-07-10 21:11:49 +02:00
Michael Eischer
7b2639b21e replace deprecated goreleaser options 2024-07-06 17:40:45 +02:00
Michael Eischer
40fd34e7c8 bump dependencies 2024-07-06 17:40:45 +02:00
Michael Eischer
a6323b5e98 Merge pull request #288 from restic/dependabot/go_modules/github.com/spf13/cobra-1.8.1
Some checks failed
test / Go ${{ matrix.go }} (1.18.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.19.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.20.x) (push) Has been cancelled
test / Go ${{ matrix.go }} (1.21.x) (push) Has been cancelled
test / lint (push) Has been cancelled
Bump github.com/spf13/cobra from 1.8.0 to 1.8.1
2024-06-26 19:26:28 +02:00
dependabot[bot]
318f0c6a89 Bump github.com/spf13/cobra from 1.8.0 to 1.8.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 19:47:55 +00:00
Michael Eischer
add9b066f9 Merge pull request #285 from restic/dependabot/github_actions/golangci/golangci-lint-action-6
Bump golangci/golangci-lint-action from 5 to 6
2024-06-11 22:19:04 +02:00
Michael Eischer
e35f649f07 Merge pull request #287 from restic/dependabot/go_modules/golang.org/x/crypto-0.24.0
Bump golang.org/x/crypto from 0.22.0 to 0.24.0
2024-06-11 22:15:24 +02:00
dependabot[bot]
e309a92895 Bump golang.org/x/crypto from 0.22.0 to 0.24.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.22.0 to 0.24.0.
- [Commits](https://github.com/golang/crypto/compare/v0.22.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 19:31:28 +00:00
dependabot[bot]
848e75c753 Bump golangci/golangci-lint-action from 5 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 19:32:17 +00:00
Michael Eischer
d3fedbefe5 Merge pull request #281 from restic/dependabot/github_actions/golangci/golangci-lint-action-5
Bump golangci/golangci-lint-action from 4 to 5
2024-05-06 21:22:34 +02:00
Michael Eischer
d8cbfd1f63 Merge pull request #280 from restic/dependabot/go_modules/golang.org/x/crypto-0.22.0
Bump golang.org/x/crypto from 0.21.0 to 0.22.0
2024-05-06 21:22:20 +02:00
dependabot[bot]
154efcee43 Bump golangci/golangci-lint-action from 4 to 5
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 19:32:25 +00:00
dependabot[bot]
9d5b956858 Bump golang.org/x/crypto from 0.21.0 to 0.22.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 19:41:12 +00:00
Michael Eischer
705c83a714 Merge pull request #278 from benjamonnguyen/update-compose-with-grafana-example
fix docker-compose.yml for compose-with-grafana example
2024-03-27 18:59:33 +01:00
Michael Eischer
421c542e31 Merge pull request #279 from restic/dependabot/go_modules/google.golang.org/protobuf-1.33.0
Bump google.golang.org/protobuf from 1.31.0 to 1.33.0
2024-03-27 18:49:14 +01:00
dependabot[bot]
c510ac2e41 Bump google.golang.org/protobuf from 1.31.0 to 1.33.0
Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 22:57:14 +00:00
Benjamin Nguyen
fed9c4b1e3 fix docker-compose.yml for compose-with-grafana example 2024-03-07 14:28:57 -08:00
Michael Eischer
a2b39539a3 Merge pull request #277 from restic/dependabot/go_modules/golang.org/x/crypto-0.21.0
Bump golang.org/x/crypto from 0.19.0 to 0.21.0
2024-03-07 13:31:29 +01:00
dependabot[bot]
16262def42 Bump golang.org/x/crypto from 0.19.0 to 0.21.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.19.0 to 0.21.0.
- [Commits](https://github.com/golang/crypto/compare/v0.19.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 19:03:57 +00:00
Michael Eischer
0c735187ba Merge pull request #272 from ae-govau/unixsocket
Enhancement: add support for unix socket listen
2024-02-23 18:39:47 +01:00
Adam Eijdenberg
b4430d3607 address feedback, fix typo in example command 2024-02-23 15:26:06 +11:00
Adam Eijdenberg
e0674c6150 Enhancement: can now listen on a unix socket 2024-02-19 14:47:43 +11:00
Michael Eischer
55f43b815c Merge pull request #274 from restic/dependabot/go_modules/golang.org/x/crypto-0.19.0
Bump golang.org/x/crypto from 0.18.0 to 0.19.0
2024-02-18 12:11:33 +01:00
Michael Eischer
b3522ae2ac Merge pull request #275 from restic/dependabot/github_actions/golangci/golangci-lint-action-4
Bump golangci/golangci-lint-action from 3 to 4
2024-02-18 12:11:27 +01:00
dependabot[bot]
6faaaa713e Bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 19:37:48 +00:00
dependabot[bot]
a4f2823515 Bump golang.org/x/crypto from 0.18.0 to 0.19.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/crypto/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 19:12:57 +00:00
Michael Eischer
13083acc1a Merge pull request #273 from ae-govau/gracefulshutdown
fix: shutdown gracefully on TERM or INT signals
2024-02-08 19:30:39 +01:00
Adam Eijdenberg
ce15402f74 fix: increase test timeout to 10s, listen on 127.0.0.1 only
This is to try to fix test reliability in GitHub actions.
2024-02-06 10:14:40 +11:00
Michael Eischer
b28bea1de4 Merge pull request #271 from ae-govau/printlistenaddr
fix: print the actual address listened on
2024-02-05 20:52:01 +01:00
Michael Eischer
4bfb2eaa81 simplify changelog for listen address 2024-02-05 20:50:09 +01:00
Adam Eijdenberg
8becd574cb fix: shutdown gracefully on TERM or INT signals
This allows server listen resources to be cleaned up appropriately.
2024-02-05 10:48:58 +11:00
Adam Eijdenberg
03ad2420db fix: print the actual address listened on
This is useful when the server is started with an ephemeral port, e.g.:

./rest-server --no-auth --listen "127.0.0.1:0"

...

start server on 127.0.0.1:46015
2024-02-05 10:44:25 +11:00
Michael Eischer
3ce6aaf2b6 Merge pull request #268 from restic/dependabot/go_modules/golang.org/x/crypto-0.18.0
Bump golang.org/x/crypto from 0.17.0 to 0.18.0
2024-01-13 19:47:30 +01:00
Michael Eischer
e388fcddde Merge pull request #270 from pagdot/patch-1
Update rest-server.socket with correct default rest-server port
2024-01-13 19:47:14 +01:00
pagdot
2d456efbe8 Update rest-server.socket with correct default rest-server port 2024-01-09 21:52:12 +01:00
dependabot[bot]
14f70457cf Bump golang.org/x/crypto from 0.17.0 to 0.18.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/crypto/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-08 19:33:07 +00:00
Michael Eischer
29753193ac Merge pull request #266 from restic/dependabot/go_modules/github.com/prometheus/client_golang-1.18.0
Bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0
2024-01-06 22:20:52 +01:00
dependabot[bot]
e2d5251cbf Bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-06 21:16:41 +00:00
Michael Eischer
f36b6504fc Merge pull request #267 from MichaelEischer/require-go-1.18
Bump require go version to 1.18
2024-01-06 22:15:52 +01:00
Michael Eischer
04418c721f require go 1.18 2024-01-06 22:11:49 +01:00
Michael Eischer
c615b9dbdd Merge pull request #264 from restic/dependabot/github_actions/actions/setup-go-5
Bump actions/setup-go from 4 to 5
2024-01-06 22:00:39 +01:00
Michael Eischer
292556a8bc Merge pull request #265 from restic/dependabot/go_modules/golang.org/x/crypto-0.17.0
Bump golang.org/x/crypto from 0.15.0 to 0.17.0
2024-01-06 21:56:30 +01:00
dependabot[bot]
ef8998a1cc Bump golang.org/x/crypto from 0.15.0 to 0.17.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 19:51:11 +00:00
dependabot[bot]
0c905fd64c Bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-11 19:26:37 +00:00
Michael Eischer
45f71173c8 Merge pull request #262 from restic/dependabot/go_modules/golang.org/x/crypto-0.15.0
Bump golang.org/x/crypto from 0.14.0 to 0.15.0
2023-11-13 22:08:05 +01:00
dependabot[bot]
ce63a80646 Bump golang.org/x/crypto from 0.14.0 to 0.15.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 19:23:42 +00:00
Michael Eischer
6f91a8638d Merge pull request #261 from restic/dependabot/go_modules/github.com/spf13/cobra-1.8.0
Bump github.com/spf13/cobra from 1.7.0 to 1.8.0
2023-11-11 21:45:21 +01:00
Michael Eischer
59d9e20cbd Merge pull request #260 from restic/dependabot/go_modules/github.com/gorilla/handlers-1.5.2
Bump github.com/gorilla/handlers from 1.5.1 to 1.5.2
2023-11-11 21:32:21 +01:00
dependabot[bot]
6aa3c966b6 Bump github.com/spf13/cobra from 1.7.0 to 1.8.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 19:52:35 +00:00
dependabot[bot]
228082e5ea Bump github.com/gorilla/handlers from 1.5.1 to 1.5.2
Bumps [github.com/gorilla/handlers](https://github.com/gorilla/handlers) from 1.5.1 to 1.5.2.
- [Release notes](https://github.com/gorilla/handlers/releases)
- [Commits](https://github.com/gorilla/handlers/compare/v1.5.1...v1.5.2)

---
updated-dependencies:
- dependency-name: github.com/gorilla/handlers
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 19:52:31 +00:00
dependabot[bot]
8523f0c968 Merge pull request #255 from restic/dependabot/go_modules/github.com/prometheus/client_golang-1.17.0 2023-10-14 22:25:17 +00:00
dependabot[bot]
74b74ed325 Bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-14 22:23:04 +00:00
Michael Eischer
2dbad90967 Merge pull request #257 from restic/dependabot/go_modules/golang.org/x/crypto-0.14.0
Bump golang.org/x/crypto from 0.13.0 to 0.14.0
2023-10-15 00:22:26 +02:00
dependabot[bot]
04eacee642 Bump golang.org/x/crypto from 0.13.0 to 0.14.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.13.0 to 0.14.0.
- [Commits](https://github.com/golang/crypto/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-09 19:10:29 +00:00
dependabot[bot]
1b1b127490 Merge pull request #254 from restic/dependabot/go_modules/golang.org/x/crypto-0.13.0 2023-09-11 19:43:10 +00:00
dependabot[bot]
f8669a2eba Bump golang.org/x/crypto from 0.12.0 to 0.13.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/golang/crypto/compare/v0.12.0...v0.13.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 19:39:59 +00:00
dependabot[bot]
fb5d49c631 Merge pull request #252 from restic/dependabot/github_actions/actions/checkout-4 2023-09-06 20:31:03 +00:00
dependabot[bot]
e6efaaf65f Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 19:34:51 +00:00
Michael Eischer
8b90047951 Merge pull request #250 from restic/dependabot/go_modules/golang.org/x/crypto-0.12.0
Bump golang.org/x/crypto from 0.11.0 to 0.12.0
2023-08-13 18:59:15 +02:00
dependabot[bot]
29fea36169 Bump golang.org/x/crypto from 0.11.0 to 0.12.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.11.0 to 0.12.0.
- [Commits](https://github.com/golang/crypto/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-07 19:14:27 +00:00
Michael Eischer
0bb8cd41d1 Merge pull request #246 from eriksjolund/adjust_restrict_address_families
Improve security of systemd service rest-server.service by restricting network access
2023-07-23 12:16:34 +02:00
Erik Sjölund
ec2ce8cd27 Improve security of rest-server.service by restricting network access
This patch improves the overall security assessment score given by
`systemd-analyze security rest-server.service` from "1.3 OK" to "0.6 SAFE"
(when using systemd-analyze version 253)

* Remove `AF_INET AF_INET6` from RestrictAddressFamilies.
  Sockets originating from socket activation are not affected by the
  systemd directive RestrictAddressFamilies.
  See systemd.exec man page.

* Add `PrivateNetwork=yes`
  as recommended for socket-activated services in the systemd.socket man page

* Add dependency on rest-server.socket

Signed-off-by: Erik Sjölund <erik.sjolund@gmail.com>
2023-07-17 08:52:26 +02:00
Michael Eischer
c38e18b708 Let goreleaser use custom release notes
If the changelog generation is disabled in goreleaser, this also ignores
custom release notes passed in via the command line. Thus keep the
default changelog configuration.
2023-07-09 21:32:35 +02:00
Alexander Neumann
76759aa52e Update version for development 2023-07-09 17:21:54 +02:00
Alexander Neumann
4231ef6f20 Generate CHANGELOG.md for 0.12.1 2023-07-09 17:17:10 +02:00
Alexander Neumann
5b288d488a Move changelog files for 0.12.1 2023-07-09 17:16:24 +02:00
Alexander Neumann
46e3c7248b Set version 2023-07-09 17:15:15 +02:00
Michael Eischer
fdf65f66e7 Merge pull request #243 from MichaelEischer/upgrade-dependencies
upgrade dependencies
2023-07-09 14:22:35 +02:00
Michael Eischer
32ab845151 upgrade dependencies 2023-07-09 14:12:47 +02:00
Michael Eischer
4bf4b62379 Merge pull request #242 from restic/dependabot/go_modules/github.com/prometheus/client_golang-1.16.0
Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0
2023-07-01 18:40:04 +02:00
dependabot[bot]
bb99f5a426 Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.15.1...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 16:14:06 +00:00
Michael Eischer
38b712c714 Merge pull request #241 from restic/dependabot/go_modules/golang.org/x/crypto-0.10.0
Bump golang.org/x/crypto from 0.9.0 to 0.10.0
2023-07-01 18:13:27 +02:00
dependabot[bot]
5f3faad48d Bump golang.org/x/crypto from 0.9.0 to 0.10.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/crypto/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-19 20:02:25 +00:00
Michael Eischer
d6a268ca01 Merge pull request #240 from MichaelEischer/update-changelog-template
Update changelog template with changes from restic
2023-06-17 22:15:39 +02:00
Michael Eischer
d890dbad69 update changelog template with changes from restic 2023-06-17 22:08:58 +02:00
Michael Eischer
3130a4bcdf Merge pull request #239 from MichaelEischer/empty-list
Return empty array if there are no objects to list
2023-06-17 22:07:57 +02:00
Michael Eischer
ff81311a98 add changelog 2023-06-17 22:03:01 +02:00
Michael Eischer
9557efad55 Fix TestListWithUnexpectedFiles test
The test should test both API versions.
2023-06-17 21:57:58 +02:00
Michael Eischer
84a8b210f5 return empty array if there are no objects to list
Previously "null" was returned, which does not match the REST backend
specification.
2023-06-17 21:57:41 +02:00
Michael Eischer
ab45fb59ff Merge pull request #236 from restic/dependabot/go_modules/github.com/minio/sha256-simd-1.0.1
Bump github.com/minio/sha256-simd from 1.0.0 to 1.0.1
2023-05-30 23:13:31 +02:00
dependabot[bot]
3284c4ab73 Bump github.com/minio/sha256-simd from 1.0.0 to 1.0.1
Bumps [github.com/minio/sha256-simd](https://github.com/minio/sha256-simd) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/minio/sha256-simd/releases)
- [Commits](https://github.com/minio/sha256-simd/compare/v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: github.com/minio/sha256-simd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-29 20:01:27 +00:00
Michael Eischer
0cd077f4ab Merge pull request #231 from MichaelEischer/fix-fsync-warning
Fix inverted condition for fsync warning
2023-05-18 16:36:04 +02:00
Michael Eischer
64ab92761c Merge pull request #234 from restic/dependabot/go_modules/golang.org/x/crypto-0.9.0
Bump golang.org/x/crypto from 0.8.0 to 0.9.0
2023-05-18 16:33:54 +02:00
dependabot[bot]
8ec316cea3 Bump golang.org/x/crypto from 0.8.0 to 0.9.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/crypto/compare/v0.8.0...v0.9.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-15 20:00:07 +00:00
Michael Eischer
420c4c6683 Add changelog for fsync warning 2023-05-13 22:05:18 +02:00
Michael Eischer
be14687a9c Print fsync warning only once
The repo.Handler is freshly instantiated for every request such that it
forget that the fsync warning was already printed. Use a single instance
in the Server instead.
2023-05-13 21:50:39 +02:00
Michael Eischer
dbb2d4690c Merge pull request #232 from restic/dependabot/go_modules/github.com/prometheus/client_golang-1.15.1
Bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1
2023-05-13 21:43:53 +02:00
Michael Eischer
0a09c8d633 Improve reproducibility of built archives
The binaries and generate archives should be reproducible now. Note that
this does not apply to the source tarball.
2023-05-10 06:42:01 +02:00
Michael Eischer
dedcb846df Enable multi-platform container builds in goreleaser
This requires `docker-buildx` and `qemu-user-static-binfmt`
2023-05-10 06:42:01 +02:00
dependabot[bot]
781d2241e0 Bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.0 to 1.15.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.15.0...v1.15.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 20:01:25 +00:00
Michael Eischer
b2e8044fbd Fix inverted condition for fsync warning
The warning should only be printed if fsync is _not_ supported and not
the other way around.
2023-05-08 21:40:23 +02:00
Michael Eischer
30ec84fcb7 Merge pull request #228 from MichaelEischer/update-ci
Update CI configuration and verify changelog entries
2023-05-05 23:05:22 +02:00
Michael Eischer
2a3bca1633 Fix type of changelog entry 2023-04-30 15:21:16 +02:00
Michael Eischer
aeb5e2982f CI: Automatically check changelog entries 2023-04-30 15:18:44 +02:00
Michael Eischer
20c4cdedfc CI: Sync tests with restic 2023-04-30 15:18:28 +02:00
Michael Eischer
c064e4c1ed Merge pull request #217 from HeikoSchlittermann/feat-log-to-stdout
feat: allow logging to stdout, stderr
2023-04-30 14:56:29 +02:00
Michael Eischer
66fe4afb7d Make changelog less technical 2023-04-30 14:51:46 +02:00
Michael Eischer
4576e1bc12 Fix docker container setup for goreleaser
The expected approach is that the binary built by goreleaser is added to
the docker container.
2023-04-28 20:41:30 +02:00
Michael Eischer
e8a839673f Set development version for 0.12.0 2023-04-27 21:41:32 +02:00
Michael Eischer
b34b9f0780 create folder for unreleased changelogs 2023-04-26 21:09:55 +02:00
Alexander Neumann
543d313f7e Fix Changelog file 2023-04-26 21:08:58 +02:00
Alexander Neumann
a98ed25c7b Rename changelog 2023-04-24 21:02:21 +02:00
Alexander Neumann
efe070c66d Add changelog, fix VERSION 2023-04-24 20:59:03 +02:00
Leo R. Lundgren
834a3378e9 doc: Polish changelogs 2023-04-24 13:15:03 +02:00
Heiko Schlittermann (HS12-RIPE)
9f074d8b3a feat: allow logging to stdout
The --log option accepts "-" as filename. This prevents rest-server from
opening the log file, it simply writes to the STDOUT stream provided by
the caller.

**BREAKING** in case use really used "-" to specify a file named "-"
you'll need to update your rest-server invocation to use "./-".
2023-04-24 11:15:38 +02:00
Leo R. Lundgren
94d5861c50 doc: Polish changelogs 2023-04-23 22:28:14 +02:00
Michael Eischer
253bebb096 Merge pull request #225 from restic/dependabot/go_modules/github.com/prometheus/client_golang-1.15.0
Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.0
2023-04-22 11:23:51 +02:00
Michael Eischer
337035c414 Merge pull request #221 from MichaelEischer/skip-files-in-intermediate-directories
Ignore unexpected files in intermediate directories
2023-04-22 11:20:11 +02:00
dependabot[bot]
399f7f1d49 Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-17 20:04:17 +00:00
Michael Eischer
afcdb2f312 Merge pull request #224 from restic/dependabot/go_modules/github.com/spf13/cobra-1.7.0
Bump github.com/spf13/cobra from 1.6.1 to 1.7.0
2023-04-11 22:42:58 +02:00
dependabot[bot]
f97c48d92e Bump github.com/spf13/cobra from 1.6.1 to 1.7.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.6.1...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-11 20:39:21 +00:00
Michael Eischer
da9ce53a95 Merge pull request #223 from restic/dependabot/go_modules/golang.org/x/crypto-0.8.0
Bump golang.org/x/crypto from 0.7.0 to 0.8.0
2023-04-11 22:38:43 +02:00
dependabot[bot]
d795b70b9f Bump golang.org/x/crypto from 0.7.0 to 0.8.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 20:04:10 +00:00
Michael Eischer
3a23555594 Merge pull request #202 from deajan/patch-1
Make dashboard portable
2023-04-08 21:22:22 +02:00
Michael Eischer
22a6412b81 Ignore unexpected files in intermediate directories
Listing the data/ folder in a repository no longer fails if it contains
files in the data/ folder. This also ignore .DS_Store files created by
macOS.
2023-04-08 20:17:44 +02:00
Michael Eischer
11c5a548e8 Merge pull request #216 from restic/dependabot/go_modules/golang.org/x/crypto-0.7.0
Bump golang.org/x/crypto from 0.6.0 to 0.7.0
2023-04-08 19:34:33 +02:00
Michael Eischer
c3d3bfbc12 Merge pull request #218 from restic/dependabot/github_actions/actions/setup-go-4
Bump actions/setup-go from 3 to 4
2023-04-07 12:35:13 +02:00
dependabot[bot]
f2f2d29064 Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 20:04:09 +00:00
dependabot[bot]
dd0f03c9bb Bump golang.org/x/crypto from 0.6.0 to 0.7.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 20:06:54 +00:00
Michael Eischer
ea461a2d76 Merge pull request #214 from restic/dependabot/go_modules/golang.org/x/crypto-0.6.0
Bump golang.org/x/crypto from 0.5.0 to 0.6.0
2023-02-19 13:54:43 +01:00
dependabot[bot]
40ad68bc5f Bump golang.org/x/crypto from 0.5.0 to 0.6.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 20:09:40 +00:00
Michael Eischer
8f3b92325f Merge pull request #213 from 0xpr03/patch-1
warn about SystemCallArchitectures in the systemd service
2023-02-10 21:44:18 +01:00
Aron Heinecke
fb6a3a62cf warn about SystemCallArchitectures
Running 32bit on a 64bit host with SystemCallArchitectures will fail without much information
2023-02-01 00:10:46 +01:00
Michael Eischer
667ce6e26b Merge pull request #210 from ph818/patch-1
add MemoryHigh to systemd example
2023-01-26 22:36:29 +01:00
ph818
091e70d903 add MemoryHigh to systemd example 2023-01-21 21:43:56 -05:00
Michael Eischer
f5c06a2e45 Merge pull request #208 from MichaelEischer/upgrade-dependencies
Upgrade dependencies
2023-01-20 22:42:58 +01:00
Michael Eischer
40e2a8b1e4 add changelog and update readme 2023-01-20 22:36:40 +01:00
Michael Eischer
fa795cc66a upgrade go-systemd to latest major version 2023-01-14 21:00:32 +01:00
Michael Eischer
99ec5e2dbb CI: update and sync ci tasks with restic 2023-01-14 20:39:32 +01:00
Michael Eischer
f299c735df bump minimum go version to 1.17 2023-01-14 19:22:08 +01:00
Michael Eischer
8ce88b24e7 enable dependabot 2023-01-14 19:18:10 +01:00
Michael Eischer
2eae8c9266 update transitive dependencies 2023-01-14 19:17:53 +01:00
Michael Eischer
30fbd043b9 update direct dependencies 2023-01-14 19:09:42 +01:00
Michael Eischer
7f29dcbd69 Merge pull request #207 from MichaelEischer/err-on-args
Error out on unexpected command line argument
2023-01-12 22:00:36 +01:00
Michael Eischer
43c96fb6f2 Error out on unexpected command line argument
rest-server doesn't accept arguments. Thus, error out to prevent wrong
usage.
2023-01-11 21:47:25 +01:00
Orsiris de Jong
c1c48c62e9 Make dashboard portable
Allows to select datasource when first importing into grafana
2022-12-28 19:44:32 +01:00
Michael Eischer
a8cd3f218d Merge pull request #199 from MichaelEischer/support-macos-ventura
Ignore fsync errors
2022-12-16 23:13:43 +01:00
Michael Eischer
80babf98e7 add fsync warning 2022-12-02 21:08:15 +01:00
Michael Eischer
408dcab92e Port fsync error handling from restic
This ignores several different combinations of errnos which are returned
if the storage destination is not able to fsync correctly.

See also https://github.com/restic/restic/pull/4021
2022-11-11 22:37:02 +01:00
Michael Eischer
2dd87ced0a Merge pull request #195 from MichaelEischer/internal-error-for-inaccessible-files
Return "internal server error" if files cannot be read
2022-10-07 22:53:41 +02:00
Michael Eischer
f763db8934 Deduplicate handler initialization in tests 2022-09-02 23:41:05 +02:00
Michael Eischer
a8fdca3b9f make deleting idempotent 2022-09-02 23:32:52 +02:00
Michael Eischer
b562edefd1 add changelog for not found error handling 2022-08-31 22:29:14 +02:00
Michael Eischer
cb2afaa4c0 test that inaccessible files result in status 'internal error' 2022-08-31 22:29:14 +02:00
Michael Eischer
991c459be3 Only return "Not Found" status if file does not exist
All other errors are now turned into a internal server error and are
logged.
2022-08-31 22:29:14 +02:00
MichaelEischer
9f4fba2c21 Merge pull request #138 from MichaelEischer/cache-basic-auth
Cache basic auth credentials
2022-07-02 21:19:46 +02:00
Michael Eischer
65fd8be3f8 Add changelog for cached basic auth 2022-07-02 21:17:57 +02:00
Michael Eischer
274f29fee8 refactor password check into separate function 2022-06-21 00:18:05 +02:00
Michael Eischer
98f0aaca1c convert htpasswd regexes to globals 2022-06-21 00:18:05 +02:00
Michael Eischer
5a6ed2ffdf use constant time comparison for sha1 password hash 2022-06-21 00:18:05 +02:00
Michael Eischer
46b020fd9e Add basic test 2022-06-21 00:18:05 +02:00
Michael Eischer
df9eb337d3 Extend htpasswd auth cache entry expiry on use 2022-06-21 00:18:05 +02:00
Michael Eischer
1eeca53812 Try to zero htpasswd cache entries before deletion 2022-06-21 00:18:05 +02:00
Michael Eischer
0bdc420e75 Cache successful basic auth credentials for a minute
This stores a hash of the username + password in map which is indexed by
the username. Indexing by username avoids accidentally introducing a
timing side-channel as a successful/failed lookup only provides
information on whether a cache entry exists for a username or not.

Hashing the username and password together makes it simple to get a
constant-time string comparison as we no longer have to worry about
string length differences.

Expriy is done by a goroutine which every few seconds checks for expired
cache entries and removes those.
2022-06-21 00:18:05 +02:00
Michael Eischer
b0036d006b Unexport users map in htpasswd struct 2022-06-21 00:18:05 +02:00
rawtaz
6bc87b8e95 Merge pull request #188 from dwmunster/f-config-htpasswd
Add configurable htpasswd file location
2022-06-20 23:40:49 +02:00
Alexander Neumann
d24ffc13d8 Fix tests 2022-04-15 09:38:23 +02:00
Alexander Neumann
a87a50ad11 Sync settinsg with restic, require Go >= 1.15 2022-04-15 09:38:23 +02:00
Alexander Neumann
8538ce7859 Copy golangci-lint settings from restic 2022-04-15 09:28:56 +02:00
Drayton Munster
87cef8f159 Clarify file accessibility for PASSWORD_FILE in docker 2022-03-25 22:36:54 -04:00
Drayton Munster
bc1545c717 Clarifying descriptions as suggested in PR review 2022-03-25 22:25:48 -04:00
Drayton Munster
3903ed000c Add configurable htpasswd location 2022-03-25 15:39:20 -04:00
MichaelEischer
cb85fb38c0 Merge pull request #184 from lgommans/patch-1
Be more clear in --help what the value of --log is supposed to be
2022-02-18 21:04:07 +01:00
Luc Gommans
1fd9538f73 Reword --log option in --help output 2022-02-17 23:33:42 +01:00
rawtaz
af36b77ece Merge pull request #183 from MichaelEischer/allow-underscore
Allow underscores in usernames
2022-02-12 21:18:05 +01:00
Michael Eischer
48067dc896 htpasswd: allow underscores in usernames 2022-02-12 21:13:40 +01:00
Alexander Neumann
096ac5a9c8 Remove plan9, rest-server fails to build 2022-02-10 20:02:10 +01:00
Alexander Neumann
4741eec619 Generate CHANGELOG.md for 0.11.0 2022-02-10 19:53:31 +01:00
Alexander Neumann
057ef39525 Move changelog files for 0.11.0 2022-02-10 19:53:14 +01:00
Alexander Neumann
b739e22b04 Update VERSION files for 0.11.0 2022-02-10 19:53:08 +01:00
Alexander Neumann
e9900b7a00 Merge pull request #171 from MichaelEischer/fix-file-permissions
Honour repo.FileMode permissions
2022-02-10 19:51:57 +01:00
Alexander Neumann
959250f543 Fix changelog file 2022-02-10 19:51:40 +01:00
Alexander Neumann
9e4442805e Merge pull request #181 from MichaelEischer/reword-changelogs
Reword changelogs
2022-02-10 19:51:09 +01:00
Michael Eischer
228d5f6051 Reword changelogs 2022-02-10 19:48:44 +01:00
Alexander Neumann
6f386876e7 Update build.go from restic 2022-02-10 19:25:41 +01:00
Alexander Neumann
7a3b9e4000 Merge pull request #180 from MichaelEischer/update-dependencies
Update dependencies
2022-02-10 19:13:22 +01:00
Michael Eischer
aaf4f4b92a Update dependencies
This also cleans up the indirect dependencies.
2022-02-08 22:55:00 +01:00
rawtaz
bf42a509ab Merge pull request #178 from JsBergbau/master
Readme: Fixed SSL certificate generation
2022-01-27 17:05:05 +01:00
JsBergbau
a7eecf40bd Fix and description update for certificate generation
Fixed SSL certificate generation
2022-01-27 16:26:21 +01:00
MichaelEischer
421da62900 Merge pull request #156 from telenieko/patch-1
Clarification of docker section in README.md
2021-10-31 19:33:41 +01:00
Marc Fargas
12cf4bbc05 Clarification of docker section in README.md
Refs #128, the two distinct docker sections of the README are merged into one.
2021-10-28 13:37:18 +00:00
Michael Eischer
51ab8e98e2 fix file permission handling 2021-09-24 23:10:16 +02:00
MichaelEischer
9f8c31b968 Merge pull request #158 from Enrico204/use-os-tempdir-for-temporary-directory
Use os.TempDir() for temporary directory in default path
2021-09-12 21:30:59 +02:00
Michael Eischer
f952bc7344 Tweak readme 2021-09-12 21:27:45 +02:00
Enrico204
223520b964 Use os.TempDir() for temporary directory in default path 2021-09-12 21:27:45 +02:00
MichaelEischer
1172d7e068 Merge pull request #160 from Enrico204/reply-with-insufficient-storage-on-disk-full
Reply "insufficient storage" on disk full or over-quota
2021-09-07 21:28:07 +02:00
Enrico204
fb5d63435a Fix tests for: reply "insufficient storage" on disk full or over-quota 2021-09-06 22:32:07 +02:00
MichaelEischer
8729a699a1 Merge pull request #164 from networkException/master
Config: Read in PrometheusNoAuth correctly
2021-09-04 13:27:46 +02:00
networkException
f373e45bc8 Config: Read in PrometheusNoAuth correctly
Previously setting --prometheus-no-auth flag would override --prometheus
2021-09-04 01:09:41 +02:00
MichaelEischer
8642729a51 Merge pull request #163 from buschjost/optimized-sha256-lib
Use Minio's optimized SHA-256
2021-08-31 21:04:25 +02:00
Oliver Buschjost
f61292b00d Use Minio's optimized SHA-256 2021-08-27 18:21:44 +02:00
rawtaz
a09ba203bd Merge pull request #161 from ubitux/readme-cleanup
Readme cleanup
2021-08-27 14:47:18 +02:00
Clément Bœsch
ac8e95c8a4 README: propose a single openssl command instead of 2 2021-08-27 13:45:58 +02:00
Clément Bœsch
f8db131226 README: remove trailing whitespaces 2021-08-27 13:45:58 +02:00
Clément Bœsch
101ad07999 README: update usage output with recent changes 2021-08-27 13:45:58 +02:00
Clément Bœsch
dbf002296b README: set language for command block quotes
Better semantic, allowing syntax coloring when appropriate (such as
strings argument in case of sh).
2021-08-27 13:45:56 +02:00
MichaelEischer
5be12cecbf Merge pull request #143 from MichaelEischer/docker-create-bcrypt
docker: use bcrypt in create_users script
2021-08-23 20:10:38 +02:00
Enrico204
9b31f17188 Add unreleased changelog entry for pull request 160 2021-08-23 12:50:12 +02:00
Enrico204
173bfb5371 Reply "insufficient storage" on disk full or over-quota
This commit will change the current behavior on disk-related errors:
* HTTP 507 "Insufficient storage" is the status on disk full or
over-quota
* HTTP 500 "Internal server error" on other disk-related errors
previously both were 400 "Bad request"
2021-08-23 12:44:09 +02:00
Michael Eischer
20edfb87ee docker: use bcrypt in create_users script
This was missed when adding bcrypt support in rest-server 0.9.7
2021-08-20 22:52:03 +02:00
Alexander Neumann
d2813ea61b Merge pull request #151 from restic/add-socket-activation
Support running on demand via systemd
2021-08-17 21:39:42 +02:00
Alexander Neumann
f90205eefe Support running on demand systemd socket activation 2021-08-17 21:37:02 +02:00
Alexander Neumann
32784a3072 Run tests on Go 1.17 2021-08-17 21:35:39 +02:00
Alexander Neumann
05773795dd Merge pull request #142 from MichaelEischer/atomic-upload
Atomic file upload and directory sync
2021-08-17 20:32:33 +02:00
Alexander Neumann
0bd1f612d2 Merge pull request #154 from JsBergbau/master
Updated self-signed certificate creation
2021-08-17 20:27:39 +02:00
JsBergbau
a5b306e65b Updated self-signed certificate creation 2021-08-17 11:26:23 +02:00
Michael Eischer
64a43228de Prefix temporary file with object id 2021-08-12 22:17:49 +02:00
Michael Eischer
28f569c0df Add changelog 2021-08-12 22:15:08 +02:00
Alexander Neumann
04d206303c Add test for race condition with aborted connection 2021-08-12 22:15:08 +02:00
Alexander Neumann
e6cc79a2ec Fix comment 2021-08-12 22:14:36 +02:00
Alexander Neumann
7fe16b69b2 Mark helper functions 2021-08-12 22:14:36 +02:00
Michael Eischer
ec0766cddd Don't sync directory on Windows
Calling sync on a directory on Windows just returns "The handle is invalid"
and fails.
2021-08-12 22:14:36 +02:00
Michael Eischer
2175029c9e Sync directory to disk after upload
After a file was uploaded, also sync its containing directory to disk to
make sure that also the directory entry is persisted after a system
crash.
2021-08-12 22:08:08 +02:00
Michael Eischer
82816c67e1 Atomic upload for blobs
A upload is now first saved to a temporary file before it gets renamed
to the final filename. This ensures that incomplete uploads don't leave
broken files behind (at least not under their real filename).

In addition, removing failed uploads is no longer prone to a race
condition with a retried upload. That scenario could occur when the
first upload fails partway and the server doesn't notice that
immediately. A later retry by restic will then delete the broken upload
and upload the file again. If the server notices now that the initial
upload has failed, then it will delete the correctly uploaded file.

This has been fixed by only ever deleting the temporary file during
upload.
2021-08-12 22:06:08 +02:00
Alexander Neumann
4db46a5d3d Check error 2021-08-11 14:32:10 +02:00
Alexander Neumann
39839cfac4 Merge pull request #135 from MichaelEischer/cpu-profile-shutdown
Properly close CPU profile on SIGINT
2021-08-11 14:27:52 +02:00
Alexander Neumann
5e71f61ae8 Merge pull request #130 from MichaelEischer/verify-upload
Verify uploaded files
2021-08-11 14:21:51 +02:00
Alexander Neumann
4c8a076976 Reword changelog 2021-08-09 16:28:09 +02:00
Michael Eischer
16889717c6 Add option to disable integrity check on upload 2021-08-09 15:40:50 +02:00
Michael Eischer
54adcb1fc7 Verify uploaded files
Restic uses the sha256 hash to calculate filenames based on the file
content. Check on the rest-server side that the uploaded file is intact
and reject it otherwise.
2021-08-09 15:35:13 +02:00
Alexander Neumann
96a6f0a5c4 Use Cobra for showing the version 2021-08-09 11:31:54 +02:00
Alexander Neumann
28c0b95b8a Upgrade dependencies 2021-08-09 11:31:51 +02:00
Alexander Neumann
d39bc8e6cf Merge pull request #112 from wojas/split-repo-handler
Split Server component and add support for subrepositories
2021-08-09 10:55:29 +02:00
Alexander Neumann
034302de95 Remove goji dependency 2021-08-09 10:50:44 +02:00
Alexander Neumann
ba9ee5c625 Address linter issues 2021-08-09 10:49:41 +02:00
Alexander Neumann
4e36854cd4 Fix typo 2021-08-09 10:49:41 +02:00
Konrad Wojas
e3b1c5d612 Metrics: require user 'metrics' for private repo mode
To match previous behaviour, require username 'metrics' when private
repo mode is enabled.
2021-08-09 10:49:41 +02:00
Konrad Wojas
aef955c513 Update readme 2021-08-09 10:49:41 +02:00
Konrad Wojas
ff88e6812d Update readme and changelog 2021-08-09 10:49:41 +02:00
Konrad Wojas
9db2d52fbe Prometheus: keep auth by default
Restore the previous behaviour where the Prometheus /metrics endpoint
required auth if auth was enabled.

A new -prometheus-no-auth flag allows you to override this and disable
auth for that specific endpoint.
2021-08-09 10:49:41 +02:00
Konrad Wojas
32c138aa84 Add tests for subrepos 2021-08-09 10:49:41 +02:00
Konrad Wojas
73a6000f10 Fix typo in error message 2021-08-09 10:49:41 +02:00
Konrad Wojas
e957e42336 Return more useful error 2021-08-09 10:49:41 +02:00
Konrad Wojas
c2958906ea Fix TestSplitURLPath 2021-08-09 10:49:41 +02:00
Konrad Wojas
63c8797ba3 Fix tests, add PanicOnError option
- Helper method for internal server errors with consistent logging.
- Add PanicOnError option to panic on internal server errors. This
  makes it easier to traces where the condition was hit in testing.
2021-08-09 10:49:41 +02:00
Konrad Wojas
d4cd47e503 Minor cleanup and fixes
- Do not allow '.' as path component, because it undermines depth
  checks, and add tests
- Fix GiB reporting
- Fix metrics label
- Helper function for http errors
2021-08-09 10:49:41 +02:00
Konrad Wojas
1f593fafaf Make Server use the new repo.Handler
This contains all the glue to make Server use the new repo.Handler:

- Remove all old handlers
- Add ServeHTTP to make Server a single http.Handler
- Remove Goji routing and replace by net/http and custom routing logic

Additionally, this implements two-level backup repositories.
2021-08-09 10:49:41 +02:00
Konrad Wojas
55e549e92c Move maxsizewriter to quota package 2021-08-09 10:49:41 +02:00
Konrad Wojas
79a8785e26 Implement repo.Handler
Refactor the old HTTP handlers to fit the purpose of the new
http.Handler:

- repo.New function to instantiate a handler for a single repo (can be done
  dynamically for every request)
- Single ServeHTTP entrypoint
- Move quota management to two methods that will be implemented later
  (stubs for now)
- Move metrics update to an external function (BlobMetricFunc type)
- Use constants and options for file modes
2021-08-09 10:49:41 +02:00
Konrad Wojas
7f14414363 Add repo package with copy of old handlers.go
Copy the old handlers.go without changes for cleaer diff of
changes later.
2021-08-09 10:49:41 +02:00
Alexander Neumann
bcbfff7b62 Update Go and golangci-lint version 2021-08-09 10:48:58 +02:00
MichaelEischer
c36ae5fe03 Merge pull request #149 from tim-seoss/systemd-unit-file-enhancement
Improve security of example systemd unit file
2021-06-05 14:37:42 +02:00
Tim Small
2bf01df6bf Fixup changelog entry based on feedback. 2021-05-31 11:46:31 +01:00
Tim Small
d1e56e80ee Remove ProcSubset=pid to allow access to /proc/stat etc. 2021-05-31 11:46:31 +01:00
Tim Small
4967dcbf74 Document and sign-post additional systemd resource control options.
The systemd administrator may wish to use additional resource control
facilities which systemd provides. Document the existence of these, and
provide some example options in commented form.
2021-05-31 11:46:31 +01:00
Tim Small
8a1535ba0c Improve commenting of systemd unit file based on review. 2021-05-31 11:46:06 +01:00
MichaelEischer
1ca9ca7e50 Merge pull request #124 from lwis/update-docker
Fix Docker configuration for DISABLE_AUTHENTICATION
2021-05-15 18:39:42 +02:00
Michael Eischer
766f1e0c00 Revert dockerfile changes and cleanup changelog 2021-05-15 18:31:01 +02:00
Lewis Juggins
cec241e5e1 Fix Docker configuration for authentication 2021-05-15 18:28:31 +02:00
MichaelEischer
a44c025cd3 Merge pull request #145 from jinnko/master
Build restic at container build time
2021-05-13 20:00:20 +02:00
Michael Eischer
73fb6419ff Tweak changelog 2021-05-13 19:49:25 +02:00
Tim Small
05a5d1f94e Improve security of example systemd unit file
The supplied systemd unit file places some basic security restrictions
on the rest service.  This patch enhances those, and improves the
overall security assessment score given by `systemd-analyze security`
from "8.3 EXPOSED" to "1.3 OK".

Closes #148
2021-04-03 08:18:45 +01:00
Jinn Koriech
3a4d901f6d Docker: build rest-server at container build time
Using docker's multi-stage builds we can build the restic/rest-server
within a golang build environment then create a container for use
(without the build environment) in a second build stage.

The advantages are:

1. Building the rest-server is predictable in a pristine environment
   each time.
2. Container builds ensure we get the latest rest-server every time.

Updated README with details of new docker build approach, and added
changelog for unreleased changes.
2021-03-28 12:52:33 +01:00
Alexander Neumann
10dc7a4a86 Fix link to status badge 2021-01-31 13:19:40 +01:00
Alexander Neumann
e3aad65559 Rework CI tests 2021-01-31 13:15:59 +01:00
Alexander Neumann
980bff189e Add config for golangci-lint 2021-01-31 13:15:53 +01:00
Alexander Neumann
a659f3d879 Merge pull request #137 from wojas/htpasswd-numbers
Allow numbers in htpasswd usernames
2021-01-04 17:11:02 +01:00
Konrad Wojas
83e78c6cd7 Allow numbers in htpasswd usernames 2021-01-05 00:08:26 +08:00
Michael Eischer
0a6e0dbdf9 Properly close CPU profile on sigint
The rest server is normally shutdown via a SIGINT signal. The http
handle calls are endless loops and don't return in the normal case. Thus
add a signal handler to shutdown the profiler.
2021-01-03 20:16:54 +01:00
Alexander Neumann
4c5077769c Merge pull request #132 from juergenhoetzel/blacklist-slash
Security: Prevent loading of usernames containing a slash
2020-12-28 16:52:43 +01:00
Juergen Hoetzel
33c41b55bb Security: Prevent loading of usernames containing a slash
"/" is valid char in HTTP authorization headers, but is also used in
rest-server to map usernames to private repos.

This commit prevents loading maliciously composed usernames like
"/foo/config" by restricting the allowed characters to the unicode
character class, numbers, "-", "." and "@".

Closes #131
2020-12-28 11:30:00 +01:00
Alexander Neumann
ba581f22ed Merge pull request #120 from andreaso/prefer-https-links
Prefer https:// links in the documentation
2020-09-14 16:32:12 +02:00
Andreas Olsson
0155ba8a1b Prefer https:// links in the documentation 2020-09-13 17:09:47 +02:00
Alexander Neumann
037fe06973 Merge pull request #118 from andreaso/restrict-systemd-service
Make example systemd service more restrictive
2020-09-13 16:13:37 +02:00
Andreas Olsson
a994d347ca Make example systemd service more restrictive
In addition to any existing filesystem restrictions on the (www-data)
backup user these config options uses namespaces and other kernel
features to further restrict what the _rest-server_ is allowed to do.

* `ProtectSystem=strict` and `ReadWritePaths=/path/to/backups` ensures
  that the _rest-server_ is only allowed to write to its data directory.

* `ProtectHome=yes` and `PrivateTmp=yes` limits what the _rest-server_
  gets (read) access to.

* `NoNewPrivileges=yes` prevents the _rest-server_ from using setuid
  binaries, etc to escalate its privileges.

See https://www.freedesktop.org/software/systemd/man/systemd.exec.html
for further details

While at I also replaced the _/tmp/restic_ path with a more explicit
placeholder path. Given that one rarely wants to backup to _/tmp_ I
figured it better to force a choice of path rather than to have
someone accidentally end up using _/tmp/restic_ for their backups.
2020-09-13 14:21:32 +02:00
Alexander Neumann
14bbb9c18a Fix URLs 2020-09-13 12:14:14 +02:00
Alexander Neumann
53576a1454 Update changelog template for rest-server 2020-09-13 12:12:58 +02:00
Alexander Neumann
4b8ef2cbd7 Fix tests 2020-09-13 12:08:46 +02:00
Alexander Neumann
65fb54cbca Update version for development 2020-09-13 11:45:09 +02:00
Alexander Neumann
9313f19441 Generate CHANGELOG.md for 0.10.0 2020-09-13 11:24:29 +02:00
Alexander Neumann
d4b929ef35 Move changelog files for 0.10.0 2020-09-13 11:24:26 +02:00
Alexander Neumann
13f56bbb3c Update VERSION files for 0.10.0 2020-09-13 11:24:22 +02:00
Alexander Neumann
ecfa514256 Fix goreleaser config for version 2020-09-13 11:22:59 +02:00
Alexander Neumann
c7a44dd1a2 Fix release documentation 2020-09-13 11:22:59 +02:00
Alexander Neumann
4171164a39 Write version into main.go 2020-09-13 11:19:30 +02:00
Alexander Neumann
fa516da2c4 Reformat Release.md 2020-09-13 11:19:30 +02:00
Alexander Neumann
6e44dd8eae Add config for goreleaser, document release process 2020-09-13 11:19:30 +02:00
Alexander Neumann
20603b1622 Remove old changelog
We've had an intermediate release (0.9.8) not covered by the changelog,
so let's start properly in 0.10.0.
2020-09-13 11:19:30 +02:00
Alexander Neumann
1488830de1 Add entry to changelog 2020-09-13 11:19:26 +02:00
Alexander Neumann
723f29e594 Cleanup path before auth check 2020-09-13 11:19:26 +02:00
Konrad Wojas
f8e774393c Stricter path sanitization
Goji routes incoming requests without first URL decoding the path, so
'%2F' in a URL will not be decoded to a '/' before routing. But by the
time that we perform the path checks for private urls on r.URL.Path,
these characters have been decoded.

As a consequence, a user 'foo' could use 'foo%2Fbar' as the repo name.
The private repo check would see that the path starts with 'foo/' and
allow it, and rest-server would happily create a 'foo/bar' repo. Other
more harmful variants are possible.

To resolve this issue, we now reject any name part that contains a '/'.

Additionally, we immediately reject a few other characters that are
disallowed under some operating systems or filesystems.
2020-09-13 11:19:26 +02:00
Alexander Neumann
6367043b2c Also run linters and tests on PRs 2020-09-13 11:16:17 +02:00
Alexander Neumann
6e44ec0763 Replace Travis with GitHub Actions 2020-09-13 11:13:35 +02:00
Alexander Neumann
06f8484400 Docker: Don't delete htpasswd file 2020-09-12 17:28:18 +02:00
Alexander Neumann
1629c824c9 Add config for GitHub 2020-09-12 17:02:11 +02:00
Leo R. Lundgren
fd635e3965 Merge branch 'jtagcat-issue-template' based on pull request #105 from jtagcat/master 2020-05-06 15:00:49 +02:00
jtagcat
8300e75c77 Issue templates: how to get version using docker 2020-05-06 14:56:06 +02:00
rawtaz
f9fcc40305 Merge pull request #101 from ProactiveServices/patch-1
Update systemd unit file to current standards
2020-04-12 20:24:34 +02:00
rawtaz
fcf9220630 Add maintaner edit checkbox to PR template 2020-04-12 19:51:03 +02:00
Adam Piggott
c74c36e175 Tweak systemd unit file
The directive "StartLimitInterval" has been replaced by [StartLimitIntervalSec=interval, StartLimitBurst=burst](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#StartLimitIntervalSec=interval). I'd suggest that the default backoff settings are fine (in Ubuntu 19.10 no more than 5 restarts per 10 seconds, else delayed by 10 seconds per attempt) so this directive can simply be removed.
2020-04-12 18:29:39 +01:00
Leo R. Lundgren
b7b5d32538 doc: Fix incorrect URL for private repos in README.md 2020-04-12 14:30:42 +02:00
Alexander Neumann
3fcbbc7b65 Merge pull request #106 from restic/remove-vendor
Remove vendored dependencies
2020-04-04 21:24:41 +02:00
Alexander Neumann
27264c0a7a Fix changelog template 2020-04-04 21:13:07 +02:00
Alexander Neumann
c69d473fa5 Add changelog 2020-04-04 21:13:07 +02:00
Alexander Neumann
687804a02b Update README, require Go >= 1.11 2020-04-04 21:13:07 +02:00
Alexander Neumann
59afaed1a6 Update Travis 2020-04-04 21:13:07 +02:00
Alexander Neumann
9ae066589d Fix build.go 2020-04-04 20:41:32 +02:00
Alexander Neumann
46fd57c36e Remove vendored dependencies 2020-04-04 20:41:24 +02:00
Alexander Neumann
0cfe4320c0 Update Go version for Travis 2020-02-26 21:35:20 +01:00
Alexander Neumann
f3408b3e46 Convert to Go Modules 2020-02-26 21:34:33 +01:00
rawtaz
d9757b2022 Update PR template to encourage preceding issues. 2019-12-26 20:52:04 +01:00
rawtaz
35e3a30949 Merge pull request #81 from qbit/reload
Reload htpasswd on SIGHUP
2019-12-19 00:32:23 +01:00
rawtaz
947374fbfb Merge pull request #86 from rafacouto/bugfix-issue#85
Fix docker create_user script error when reading password from command line.
2019-12-18 23:55:23 +01:00
Rafa Couto
13cae78c8f Patch to issue #85 (Docker create_user script error when reading password as argument). 2019-12-18 23:53:36 +01:00
rawtaz
a48d6947d9 Merge pull request #98 from rawtaz/95-templates
Add templates for bug and feature issues as well as PRs.
2019-12-18 23:19:20 +01:00
rawtaz
9a62754e15 Merge pull request #97 from rawtaz/96-persist-unreleased
Add .gitkeep to persist changelog/unreleased/ when empty.
2019-12-18 23:18:36 +01:00
Leo R. Lundgren
527c7ab1c8 Add templates for bug and feature issues as well as PRs. 2019-12-18 23:17:13 +01:00
Leo R. Lundgren
6ebedcc0b2 Add .gitkeep to persist changelog/unreleased/ when empty. 2019-12-18 23:14:09 +01:00
Aaron Bieber
f18a5c16be reload htpasswd on SIGHUP 2019-03-04 16:55:29 -07:00
Matt Holt
a87d968870 Add --max-size flag to limit size of repositories (#72)
* Add --max-size flag to limit repository size

* Only update repo size on successful write

* Use initial size as current size for first SaveBlob

* Apply LimitReader to request body

* Use HTTP 413 for size overage responses

* Refactor size limiting; do checks after every write

* Remove extra commented lines, d'oh

* Account for deleting blobs when counting space usage

* Remove extra commented line

* Fix unrelated bug (inverted err check)

* Update comment to trigger new CI build
2018-06-14 15:53:29 -06:00
Alexander Trost
6f412e6a8a Exclude /metrics path from private repos check (#69)
Signed-off-by: Alexander Trost <galexrt@googlemail.com>
2018-06-14 15:53:12 -06:00
Alexander Neumann
420b1d3ee8 Merge pull request #67 from mholt/master
Refactor handlers: make Config not global
2018-06-11 22:04:33 +02:00
Alexander Neumann
9cda1814b6 Update URL for Travis 2018-05-08 20:42:24 +02:00
Matthew Holt
df3b6aa1cf Rename Config to Server and use singular one in main 2018-04-15 08:31:50 -06:00
Matthew Holt
b98c171644 Refactor handlers: make Config not global 2018-04-12 19:55:44 -06:00
Alexander Neumann
7dd5483ea3 Merge pull request #64 from restic/fix-append-only
Security: Refuse overwriting the config file in append-only mode
2018-04-02 13:25:46 +02:00
Alexander Neumann
0f4f747b74 Add entry to changelog 2018-04-02 13:09:37 +02:00
Alexander Neumann
0f72176ddd Refuse writing the config in append-only mode 2018-04-02 13:09:37 +02:00
Alexander Neumann
8dad5a5f41 Add test for append-only mode 2018-04-02 13:09:37 +02:00
Alexander Neumann
899ef655ef Merge pull request #62 from restic/add-changelog
Add changelog generated by calens
2018-04-02 12:45:17 +02:00
Alexander Neumann
7126dfec79 Merge pull request #63 from jcgruenhage/patch-1
remove sudo from makefile
2018-03-30 11:46:27 +02:00
Jan Christian Grünhage
9107b94367 remove sudo from makefile
the makefile should not depend on sudo
2018-03-29 11:27:03 +02:00
Alexander Neumann
9d6316bd6e Add pull request URL 2018-03-24 17:41:54 +01:00
Alexander Neumann
897d5a026c Add changelog generated by calens
Closes #44
2018-03-24 17:40:49 +01:00
Konrad Wojas
4d2493388a Require auth by default, add --no-auth flag
In order to prevent users from accidentally exposing rest-server without
authentication, rest-server now defaults to requiring a .htpasswd. If
you want to disable authentication, you need to explicitly pass the new
--no-auth flag.
2018-03-24 13:30:54 +01:00
Leo R. Lundgren
02196a18d8 Clarify that the server does NOT authenticate users without a .htpasswd file. 2018-03-21 23:34:41 +01:00
Leo R. Lundgren
cbafb98113 Add --version flag to print version and exit. 2018-03-21 22:50:14 +01:00
Alexander Neumann
a6961e877b Travis: Fix tests (again)
The problem is that in Go < 1.9 "..." also matches the vendor directory,
and we don't want to run those tests :)
2018-03-20 22:19:42 +01:00
Leo R. Lundgren
ec7289235c Rename --cpuprofile flag to --cpu-profile (#53) 2018-03-20 21:46:30 +01:00
Alexander Neumann
698b6331b9 Travis: Test all the versions that we support
At the moment, `build.go` is configured to check that Go >= 1.7 is used,
so let's test that on Travis.
2018-03-20 21:16:58 +01:00
Alexander Neumann
69ed06aa66 Move TestIsUserPath to correct package 2018-03-20 21:16:58 +01:00
Alexander Neumann
cda126a99c Fix tests on Travis 2018-03-20 21:16:58 +01:00
rawtaz
4019e3f45e doc: Add --private-repos flag to README.md (#48) 2018-03-20 20:35:18 +01:00
rawtaz
ef5733293f doc: Add --private-repos flag to README.md (#48) 2018-03-20 20:02:17 +01:00
rawtaz
6f6f570b31 doc: Minor grammar fixes. 2018-03-20 20:02:17 +01:00
Aaron Bieber
496f8ea1f6 remove ip from freebsd example 2018-03-01 08:08:33 +01:00
Aaron Bieber
dfe9755ed0 add example rc scripts for OpenBSD and FreeBSD 2018-03-01 08:08:33 +01:00
Zlatko Čalušić
2209f1437e Version 0.9.7 2018-02-18 15:52:29 +01:00
Zlatko Čalušić
c3b57a177c Fix error handling in build.go copyFile() 2018-02-18 15:47:47 +01:00
Zlatko Čalušić
668c992035 Update AUTHORS 2018-02-18 15:31:38 +01:00
Zlatko Čalušić
16a4d01ac1 Update .travis.yml 2018-02-18 15:30:21 +01:00
Zlatko Čalušić
36012fd7b3 Update dependencies 2018-02-18 15:29:43 +01:00
Zlatko Čalušić
facfd2437e Update README.md 2018-02-15 17:26:09 +01:00
Aaron Bieber
bdaa1ae345 Check for more bcrypt prefixes 2018-02-15 17:19:17 +01:00
Aaron Bieber
733c8da8fc Enable support for bcrypt'd password hashes in htpasswd 2018-02-15 17:19:17 +01:00
Alexander Neumann
03958beb57 Update build.go 2018-02-11 11:34:49 +01:00
Zlatko Čalušić
aee26a5045 Fix link in README.md 2018-01-28 19:32:39 +01:00
Zlatko Čalušić
f25bf989dc Move systemd service file under examples/systemd/ 2018-01-28 19:28:16 +01:00
Zlatko Čalušić
05c6a03ad7 Dockerfile: use latest alpine base image 2018-01-28 19:23:53 +01:00
Alexander Neumann
bf34e9d62d Implement amended API protocol v2
The version is now selected via the HTTP request header "Accept".
2018-01-23 23:34:32 +01:00
Zlatko Čalušić
cd4d054887 Introduce ListBlobsV2()
Returns not only blob names, but also their sizes.

References:
 https://github.com/restic/restic/issues/1567
 https://github.com/restic/restic/pull/1571
2018-01-23 23:34:32 +01:00
Mebus
55134ae37a removed some blank lines 2018-01-23 13:56:26 +01:00
Zlatko Čalušić
b91d38076a Optimize Config struct (maligned) 2018-01-23 13:56:26 +01:00
Mebus
97835b4cfd added test for private repos 2018-01-23 13:56:26 +01:00
Mebus
0f85243f5a implemented wojas proposal in handlers.go 2018-01-23 13:56:26 +01:00
Mebus
75578acd66 fixed the code style with goimports 2018-01-23 13:56:26 +01:00
Mebus
6c846f856c added a feature for private repositories 2018-01-23 13:56:26 +01:00
Zlatko Čalušić
f99197dcf9 Update Makefile
- use latest Golang alpine image for building
- tag and push specific version of container
2018-01-21 21:01:09 +01:00
Zlatko Čalušić
c48c660678 Version 0.9.6 2018-01-21 19:52:32 +01:00
Zlatko Čalušić
9ef84dcdea Update dependencies 2018-01-21 19:43:52 +01:00
Zlatko Čalušić
0a5606e954 Update examples/compose-with-grafana/README.md 2018-01-21 19:36:01 +01:00
Zlatko Čalušić
5a2c70c9e1 Update AUTHORS 2018-01-21 19:28:17 +01:00
Zlatko Čalušić
cbe5cf5c74 Optimize Config struct (maligned) 2018-01-21 19:25:12 +01:00
Alexander Neumann
cd62c2bceb Update build.go 2018-01-13 10:52:46 +01:00
Alexander Neumann
f02ee7386a Auto create data/ subdirs on demand
Closes #40
2018-01-05 18:04:26 +01:00
n0npax
b786c5d1cc add option to secify tls cert and tls key as params
fix  #35
2017-11-23 20:20:09 +01:00
Brice Waegeneire
1bc1698f4e fix prometheus config flag 2017-11-06 00:50:16 +01:00
Konrad Wojas
8d8ecd7b0e Grafana dashboard fix: select instance
Fix: the queries were not filtering on the selected instance.
2017-10-30 17:05:56 +01:00
Konrad Wojas
213ff91b05 Full stack Docker Compose demo with Grafana dashboard
Add a full stack demo using Docker Compose that runs Rest Server,
Prometheus and Grafana with a Rest Server dashboard.
2017-10-30 17:05:56 +01:00
Konrad Wojas
d1504d7d66 Prometheus: add user label and delete blob metrics 2017-10-30 17:05:56 +01:00
Zlatko Čalušić
14f8cd5bca Update README.md 2017-10-25 19:19:24 +02:00
Zlatko Čalušić
791bb572f8 Update LICENSE, add AUTHORS 2017-10-25 19:13:07 +02:00
Zlatko Čalušić
d056b85432 Check errors in many places
Admittedly, in some places just document the fact that we ignore error
return values, 'cause we don't know what to do with it.  At least, the
linter is happy.
2017-10-25 18:31:34 +02:00
Zlatko Čalušić
10951e4540 Unshadow err in one place 2017-10-25 18:19:14 +02:00
Zlatko Čalušić
0f9ea68a2e Optimize Config struct (maligned) 2017-10-25 18:16:14 +02:00
Zlatko Čalušić
9b89df0842 Comment global variables 2017-10-25 18:14:07 +02:00
Konrad Wojas
b213d2a274 Fix goimports 2017-10-25 17:53:37 +02:00
Konrad Wojas
ff6270ab62 dep ensure for Prometheus deps 2017-10-25 17:53:37 +02:00
Konrad Wojas
4cd82b6802 Help needed: vendor files for Prometheus support
These are the vendor files needed for Prometheus support.
I have not been able to figure out how to do this properly with gopkg.
2017-10-25 17:53:37 +02:00
Konrad Wojas
ca0e09261f Add Prometheus metrics
Exposes a few metrics for Prometheus under /metrics if started with --prometheus.

Example:

    # HELP rest_server_blob_read_bytes_total Total number of bytes read from blobs
    # TYPE rest_server_blob_read_bytes_total counter
    rest_server_blob_read_bytes_total{repo="test",type="data"} 2.13557024e+09
    rest_server_blob_read_bytes_total{repo="test",type="index"} 1.198653e+06
    rest_server_blob_read_bytes_total{repo="test",type="keys"} 5388
    rest_server_blob_read_bytes_total{repo="test",type="locks"} 1975
    rest_server_blob_read_bytes_total{repo="test",type="snapshots"} 10018
    # HELP rest_server_blob_read_total Total number of blobs read
    # TYPE rest_server_blob_read_total counter
    rest_server_blob_read_total{repo="test",type="data"} 3985
    rest_server_blob_read_total{repo="test",type="index"} 21
    rest_server_blob_read_total{repo="test",type="keys"} 12
    rest_server_blob_read_total{repo="test",type="locks"} 12
    rest_server_blob_read_total{repo="test",type="snapshots"} 32
    # HELP rest_server_blob_write_bytes_total Total number of bytes written to blobs
    # TYPE rest_server_blob_write_bytes_total counter
    rest_server_blob_write_bytes_total{repo="test",type="data"} 1.063726179e+09
    rest_server_blob_write_bytes_total{repo="test",type="index"} 395586
    rest_server_blob_write_bytes_total{repo="test",type="locks"} 1975
    rest_server_blob_write_bytes_total{repo="test",type="snapshots"} 1933
    # HELP rest_server_blob_write_total Total number of blobs written
    # TYPE rest_server_blob_write_total counter
    rest_server_blob_write_total{repo="test",type="data"} 226
    rest_server_blob_write_total{repo="test",type="index"} 6
    rest_server_blob_write_total{repo="test",type="locks"} 12
    rest_server_blob_write_total{repo="test",type="snapshots"} 6
2017-10-25 17:53:37 +02:00
Konrad Wojas
526a2b3837 Limit htpasswd checks to once per 30s 2017-10-24 13:04:23 +02:00
Konrad Wojas
67a0f63773 Run goimports on htpasswd.go 2017-10-24 13:04:23 +02:00
Konrad Wojas
3e4edd3dd8 Automatically reload htpasswd
If htpasswd was modified, it will be automatically reloaded. This check
happens at most once per second and only on incoming requests.

Note that this removes the public `NewHtpasswd()` function.
2017-10-24 13:04:23 +02:00
Zlatko Čalušić
50b52dfd74 Version 0.9.5 2017-10-19 11:04:22 +02:00
Zlatko Čalušić
a20136a8da Set docker image default port also to 8000
So that rest-server can be started as non-root user.  You can choose
which port to expose, anyway.
2017-10-19 00:16:54 +02:00
Zlatko Čalušić
132232db69 Update dependencies 2017-10-19 00:05:02 +02:00
Zlatko Čalušić
e4071748b9 Update README.md 2017-10-18 23:52:03 +02:00
Zlatko Čalušić
ca5664b8a7 Makefile: add rules for docker image build 2017-10-18 23:38:18 +02:00
Zlatko Čalušić
ebe3bc04b5 Docker: various updates
- use exec in entrypoint.sh, get rid of extra shell process
- use CMD instead of ENTRYPOINT, so image can be run with /bin/sh arg
2017-10-18 23:18:44 +02:00
Zlatko Čalušić
87356ac452 Docker: use golang:1.9.1-alpine to build rest-server binary 2017-10-18 23:01:53 +02:00
Zlatko Čalušić
a43bfa19e4 Update Makefile 2017-10-15 21:19:38 +02:00
Zlatko Čalušić
dc9b99777b Update .gitignore 2017-10-10 19:07:48 +02:00
Zlatko Čalušić
fff8ecd210 Update Makefile, README.md
We now have make install rule.
2017-10-05 09:53:45 +02:00
Zlatko Čalušić
8bd0ed3219 Update .travis.yml 2017-10-04 22:20:26 +02:00
Zlatko Čalušić
c091bdc8bb Introduce Makefile 2017-10-04 22:17:34 +02:00
Zlatko Čalušić
d5bbf6aac8 Fix case 2017-10-04 22:07:06 +02:00
Zlatko Čalušić
ac7c83fa47 Update README.md 2017-10-04 21:59:58 +02:00
Zlatko Čalušić
56954b3131 Update dependencies 2017-10-04 21:44:44 +02:00
cgonzalez
65a41ff4e3 Allow the $OPTIONS env var when using docker image
Example: docker run -e OPTIONS="--append-only" restic/rest-server
2017-09-20 18:48:22 +02:00
Kenny Keslar
67583ff459 Fix formatting. 2017-09-13 14:46:56 +02:00
Kenny Keslar
618b530b88 Implement an append only mode. 2017-09-13 14:46:56 +02:00
Alexander Neumann
cff373e8aa Merge pull request #29 from wscott/master
Add a hint of how to make restic use this server.
2017-09-13 14:33:10 +02:00
Wayne Scott
9d0ff790e6 Add a hint of how to make restic use this server. 2017-09-10 09:36:36 -04:00
Zlatko Čalušić
16022faa7a Update .travis.yml 2017-08-25 09:45:26 +02:00
Zlatko Čalušić
0a0ed9c4b5 Version 0.9.4 2017-07-31 11:40:22 +02:00
Zlatko Čalušić
57ca1d7d6e Slightly improve error handling
Pass errors from Cobra runRoot() to main().
2017-07-30 17:45:23 +02:00
Alexander Neumann
a628c4e01a Fix directory traversal
This commit introduces the strict checks from net/http.Dir, which fixes
a directory traversal issue.

Closes #22
2017-07-30 17:37:45 +02:00
Zlatko Čalušić
9a6bb5eebe Update README.md
Document restic v0.7.1 as the required version to run with Rest Server.

Closes #11
2017-07-22 12:32:05 +02:00
Zlatko Čalušić
cae51e1478 Update dependencies 2017-07-19 22:18:06 +02:00
Zlatko Čalušić
0e5f662fed Switch to dep dependency management tool 2017-07-19 22:12:34 +02:00
Alexander Neumann
96cdf7b3b4 Add port publish to docker instructions
Closes #19
2017-07-03 19:52:17 +02:00
Zlatko Čalušić
b2e4715d1b Version 0.9.3 2017-06-29 00:01:23 +02:00
Zlatko Čalušić
4873fd9ffe Update dependencies 2017-06-25 12:06:41 +02:00
Zlatko Čalušić
0c22253d41 Update README.md 2017-06-25 12:04:48 +02:00
Alexander Neumann
90b868dfbc build.go: Fix path for new cmd/ subdir 2017-06-25 11:48:02 +02:00
Matthew Holt
65152c7bf5 Move main function into separate package (closes #12) 2017-06-25 11:48:02 +02:00
Bruno Clermont
07b6d5facf add docker image 2017-06-22 21:17:17 +02:00
Zlatko Čalušić
6b821132ec Travis can't determine latest versions of Go 2017-06-09 00:10:45 +02:00
Zlatko Čalušić
99eb5a4682 Update .travis.yml 2017-06-05 20:57:17 +02:00
Zlatko Čalušić
d54a589176 README.md: document why and when to use rest-server 2017-06-04 12:10:11 +02:00
Zlatko Čalušić
907801c8b9 Update dependencies 2017-06-02 10:27:10 +02:00
755 changed files with 6389 additions and 160979 deletions

85
.github/ISSUE_TEMPLATE/BUG.md vendored Normal file
View File

@@ -0,0 +1,85 @@
---
name: Bug report
about: Report a problem with rest-server to help us resolve it and improve
---
<!--
Welcome! - We kindly ask that you:
1. Fill out the issue template below - not doing so needs a good reason.
2. Use the forum if you have a question rather than a bug or feature request.
The forum is at: https://forum.restic.net
NOTE: Not filling out the issue template needs a good reason, as otherwise it
may take a lot longer to find the problem, not to mention it can take up a lot
more time which can otherwise be spent on development. Please also take the
time to help us debug the issue by collecting relevant information, even if
it doesn't seem to be relevant to you. Thanks!
The forum is a better place for questions about rest-server or general suggestions
and topics, e.g. usage or documentation questions! This issue tracker is mainly
for tracking bugs and feature requests directly relating to the development of
the software itself, rather than the project.
Thanks for understanding, and for contributing to the project!
-->
Output of `rest-server --version` <!-- If using docker, output of `docker images restic/rest-server:latest -q` -->
---------------------------------
Problem description / Steps to reproduce
----------------------------------------
<!--
This section should include at least:
* A description of the problem you are having with rest-server.
* The complete command line and any environment variables you used to
configure rest-server. Make sure to replace sensitive values!
* The output of the commands, what rest-server prints gives may give us much
information to diagnose the problem!
* The more time you spend describing an easy way to reproduce the behavior (if
this is possible), the easier it is for the project developers to fix it!
-->
Expected behavior
-----------------
<!--
Describe what you'd like rest-server to do differently.
-->
Actual behavior
---------------
<!--
In this section, please try to concentrate on observations, so only describe
what you observed directly.
-->
Do you have any idea what may have caused this?
-----------------------------------------------
<!--
Did something noteworthy happen on your system, Internet connection, backend services, etc?
-->
Did rest-server help you today? Did it make you happy in any way?
-----------------------------------------------------------------
<!--
Answering this question is not required, but if you have anything positive to share, please do so here!
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
-->

57
.github/ISSUE_TEMPLATE/FEATURE.md vendored Normal file
View File

@@ -0,0 +1,57 @@
---
name: Feature request
about: Suggest a new feature or enhancement for rest-server
---
<!--
Welcome! - We kindly ask that you:
1. Fill out the issue template below - not doing so needs a good reason.
2. Use the forum if you have a question rather than a bug or feature request.
The forum is at: https://forum.restic.net
The forum is a better place for questions about rest-server or general suggestions
and topics, e.g. usage or documentation questions! This issue tracker is mainly
for tracking bugs and feature requests directly relating to the development of
the software itself, rather than the project.
Thanks for understanding, and for contributing to the project!
-->
Output of `rest-server --version` <!-- If using docker, output of `docker images restic/rest-server:latest -q` -->
---------------------------------
<!--
Please add the version of rest-server you're currently using here, this helps us
later to see what has changed in rest-server when we revisit this issue after some
time.
-->
What should rest-server do differently? Which functionality do you think we should add?
---------------------------------------------------------------------------------------
<!--
Please describe the feature you'd like us to add here.
-->
What are you trying to do? What problem would this solve?
---------------------------------------------------------
<!--
This section should contain a brief description what you're trying to do, which
would be possible after implementing the new feature.
-->
Did rest-server help you today? Did it make you happy in any way?
-----------------------------------------------------------------
<!--
Answering this question is not required, but if you have anything positive to share, please do so here!
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
-->

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
contact_links:
- name: restic forum
url: https://forum.restic.net
about: Please ask questions about using restic here, do not open an issue for questions.

41
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,41 @@
<!--
Thank you very much for contributing code or documentation to rest-server! Please
fill out the following questions to make it easier for us to review your
changes.
-->
What does this PR change? What problem does it solve?
-----------------------------------------------------
<!--
Describe the changes and their purpose here, as detailed as needed.
-->
Was the change previously discussed in an issue or on the forum?
----------------------------------------------------------------
<!--
Link issues and relevant forum posts here.
If this PR resolves an issue on GitHub, use "Closes #1234" so that the issue
is closed automatically when this PR is merged.
-->
Checklist
---------
<!--
You do not need to check all the boxes below all at once. Feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box. Enable a checkbox by replacing [ ] with [x].
Please always follow these steps:
- Enable [maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
- Run `gofmt` on the code in all commits.
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/rest-server/blob/master/CONTRIBUTING.md#git-commits).
-->
- [ ] I have added tests for all code changes.
- [ ] I have added documentation for relevant changes (in the manual).
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/rest-server/blob/master/changelog/TEMPLATE)).
- [ ] I'm done! This pull request is ready for review.

13
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
version: 2
updates:
# Dependencies listed in go.mod
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
# Dependencies listed in .github/workflows/*.yml
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

119
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,119 @@
name: test
on:
# run tests on push to master, but not when other branches are pushed to
push:
branches:
- master
# run tests for all pull requests
pull_request:
merge_group:
permissions:
contents: read
env:
latest_go: "1.24.x"
GO111MODULE: on
jobs:
test:
strategy:
matrix:
include:
- job_name: Linux
go: 1.24.x
os: ubuntu-latest
check_changelog: true
- job_name: Linux (race)
go: 1.24.x
os: ubuntu-latest
test_opts: "-race"
- job_name: Linux
go: 1.23.x
os: ubuntu-latest
name: ${{ matrix.job_name }} Go ${{ matrix.go }}
runs-on: ${{ matrix.os }}
env:
GOPROXY: https://proxy.golang.org
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Build with build.go
run: |
go run build.go --goos linux
go run build.go --goos windows
go run build.go --goos darwin
- name: Run local Tests
run: |
go test -cover ${{matrix.test_opts}} ./...
- name: Check changelog files with calens
run: |
echo "install calens"
go install github.com/restic/calens@latest
echo "check changelog files"
calens
if: matrix.check_changelog
lint:
name: lint
runs-on: ubuntu-latest
permissions:
contents: read
# allow annotating code in the PR
checks: write
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go ${{ env.latest_go }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.latest_go }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.64.8
args: --verbose --timeout 5m
# only run golangci-lint for pull requests, otherwise ALL hints get
# reported. We need to slowly address all issues until we can enable
# linting the master branch :)
if: github.event_name == 'pull_request'
- name: Check go.mod/go.sum
run: |
echo "check if go.mod and go.sum are up to date"
go mod tidy
git diff --exit-code go.mod go.sum
analyze:
name: Analyze results
needs: [test, lint]
if: always()
permissions: # no need to access code
contents: none
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe
with:
jobs: ${{ toJSON(needs) }}

4
.gitignore vendored
View File

@@ -1,5 +1 @@
*~
\#*\#
.\#*
/rest-server

56
.golangci.yml Normal file
View File

@@ -0,0 +1,56 @@
# This is the configuration for golangci-lint for the restic project.
#
# A sample config with all settings is here:
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
linters:
# only enable the linters listed below
disable-all: true
enable:
# make sure all errors returned by functions are handled
- errcheck
# show how code can be simplified
- gosimple
# make sure code is formatted
- gofmt
# examine code and report suspicious constructs, such as Printf calls whose
# arguments do not align with the format string
- govet
# make sure names and comments are used according to the conventions
- revive
# detect when assignments to existing variables are not used
- ineffassign
# run static analysis and find errors
- staticcheck
# find unused variables, functions, structs, types, etc.
- unused
# parse and typecheck code
- typecheck
# ensure that http response bodies are closed
- bodyclose
- importas
issues:
# don't use the default exclude rules, this hides (among others) ignored
# errors from Close() calls
exclude-use-default: false
# list of things to not warn about
exclude:
# revive: do not warn about missing comments for exported stuff
- exported (function|method|var|type|const) .* should have comment or be unexported
# revive: ignore constants in all caps
- don't use ALL_CAPS in Go names; use CamelCase
# revive: lots of packages don't have such a comment
- "package-comments: should have a package comment"
- "redefines-builtin-id:"

244
.goreleaser.yml Normal file
View File

@@ -0,0 +1,244 @@
---
version: 2
before:
# Run a few commands to check the state of things. When anything is changed
# in files commited to the repo, goreleaser will abort before building
# anything because the git checkout is dirty.
hooks:
# make sure all modules are available
- go mod download
# make sure all generated code is up to date
- go generate ./...
# check that $VERSION is set
- test -n "{{ .Env.VERSION }}"
# make sure the file VERSION contains the latest version (used for build.go)
- bash -c 'echo "{{ .Env.VERSION }}" > VERSION'
# make sure that main.go contains the latest version
- echo sed -i 's/var version = "[^"]*"/var version = "{{ .Env.VERSION }}"/' cmd/rest-server/main.go
# make sure the file CHANGELOG.md is up to date
- calens --output CHANGELOG.md
# build a single binary
builds:
- id: default
# make sure everything is statically linked by disabling cgo altogether
env: &build_env
- CGO_ENABLED=0
# set the package for the main binary
main: ./cmd/rest-server
flags:
&build_flags # don't include any paths to source files in the resulting binary
- -trimpath
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags: &build_ldflags # set the version variable in the main package
- "-s -w -X main.version={{ .Version }}"
# list all operating systems and architectures we build binaries for
goos:
- linux
- darwin
- freebsd
- netbsd
- openbsd
- dragonfly
- solaris
goarch:
- amd64
- "386"
- arm
- arm64
- mips
- mips64
- mips64le
- ppc64
- ppc64le
goarm:
- "6"
- "7"
- id: windows-only
env: *build_env
main: ./cmd/rest-server
flags: *build_flags
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags: *build_ldflags
goos:
- windows
goarch:
- amd64
- "386"
- arm
- arm64
# configure the resulting archives to create
archives:
- id: default
builds: [default, windows-only]
format: tar.gz
# package a directory which contains the source file
wrap_in_directory: true
builds_info: &archive_file_info
owner: root
group: root
mtime: "{{ .CommitDate }}"
mode: 0644
# add these files to all archives
files: &archive_files
- src: LICENSE
dst: LICENSE
info: *archive_file_info
- src: README.md
dst: README.md
info: *archive_file_info
- src: CHANGELOG.md
dst: CHANGELOG.md
info: *archive_file_info
- id: windows-only
builds: [windows-only]
formats: [zip]
wrap_in_directory: true
builds_info: *archive_file_info
files: *archive_files
# also build an archive of the source code
source:
enabled: true
# build a file containing the SHA256 hashes
checksum:
name_template: "SHA256SUMS"
# sign the checksum file
signs:
- artifacts: checksum
signature: "${artifact}.asc"
args:
- "--armor"
- "--output"
- "${signature}"
- "--detach-sign"
- "${artifact}"
# configure building the rest-server docker image
dockers:
- image_templates:
- restic/rest-server:{{ .Version }}-amd64
build_flag_templates:
- "--platform=linux/amd64"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.licenses=BSD-2-Clause"
use: buildx
dockerfile: "Dockerfile.goreleaser"
extra_files: &extra_files
- docker/create_user
- docker/delete_user
- docker/entrypoint.sh
- image_templates:
- restic/rest-server:{{ .Version }}-i386
goarch: "386"
build_flag_templates:
- "--platform=linux/386"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.licenses=BSD-2-Clause"
use: buildx
dockerfile: "Dockerfile.goreleaser"
extra_files: *extra_files
- image_templates:
- restic/rest-server:{{ .Version }}-arm32v6
goarch: arm
goarm: 6
build_flag_templates:
- "--platform=linux/arm/v6"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.licenses=BSD-2-Clause"
use: buildx
dockerfile: "Dockerfile.goreleaser"
extra_files: *extra_files
- image_templates:
- restic/rest-server:{{ .Version }}-arm32v7
goarch: arm
goarm: 7
build_flag_templates:
- "--platform=linux/arm/v7"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.licenses=BSD-2-Clause"
use: buildx
dockerfile: "Dockerfile.goreleaser"
extra_files: *extra_files
- image_templates:
- restic/rest-server:{{ .Version }}-arm64v8
goarch: arm64
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.licenses=BSD-2-Clause"
use: buildx
dockerfile: "Dockerfile.goreleaser"
extra_files: *extra_files
- image_templates:
- restic/rest-server:{{ .Version }}-ppc64le
goarch: ppc64le
build_flag_templates:
- "--platform=linux/ppc64le"
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.licenses=BSD-2-Clause"
use: buildx
dockerfile: "Dockerfile.goreleaser"
extra_files: *extra_files
docker_manifests:
- name_template: "restic/rest-server:{{ .Version }}"
image_templates:
- "restic/rest-server:{{ .Version }}-amd64"
- "restic/rest-server:{{ .Version }}-i386"
- "restic/rest-server:{{ .Version }}-arm32v6"
- "restic/rest-server:{{ .Version }}-arm32v7"
- "restic/rest-server:{{ .Version }}-arm64v8"
- "restic/rest-server:{{ .Version }}-ppc64le"
- name_template: "restic/rest-server:latest"
image_templates:
- "restic/rest-server:{{ .Version }}-amd64"
- "restic/rest-server:{{ .Version }}-i386"
- "restic/rest-server:{{ .Version }}-arm32v6"
- "restic/rest-server:{{ .Version }}-arm32v7"
- "restic/rest-server:{{ .Version }}-arm64v8"
- "restic/rest-server:{{ .Version }}-ppc64le"

View File

@@ -1,35 +0,0 @@
os: linux
sudo: false
language: go
go:
- 1.7.6
- 1.8.3
- tip
matrix:
allow_failures:
- go: tip
branches:
only:
- master
notifications:
email:
on_success: always
install:
- export GOBIN="$GOPATH/bin"
- export PATH="$PATH:$GOBIN"
- go get -u github.com/golang/lint/golint
- go get golang.org/x/tools/cmd/goimports
script:
- go install
- go test -v .
- go run build.go -v -T
- diff <(goimports -d *.go) <(printf "")
after_success:
- diff <(golint *.go) <(printf "")

16
AUTHORS Normal file
View File

@@ -0,0 +1,16 @@
# This is the official list of Rest Server authors for copyright purposes.
Aaron Bieber <aaron@bolddaemon.com>
Alexander Neumann <alexander@bumpern.de>
Bertil Chapuis <bchapuis@agimem.com>
Brice Waegeneire <brice.wge@gmail.com>
Bruno Clermont <bruno@robotinfra.com>
Chapuis Bertil <bchapuis@agimem.com>
Kenny Keslar <r3dey3@r3dey3.com>
Konrad Wojas <github@m.wojas.nl>
Matthew Holt <mholt@users.noreply.github.com>
Mebus <mebus.inbox@googlemail.com>
Wayne Scott <wsc9tt@gmail.com>
Zlatko Čalušić <zcalusic@bitsync.net>
cgonzalez <chgonzalezg@gmail.com>
n0npax <marcin@niemira.net>

525
CHANGELOG.md Normal file
View File

@@ -0,0 +1,525 @@
Changelog for rest-server 0.14.0 (2025-05-31)
============================================
The following sections list the changes in rest-server 0.14.0 relevant
to users. The changes are ordered by importance.
Summary
-------
* Sec #318: Fix world-readable permissions on new `.htpasswd` files
* Chg #322: Update dependencies and require Go 1.23 or newer
* Enh #174: Support proxy-based authentication
* Enh #189: Support group accessible repositories
* Enh #295: Output status of append-only mode on startup
* Enh #315: Hardened tls settings
* Enh #321: Add zip archive format for Windows releases
Details
-------
* Security #318: Fix world-readable permissions on new `.htpasswd` files
On startup the rest-server Docker container creates an empty `.htpasswd` file if
none exists yet. This file was world-readable by default, which can be a
security risk, even though the file only contains hashed passwords.
This has been fixed such that new `.htpasswd` files are no longer
world-readabble.
The permissions of existing `.htpasswd` files must be manually changed if
relevant in your setup.
https://github.com/restic/rest-server/issues/318
https://github.com/restic/rest-server/pull/340
* Change #322: Update dependencies and require Go 1.23 or newer
All dependencies have been updated. Rest-server now requires Go 1.23 or newer to
build.
This also disables support for TLS versions older than TLS 1.2. On Windows,
rest-server now requires at least Windows 10 or Windows Server 2016. On macOS,
rest-server now requires at least macOS 11 Big Sur.
https://github.com/restic/rest-server/pull/322
https://github.com/restic/rest-server/pull/338
* Enhancement #174: Support proxy-based authentication
Rest-server now supports authentication via HTTP proxy headers. This feature can
be enabled by specifying the username header using the `--proxy-auth-username`
option (e.g., `--proxy-auth-username=X-Forwarded-User`).
When enabled, the server authenticates users based on the specified header and
disables Basic Auth. Note that proxy authentication is disabled when `--no-auth`
is set.
https://github.com/restic/rest-server/issues/174
https://github.com/restic/rest-server/pull/307
* Enhancement #189: Support group accessible repositories
Rest-server now supports making repositories accessible to the filesystem group
by setting the `--group-accessible-repos` option. Note that permissions of
existing files are not modified. To allow the group to read and write file, use
a umask of `007`. To only grant read access use `027`. To make an existing
repository group-accessible, use `chmod -R g+rwX /path/to/repo`.
https://github.com/restic/rest-server/issues/189
https://github.com/restic/rest-server/pull/308
* Enhancement #295: Output status of append-only mode on startup
Rest-server now displays the status of append-only mode during startup.
https://github.com/restic/rest-server/pull/295
* Enhancement #315: Hardened tls settings
Rest-server now uses a secure TLS cipher suite set by default. The minimum TLS
version is now TLS 1.2 and can be further increased using the new
`--tls-min-ver` option, allowing users to enforce stricter security
requirements.
https://github.com/restic/rest-server/pull/315
* Enhancement #321: Add zip archive format for Windows releases
Windows users can now download rest-server binaries in zip archive format (.zip)
in addition to the existing tar.gz archives.
https://github.com/restic/rest-server/issues/321
https://github.com/restic/rest-server/pull/346
Changelog for rest-server 0.13.0 (2024-07-26)
============================================
The following sections list the changes in rest-server 0.13.0 relevant
to users. The changes are ordered by importance.
Summary
-------
* Chg #267: Update dependencies and require Go 1.18 or newer
* Chg #273: Shut down cleanly on TERM and INT signals
* Enh #271: Print listening address after start-up
* Enh #272: Support listening on a unix socket
Details
-------
* Change #267: Update dependencies and require Go 1.18 or newer
Most dependencies have been updated. Since some libraries require newer language
features, support for Go 1.17 has been dropped, which means that rest-server now
requires at least Go 1.18 to build.
https://github.com/restic/rest-server/pull/267
* Change #273: Shut down cleanly on TERM and INT signals
Rest-server now listens for TERM and INT signals and cleanly closes down the
http.Server and listener when receiving either of them.
This is particularly useful when listening on a unix socket, as the server will
now remove the socket file when it shuts down.
https://github.com/restic/rest-server/pull/273
* Enhancement #271: Print listening address after start-up
When started with `--listen :0`, rest-server would print `start server on :0`
The message now also includes the actual address listened on, for example `start
server on 0.0.0.0:37333`. This is useful when starting a server with an
auto-allocated free port number (port 0).
https://github.com/restic/rest-server/pull/271
* Enhancement #272: Support listening on a unix socket
It is now possible to make rest-server listen on a unix socket by prefixing the
socket filename with `unix:` and passing it to the `--listen` option, for
example `--listen unix:/tmp/foo`.
This is useful in combination with remote port forwarding to enable a remote
server to backup locally, e.g.:
```
rest-server --listen unix:/tmp/foo &
ssh -R /tmp/foo:/tmp/foo user@host restic -r rest:http+unix:///tmp/foo:/repo backup
```
https://github.com/restic/rest-server/pull/272
Changelog for rest-server 0.12.1 (2023-07-09)
============================================
The following sections list the changes in rest-server 0.12.1 relevant
to users. The changes are ordered by importance.
Summary
-------
* Fix #230: Fix erroneous warnings about unsupported fsync
* Fix #238: API: Return empty array when listing empty folders
* Enh #217: Log to stdout using the `--log -` option
Details
-------
* Bugfix #230: Fix erroneous warnings about unsupported fsync
Due to a regression in rest-server 0.12.0, it continuously printed `WARNING:
fsync is not supported by the data storage. This can lead to data loss, if the
system crashes or the storage is unexpectedly disconnected.` for systems that
support fsync. We have fixed the warning.
https://github.com/restic/rest-server/issues/230
https://github.com/restic/rest-server/pull/231
* Bugfix #238: API: Return empty array when listing empty folders
Rest-server returned `null` when listing an empty folder. This has been changed
to returning an empty array in accordance with the REST protocol specification.
This change has no impact on restic users.
https://github.com/restic/rest-server/issues/238
https://github.com/restic/rest-server/pull/239
* Enhancement #217: Log to stdout using the `--log -` option
Logging to stdout was possible using `--log /dev/stdout`. However, when the rest
server is run as a different user, for example, using
`sudo -u restic rest-server [...] --log /dev/stdout`
This did not work due to permission issues.
For logging to stdout, the `--log` option now supports the special filename `-`
which also works in these cases.
https://github.com/restic/rest-server/pull/217
Changelog for rest-server 0.12.0 (2023-04-24)
============================================
The following sections list the changes in rest-server 0.12.0 relevant
to users. The changes are ordered by importance.
Summary
-------
* Fix #183: Allow usernames containing underscore and more
* Fix #219: Ignore unexpected files in the data/ folder
* Fix #1871: Return 500 "Internal server error" if files cannot be read
* Chg #207: Return error if command-line arguments are specified
* Chg #208: Update dependencies and require Go 1.17 or newer
* Enh #133: Cache basic authentication credentials
* Enh #187: Allow configurable location for `.htpasswd` file
Details
-------
* Bugfix #183: Allow usernames containing underscore and more
The security fix in rest-server 0.11.0 (#131) disallowed usernames containing
and underscore "_". The list of allowed characters has now been changed to
include Unicode characters, numbers, "_", "-", "." and "@".
https://github.com/restic/rest-server/issues/183
https://github.com/restic/rest-server/pull/184
* Bugfix #219: Ignore unexpected files in the data/ folder
If the data folder of a repository contained files, this would prevent restic
from retrieving a list of file data files. This has been fixed. As a workaround
remove the files that are directly contained in the data folder (e.g.,
`.DS_Store` files).
https://github.com/restic/rest-server/issues/219
https://github.com/restic/rest-server/pull/221
* Bugfix #1871: Return 500 "Internal server error" if files cannot be read
When files in a repository cannot be read by rest-server, for example after
running `restic prune` directly on the server hosting the repositories in a way
that causes filesystem permissions to be wrong, rest-server previously returned
404 "Not Found" as status code. This was causing confusing for users.
The error handling has now been fixed to only return 404 "Not Found" if the file
actually does not exist. Otherwise a 500 "Internal server error" is reported to
the client and the underlying error is logged at the server side.
https://github.com/restic/rest-server/issues/1871
https://github.com/restic/rest-server/pull/195
* Change #207: Return error if command-line arguments are specified
Command line arguments are ignored by rest-server, but there was previously no
indication of this when they were supplied anyway.
To prevent usage errors an error is now printed when command line arguments are
supplied, instead of them being silently ignored.
https://github.com/restic/rest-server/pull/207
* Change #208: Update dependencies and require Go 1.17 or newer
Most dependencies have been updated. Since some libraries require newer language
features, support for Go 1.15-1.16 has been dropped, which means that
rest-server now requires at least Go 1.17 to build.
https://github.com/restic/rest-server/pull/208
* Enhancement #133: Cache basic authentication credentials
To speed up the verification of basic auth credentials, rest-server now caches
passwords for a minute in memory. That way the expensive verification of basic
auth credentials can be skipped for most requests issued by a single restic run.
The password is kept in memory in a hashed form and not as plaintext.
https://github.com/restic/rest-server/issues/133
https://github.com/restic/rest-server/pull/138
* Enhancement #187: Allow configurable location for `.htpasswd` file
It is now possible to specify the location of the `.htpasswd` file using the
`--htpasswd-file` option.
https://github.com/restic/rest-server/issues/187
https://github.com/restic/rest-server/pull/188
Changelog for rest-server 0.11.0 (2022-02-10)
============================================
The following sections list the changes in rest-server 0.11.0 relevant
to users. The changes are ordered by importance.
Summary
-------
* Sec #131: Prevent loading of usernames containing a slash
* Fix #119: Fix Docker configuration for `DISABLE_AUTHENTICATION`
* Fix #142: Fix possible data loss due to interrupted network connections
* Fix #155: Reply "insufficient storage" on disk full or over-quota
* Fix #157: Use platform-specific temporary directory as default data directory
* Chg #112: Add subrepo support and refactor server code
* Chg #146: Build rest-server at docker container build time
* Enh #122: Verify uploaded files
* Enh #126: Allow running rest-server via systemd socket activation
* Enh #148: Expand use of security features in example systemd unit file
Details
-------
* Security #131: Prevent loading of usernames containing a slash
"/" is valid char in HTTP authorization headers, but is also used in rest-server
to map usernames to private repos.
This commit prevents loading maliciously composed usernames like "/foo/config"
by restricting the allowed characters to the unicode character class, numbers,
"-", "." and "@".
This prevents requests to other users files like:
Curl -v -X DELETE -u foo/config:attack http://localhost:8000/foo/config
https://github.com/restic/rest-server/issues/131
https://github.com/restic/rest-server/pull/132
https://github.com/restic/rest-server/pull/137
* Bugfix #119: Fix Docker configuration for `DISABLE_AUTHENTICATION`
Rest-server 0.10.0 introduced a regression which caused the
`DISABLE_AUTHENTICATION` environment variable to stop working for the Docker
container. This has been fixed by automatically setting the option `--no-auth`
to disable authentication.
https://github.com/restic/rest-server/issues/119
https://github.com/restic/rest-server/pull/124
* Bugfix #142: Fix possible data loss due to interrupted network connections
When rest-server was run without `--append-only` it was possible to lose
uploaded files in a specific scenario in which a network connection was
interrupted.
For the data loss to occur a file upload by restic would have to be interrupted
such that restic notices the interrupted network connection before the
rest-server. Then restic would have to retry the file upload and finish it
before the rest-server notices that the initial upload has failed. Then the
uploaded file would be accidentally removed by rest-server when trying to
cleanup the failed upload.
This has been fixed by always uploading to a temporary file first which is moved
in position only once it was uploaded completely.
https://github.com/restic/rest-server/pull/142
* Bugfix #155: Reply "insufficient storage" on disk full or over-quota
When there was no space left on disk, or any other write-related error occurred,
rest-server replied with HTTP status code 400 (Bad request). This is misleading
(restic client will dump the status code to the user).
Rest-server now replies with two different status codes in these situations: *
HTTP 507 "Insufficient storage" is the status on disk full or repository
over-quota * HTTP 500 "Internal server error" is used for other disk-related
errors
https://github.com/restic/rest-server/issues/155
https://github.com/restic/rest-server/pull/160
* Bugfix #157: Use platform-specific temporary directory as default data directory
If no data directory is specificed, then rest-server now uses the Go standard
library functions to retrieve the standard temporary directory path for the
current platform.
https://github.com/restic/rest-server/issues/157
https://github.com/restic/rest-server/pull/158
* Change #112: Add subrepo support and refactor server code
Support for multi-level repositories has been added, so now each user can have
its own subrepositories. This feature is always enabled.
Authentication for the Prometheus /metrics endpoint can now be disabled with the
new `--prometheus-no-auth` flag.
We have split out all HTTP handling to a separate `repo` subpackage to cleanly
separate the server code from the code that handles a single repository. The new
RepoHandler also makes it easier to reuse rest-server as a Go component in any
other HTTP server.
The refactoring makes the code significantly easier to follow and understand,
which in turn makes it easier to add new features, audit for security and debug
issues.
https://github.com/restic/rest-server/issues/109
https://github.com/restic/rest-server/issues/107
https://github.com/restic/rest-server/pull/112
* Change #146: Build rest-server at docker container build time
The Dockerfile now includes a build stage such that the latest rest-server is
always built and packaged. This is done in a standard golang container to ensure
a clean build environment and only the final binary is shipped rather than the
whole build environment.
https://github.com/restic/rest-server/issues/146
https://github.com/restic/rest-server/pull/145
* Enhancement #122: Verify uploaded files
The rest-server now by default verifies that the hash of content of uploaded
files matches their filename. This ensures that transmission errors are detected
and forces restic to retry the upload. On low-power devices it can make sense to
disable this check by passing the `--no-verify-upload` flag.
https://github.com/restic/rest-server/issues/122
https://github.com/restic/rest-server/pull/130
* Enhancement #126: Allow running rest-server via systemd socket activation
We've added the option to have systemd create the listening socket and start the
rest-server on demand.
https://github.com/restic/rest-server/issues/126
https://github.com/restic/rest-server/pull/151
https://github.com/restic/rest-server/pull/127
* Enhancement #148: Expand use of security features in example systemd unit file
The example systemd unit file now enables additional systemd features to
mitigate potential security vulnerabilities in rest-server and the various
packages and operating system components which it relies upon.
https://github.com/restic/rest-server/issues/148
https://github.com/restic/rest-server/pull/149
Changelog for rest-server 0.10.0 (2020-09-13)
============================================
The following sections list the changes in rest-server 0.10.0 relevant
to users. The changes are ordered by importance.
Summary
-------
* Sec #60: Require auth by default, add --no-auth flag
* Sec #64: Refuse overwriting config file in append-only mode
* Sec #117: Stricter path sanitization
* Chg #102: Remove vendored dependencies
* Enh #44: Add changelog file
Details
-------
* Security #60: Require auth by default, add --no-auth flag
In order to prevent users from accidentally exposing rest-server without
authentication, rest-server now defaults to requiring a .htpasswd. If you want
to disable authentication, you need to explicitly pass the new --no-auth flag.
https://github.com/restic/rest-server/issues/60
https://github.com/restic/rest-server/pull/61
* Security #64: Refuse overwriting config file in append-only mode
While working on the `rclone serve restic` command we noticed that is currently
possible to overwrite the config file in a repo even if `--append-only` is
specified. The first commit adds proper tests, and the second commit fixes the
issue.
https://github.com/restic/rest-server/pull/64
* Security #117: Stricter path sanitization
The framework we're using in rest-server to decode paths to repositories allowed
specifying URL-encoded characters in paths, including sensitive characters such
as `/` (encoded as `%2F`).
We've changed this unintended behavior, such that rest-server now rejects such
paths. In particular, it is no longer possible to specify sub-repositories for
users by encoding the path with `%2F`, such as
`http://localhost:8000/foo%2Fbar`, which means that this will unfortunately be a
breaking change in that case.
If using sub-repositories for users is important to you, please let us know in
the forum, so we can learn about your use case and implement this properly. As
it currently stands, the ability to use sub-repositories was an unintentional
feature made possible by the URL decoding framework used, and hence never meant
to be supported in the first place. If we wish to have this feature in
rest-server, we'd like to have it implemented properly and intentionally.
https://github.com/restic/rest-server/issues/117
* Change #102: Remove vendored dependencies
We've removed the vendored dependencies (in the subdir `vendor/`) similar to
what we did for `restic` itself. When building restic, the Go compiler
automatically fetches the dependencies. It will also cryptographically verify
that the correct code has been fetched by using the hashes in `go.sum` (see the
link to the documentation below).
Building the rest-server now requires Go 1.11 or newer, since we're using Go
Modules for dependency management. Older Go versions are not supported any more.
https://github.com/restic/rest-server/issues/102
https://golang.org/cmd/go/#hdr-Module_downloading_and_verification
* Enhancement #44: Add changelog file
https://github.com/restic/rest-server/issues/44
https://github.com/restic/rest-server/pull/62

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM golang:alpine AS builder
ENV CGO_ENABLED 0
COPY . /build
WORKDIR /build
RUN go build -o rest-server ./cmd/rest-server
FROM alpine
ENV DATA_DIRECTORY /data
ENV PASSWORD_FILE /data/.htpasswd
RUN apk add --no-cache --update apache2-utils
COPY docker/create_user /usr/bin/
COPY docker/delete_user /usr/bin/
COPY docker/entrypoint.sh /entrypoint.sh
COPY --from=builder /build/rest-server /usr/bin
VOLUME /data
EXPOSE 8000
CMD [ "/entrypoint.sh" ]

16
Dockerfile.goreleaser Normal file
View File

@@ -0,0 +1,16 @@
FROM alpine
ENV DATA_DIRECTORY /data
ENV PASSWORD_FILE /data/.htpasswd
RUN apk add --no-cache --update apache2-utils
COPY docker/create_user /usr/bin/
COPY docker/delete_user /usr/bin/
COPY docker/entrypoint.sh /entrypoint.sh
COPY rest-server /usr/bin
VOLUME /data
EXPOSE 8000
CMD [ "/entrypoint.sh" ]

View File

@@ -2,6 +2,7 @@ The BSD 2-Clause License
Copyright © 2015, Bertil Chapuis
Copyright © 2016, Zlatko Čalušić, Alexander Neumann
Copyright © 2017, The Rest Server Authors
All rights reserved.
Redistribution and use in source and binary forms, with or without

221
README.md
View File

@@ -1,118 +1,179 @@
# Rest Server
[![Build Status](https://travis-ci.org/restic/rest-server.svg?branch=master)](https://travis-ci.org/restic/rest-server)
[![Status badge for CI tests](https://github.com/restic/rest-server/workflows/test/badge.svg)](https://github.com/restic/rest-server/actions?query=workflow%3Atest)
[![Go Report Card](https://goreportcard.com/badge/github.com/restic/rest-server)](https://goreportcard.com/report/github.com/restic/rest-server)
[![GoDoc](https://godoc.org/github.com/restic/rest-server?status.svg)](https://godoc.org/github.com/restic/rest-server)
[![License](https://img.shields.io/badge/license-BSD%20%282--Clause%29-003262.svg?maxAge=2592000)](https://github.com/restic/rest-server/blob/master/LICENSE)
[![Powered by](https://img.shields.io/badge/powered_by-Go-5272b4.svg?maxAge=2592000)](https://golang.org/)
Rest Server is a high performance HTTP server that implements restic's [REST backend
API](https://github.com/restic/restic/blob/master/doc/rest_backend.rst). It provides secure and efficient way to backup
data remotely, using [restic](https://github.com/restic/restic) backup client.
Rest Server is a high performance HTTP server that implements restic's [REST backend API](https://restic.readthedocs.io/en/latest/100_references.html#rest-backend). It provides secure and efficient way to backup data remotely, using [restic](https://github.com/restic/restic) backup client via the [rest: URL](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server).
## Requirements
Rest Server requires Go 1.7 or higher to build. The only tested compiler is the official Go compiler. Building server
with gccgo may work, but is not supported.
Rest Server requires Go 1.23 or higher to build. The only tested compiler is the official Go compiler.
The required version of restic backup client to use with rest-server is
[v0.6.1](https://github.com/restic/restic/releases/tag/v0.6.1) or higher, due to some
[changes](https://github.com/restic/restic/commit/1a538509d0232f1a532266e07da509875fe9e0d6) in the REST backend API and
performance [improvements](https://github.com/restic/restic/commit/04b262d8f10ba9eacde041734c08f806c4685e7f).
The required version of restic backup client to use with `rest-server` is [v0.7.1](https://github.com/restic/restic/releases/tag/v0.7.1) or higher.
## Installation
## Build
Run ```go run build.go```, afterwards you'll find the binary in the current directory. You can move it anywhere you
want. There's also an [example systemd service
file](https://github.com/restic/rest-server/blob/master/etc/rest-server.service) included, so you can get it up &
running as a proper service in no time. Of course, you can also test it from the command line.
For building the `rest-server` binary run `CGO_ENABLED=0 go build -o rest-server ./cmd/rest-server`
```
% go run build.go
## Usage
To learn how to use restic backup client with REST backend, please consult [restic manual](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server).
```console
$ rest-server --help
% ./rest-server --help
Run a REST server for use with restic
Usage:
rest-server [flags]
Flags:
--cpuprofile string write CPU profile to file
--debug output debug messages
-h, --help help for rest-server
--listen string listen address (default ":8000")
--log string log HTTP requests in the combined log format
--path string data directory (default "/tmp/restic")
--tls turn on TLS support
--append-only enable append only mode
--cpu-profile string write CPU profile to file
--debug output debug messages
--group-accessible-repos let filesystem group be able to access repo files
-h, --help help for rest-server
--htpasswd-file string location of .htpasswd file (default: "<data directory>/.htpasswd)"
--listen string listen address (default ":8000")
--log filename write HTTP requests in the combined log format to the specified filename (use "-" for logging to stdout)
--max-size int the maximum size of the repository in bytes
--no-auth disable .htpasswd authentication
--no-verify-upload do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device
--path string data directory (default "/tmp/restic")
--private-repos users can only access their private repo
--prometheus enable Prometheus metrics
--prometheus-no-auth disable auth for Prometheus /metrics endpoint
--proxy-auth-username string specifies the HTTP header containing the username for proxy-based authentication
--tls turn on TLS support
--tls-cert string TLS certificate path
--tls-key string TLS key path
--tls-min-ver string TLS min version, one of (1.2|1.3) (default "1.2")
-v, --version version for rest-server
```
Alternatively, you can compile and install it in your $GOBIN with a standard `go install`. But, beware, you won't have
version info built into binary, when compiled that way.
By default the server persists backup data in the OS temporary directory (`/tmp/restic` on Linux/BSD and others, in `%TEMP%\\restic` in Windows, etc). **If `rest-server` is launched using the default path, all backups will be lost**. To start the server with a custom persistence directory and with authentication disabled:
## Getting started
By default the server persists backup data in `/tmp/restic`. Start the server with a custom persistence directory:
```
% rest-server --path /user/home/backup
```sh
rest-server --path /user/home/backup --no-auth
```
The server uses an `.htpasswd` file to specify users. You can create such a file at the root of the persistence
directory by executing the following command. In order to append new user to the file, just omit the `-c` argument.
To authenticate users (for access to the rest-server), the server supports using a `.htpasswd` file to specify users. By default, the server looks for this file at the root of the persistence directory, but this can be changed using the `--htpasswd-file` option. You can create such a file by executing the following command (note that you need the `htpasswd` program from Apache's http-tools). In order to append new user to the file, just omit the `-c` argument. Only bcrypt and SHA encryption methods are supported, so use -B (very secure) or -s (insecure by today's standards) when adding/changing passwords.
```
% htpasswd -s -c .htpasswd username
```sh
htpasswd -B -c .htpasswd username
```
By default the server uses HTTP protocol. This is not very secure since with Basic Authentication, username and
passwords will travel in cleartext in every request. In order to enable TLS support just add the `-tls` argument and
add a private and public key at the root of your persistence directory.
If you want to disable authentication, you must add the `--no-auth` flag. If this flag is not specified and the `.htpasswd` cannot be opened, rest-server will refuse to start.
Signed certificate is required by the restic backend, but if you just want to test the feature you can generate unsigned
keys with the following commands:
NOTE: In older versions of rest-server (up to 0.9.7), this flag does not exist and the server disables authentication if `.htpasswd` is missing or cannot be opened.
```
% openssl genrsa -out private_key 2048
% openssl req -new -x509 -key private_key -out public_key -days 365
By default the server uses HTTP protocol. This is not very secure since with Basic Authentication, user name and passwords will be sent in clear text in every request. In order to enable TLS support just add the `--tls` argument and add a private and public key at the root of your persistence directory. You may also specify private and public keys by `--tls-cert` and `--tls-key` and set the minimum TLS version to 1.3 using `--tls-min-ver 1.3`.
Signed certificate is normally required by the restic backend, but if you just want to test the feature you can generate password-less unsigned keys with the following command:
```sh
openssl req -newkey rsa:2048 -nodes -x509 -keyout private_key -out public_key -days 365 -addext "subjectAltName = IP:127.0.0.1,DNS:yourdomain.com"
```
Rest Server uses exactly the same directory structure as local backend, so you should be able to access it both locally
and via HTTP, even simultaneously.
Omit the `IP:127.0.0.1` if you don't need your server be accessed via SSH Tunnels. No need to change default values in the openssl dialog, hitting enter every time is sufficient. To access this server via restic use `--cacert public_key`, meaning with a self-signed certificate you have to distribute your `public_key` file to every restic client.
To learn how to use restic backup client with REST backend, please consult [restic
manual](https://restic.readthedocs.io/en/latest/manual.html#rest-server).
The `--append-only` mode allows creation of new backups but prevents deletion and modification of existing backups. This can be useful when backing up systems that have a potential of being hacked.
To prevent your users from accessing each others' repositories, you may use the `--private-repos` flag which grants access only when a subdirectory with the same name as the user is specified in the repository URL. For example, user "foo" using the repository URLs `rest:https://foo:pass@host:8000/foo` or `rest:https://foo:pass@host:8000/foo/` would be granted access, but the same user using repository URLs `rest:https://foo:pass@host:8000/` or `rest:https://foo:pass@host:8000/foobar/` would be denied access. Users can also create their own subrepositories, like `/foo/bar/`.
Rest Server uses exactly the same directory structure as local backend, so you should be able to access it both locally and via HTTP, even simultaneously.
### Systemd
There's an example [systemd service file](https://github.com/restic/rest-server/blob/master/examples/systemd/rest-server.service) included with the source, so you can get Rest Server up & running as a proper Systemd service in no time. Before installing, adapt paths and options to your environment.
### Docker
Rest Server works well inside a container, images are [published to Docker Hub](https://hub.docker.com/r/restic/rest-server).
#### Start server
You can run the server with any container runtime, like Docker:
```sh
docker pull restic/rest-server:latest
docker run -p 8000:8000 -v /my/data:/data --name rest_server restic/rest-server
```
Note that:
- **contrary to the defaults** of `rest-server`, the persistent data volume is located to `/data`.
- By default, the image uses authentication. To turn it off, set environment variable `DISABLE_AUTHENTICATION` to any value.
- By default, the image loads the `.htpasswd` file from the persistent data volume (i.e. from `/data/.htpasswd`). To change the location of this file, set the environment variable `PASSWORD_FILE` to the path of the `.htpasswd` file. Please note that this path must be accessible from inside the container and should be persisted. This is normally done by bind-mounting a path into the container or with another docker volume.
- It's suggested to set a container name to more easily manage users (`--name` parameter to `docker run`).
- You can set environment variable `OPTIONS` to any extra flags you'd like to pass to rest-server.
#### Customize the image
The [published image](https://hub.docker.com/r/restic/rest-server) is built from the `Dockerfile` available on this repository, which you may use as a basis for building your own customized images.
```sh
git clone https://github.com/restic/rest-server.git
cd rest-server
docker build -t restic/rest-server:latest .
```
#### Manage users
##### Add user
```sh
docker exec -it rest_server create_user myuser
```
or
```sh
docker exec -it rest_server create_user myuser mypassword
```
##### Delete user
```sh
docker exec -it rest_server delete_user myuser
```
## Proxy Authentication
See above for no authentication (`--no-auth`) and basic authentication.
To delegate authentication to a proxy, use the `--proxy-auth-username` flag. The specified header name, for example `X-Forwarded-User`,
must be present in the request headers and specifies the username. Basic authentication is disabled when this flag is set.
Warning: rest-server trusts the username in the header. It is the responsibility of the proxy
to ensure that the username is correct and cannot be forged by an attacker.
## Prometheus support and Grafana dashboard
The server can be started with `--prometheus` to expose [Prometheus](https://prometheus.io/) metrics at `/metrics`. If authentication is enabled, this endpoint requires authentication for the 'metrics' user, but this can be overridden with the `--prometheus-no-auth` flag.
This repository contains an example full stack Docker Compose setup with a Grafana dashboard in [examples/compose-with-grafana/](examples/compose-with-grafana/).
## Group-accessible Repositories
Rest-server supports making repositories accessible to the filesystem group by setting the `--group-accessible-repos` option. Note that permissions of existing files are not modified. To allow the group to read and write file, use a umask of `007`. To only grant read access use `027`. To make an existing repository group-accessible, use `chmod -R g+rwX /path/to/repo`.
## Why use Rest Server?
Compared to the SFTP backend, the REST backend has better performance, especially so if you can skip additional crypto overhead by using plain HTTP transport (restic already properly encrypts all data it sends, so using HTTPS is mostly about authentication).
But, even if you use HTTPS transport, the REST protocol should be faster and more scalable, due to some inefficiencies of the SFTP protocol (everything needs to be transferred in chunks of 32 KiB at most, each packet needs to be acknowledged by the server).
One important safety feature that Rest Server adds is the optional ability to run in append-only mode. This prevents an attacker from wiping your server backups when access is gained to the server being backed up.
Finally, the Rest Server implementation is really simple and as such could be used on the low-end devices, no problem. Also, in some cases, for example behind corporate firewalls, HTTP/S might be the only protocol allowed. Here too REST backend might be the perfect option for your backup needs.
## Contributors
Contributors are welcome, just open a new issue / pull request.
## License
```
The BSD 2-Clause License
Copyright © 2015, Bertil Chapuis
Copyright © 2016, Zlatko Čalušić, Alexander Neumann
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

43
Release.md Normal file
View File

@@ -0,0 +1,43 @@
1. Export `$VERSION`:
export VERSION=0.10.0
2. Add new version to file `VERSION` and `main.go` and commit the result:
echo "${VERSION}" | tee VERSION
sed -i "s/var version = \"[^\"]*\"/var version = \"${VERSION}\"/" cmd/rest-server/main.go
git commit -m "Update VERSION files for ${VERSION}" VERSION cmd/rest-server/main.go
3. Move changelog files for `calens`:
mv changelog/unreleased "changelog/${VERSION}_$(date +%Y-%m-%d)"
rm -f "changelog/${VERSION}_$(date +%Y-%m-%d)/.gitkeep"
git add "changelog/${VERSION}"*
git rm -r changelog/unreleased
mkdir changelog/unreleased
touch changelog/unreleased/.gitkeep
git add changelog/unreleased/.gitkeep
git commit -m "Move changelog files for ${VERSION}" changelog/{unreleased,"${VERSION}"*}
4. Generate changelog:
calens > CHANGELOG.md
git add CHANGELOG.md
git commit -m "Generate CHANGELOG.md for ${VERSION}" CHANGELOG.md
5. Tag new version and push the tag:
git tag -a -s -m "v${VERSION}" "v${VERSION}"
git push --tags
6. Build the project (use `--snapshot` for testing, or pass `--config` to
use another config file):
goreleaser \
release --parallelism 4 \
--release-notes <(calens --template changelog/CHANGELOG-GitHub.tmpl --version "${VERSION}")
7. Set a new version in `main.go` and commit the result:
sed -i "s/var version = \"[^\"]*\"/var version = \"${VERSION}-dev\"/" cmd/rest-server/main.go
git commit -m "Update version for development" cmd/rest-server/main.go

View File

@@ -1 +1 @@
0.9.2
0.14.0

480
build.go
View File

@@ -1,4 +1,42 @@
// +build ignore
// Description
//
// This program aims to make building Go programs for end users easier by just
// calling it with `go run`, without having to setup a GOPATH.
//
// This program needs Go >= 1.12. It'll use Go modules for compilation. It
// builds the package configured as Main in the Config struct.
// BSD 2-Clause License
//
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
// All rights reserved.
//
// This file has been derived from the repository at:
// https://github.com/fd0/build-go
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//go:build ignore_build_go
// +build ignore_build_go
package main
@@ -6,147 +44,46 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
)
// config contains the configuration for the program to build.
var config = Config{
Name: "rest-server", // name of the program executable and directory
Namespace: "github.com/restic/rest-server", // subdir of GOPATH, e.g. "github.com/foo/bar"
Main: "github.com/restic/rest-server/cmd/rest-server", // package name for the main package
Tests: []string{"./..."}, // tests to run
MinVersion: GoVersion{Major: 1, Minor: 23, Patch: 0}, // minimum Go version supported
}
// Config configures the build.
type Config struct {
Name string
Namespace string
Main string
DefaultBuildTags []string
Tests []string
MinVersion GoVersion
}
var (
verbose bool
keepGopath bool
runTests bool
enableCGO bool
verbose bool
runTests bool
enableCGO bool
enablePIE bool
goVersion = ParseGoVersion(runtime.Version())
)
var config = struct {
Name string
Namespace string
Main string
Tests []string
}{
Name: "rest-server", // name of the program executable and directory
Namespace: "github.com/restic/rest-server", // subdir of GOPATH, e.g. "github.com/foo/bar"
Main: "github.com/restic/rest-server", // package name for the main package
Tests: []string{"github.com/restic/rest-server"}, // tests to run
}
// specialDir returns true if the file begins with a special character ('.' or '_').
func specialDir(name string) bool {
if name == "." {
return false
}
base := filepath.Base(name)
if base[0] == '_' || base[0] == '.' {
return true
}
return false
}
// excludePath returns true if the file should not be copied to the new GOPATH.
func excludePath(name string) bool {
ext := path.Ext(name)
if ext == ".go" || ext == ".s" || ext == ".h" {
return false
}
parentDir := filepath.Base(filepath.Dir(name))
if parentDir == "testdata" {
return false
}
return true
}
// updateGopath builds a valid GOPATH at dst, with all Go files in src/ copied to dst/prefix/, so calling
//
// updateGopath("/tmp/gopath", "/home/u/rest-server", "github.com/restic/rest-server")
//
// with "/home/u/restic" containing the file "foo.go" yields the following tree at "/tmp/gopath":
//
// /tmp/gopath
// └── src
// └── github.com
// └── restic
// └── rest-server
// └── foo.go
func updateGopath(dst, src, prefix string) error {
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
if specialDir(name) {
if fi.IsDir() {
return filepath.SkipDir
}
return nil
}
if fi.IsDir() {
return nil
}
if excludePath(name) {
return nil
}
intermediatePath, err := filepath.Rel(src, name)
if err != nil {
return err
}
fileSrc := filepath.Join(src, intermediatePath)
fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
return copyFile(fileDst, fileSrc)
})
}
func directoryExists(dirname string) bool {
stat, err := os.Stat(dirname)
if err != nil && os.IsNotExist(err) {
return false
}
return stat.IsDir()
}
// copyFile creates dst from src, preserving file attributes and timestamps.
func copyFile(dst, src string) error {
fi, err := os.Stat(src)
if err != nil {
return err
}
fsrc, err := os.Open(src)
if err != nil {
return err
}
defer fsrc.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst))
return err
}
fdst, err := os.Create(dst)
if err != nil {
return err
}
defer fdst.Close()
_, err = io.Copy(fdst, fsrc)
if err == nil {
err = os.Chmod(dst, fi.Mode())
}
if err == nil {
err = os.Chtimes(dst, fi.ModTime(), fi.ModTime())
}
return err
// die prints the message with fmt.Fprintf() to stderr and exits with an error
// code.
func die(message string, args ...interface{}) {
fmt.Fprintf(os.Stderr, message, args...)
os.Exit(1)
}
func showUsage(output io.Writer) {
@@ -155,12 +92,13 @@ func showUsage(output io.Writer) {
fmt.Fprintf(output, "OPTIONS:\n")
fmt.Fprintf(output, " -v --verbose output more messages\n")
fmt.Fprintf(output, " -t --tags specify additional build tags\n")
fmt.Fprintf(output, " -k --keep-gopath do not remove the GOPATH after build\n")
fmt.Fprintf(output, " -T --test run tests\n")
fmt.Fprintf(output, " -o --output set output file name\n")
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n")
fmt.Fprintf(output, " --enable-pie use PIE buildmode\n")
fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n")
fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n")
fmt.Fprintf(output, " --goarm value set GOARM for cross-compilation\n")
}
func verbosePrintf(message string, args ...interface{}) {
@@ -171,53 +109,74 @@ func verbosePrintf(message string, args ...interface{}) {
fmt.Printf("build: "+message, args...)
}
// cleanEnv returns a clean environment with GOPATH and GOBIN removed (if present).
func cleanEnv() (env []string) {
for _, v := range os.Environ() {
if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
// printEnv prints Go-relevant environment variables in a nice way using verbosePrintf.
func printEnv(env []string) {
verbosePrintf("environment (GO*):\n")
for _, v := range env {
// ignore environment variables which do not start with GO*.
if !strings.HasPrefix(v, "GO") {
continue
}
env = append(env, v)
verbosePrintf(" %s\n", v)
}
return env
}
// build runs "go build args..." with GOPATH set to gopath.
func build(cwd, goos, goarch, gopath string, args ...string) error {
a := []string{"build"}
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
func build(cwd string, env map[string]string, args ...string) error {
// -trimpath removes all absolute paths from the binary.
a := []string{"build", "-trimpath"}
if enablePIE {
a = append(a, "-buildmode=pie")
}
a = append(a, args...)
cmd := exec.Command("go", a...)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
cmd.Env = os.Environ()
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
printEnv(cmd.Env)
cmd.Dir = cwd
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
verbosePrintf("go %s\n", args)
verbosePrintf("chdir %q\n", cwd)
verbosePrintf("go %q\n", a)
return cmd.Run()
}
// test runs "go test args..." with GOPATH set to gopath.
func test(cwd, gopath string, args ...string) error {
args = append([]string{"test"}, args...)
func test(cwd string, env map[string]string, args ...string) error {
args = append([]string{"test", "-count", "1"}, args...)
cmd := exec.Command("go", args...)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath)
cmd.Env = os.Environ()
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
cmd.Dir = cwd
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
verbosePrintf("go %s\n", args)
printEnv(cmd.Env)
verbosePrintf("chdir %q\n", cwd)
verbosePrintf("go %q\n", args)
return cmd.Run()
}
// getVersion returns the version string from the file VERSION in the current directory.
// getVersion returns the version string from the file VERSION in the current
// directory.
func getVersionFromFile() string {
buf, err := ioutil.ReadFile("VERSION")
if err != nil {
@@ -228,8 +187,9 @@ func getVersionFromFile() string {
return strings.TrimSpace(string(buf))
}
// getVersion returns a version string which is a combination of the contents of the file VERSION in the current
// directory and the version from git (if available).
// getVersion returns a version string which is a combination of the contents
// of the file VERSION in the current directory and the version from git (if
// available).
func getVersion() string {
versionFile := getVersionFromFile()
versionGit := getVersionFromGit()
@@ -247,7 +207,8 @@ func getVersion() string {
return fmt.Sprintf("%s (%s)", versionFile, versionGit)
}
// getVersionFromGit returns a version string that identifies the currently checked out git commit.
// getVersionFromGit returns a version string that identifies the currently
// checked out git commit.
func getVersionFromGit() string {
cmd := exec.Command("git", "describe",
"--long", "--tags", "--dirty", "--always")
@@ -262,7 +223,8 @@ func getVersionFromGit() string {
return version
}
// Constants represents a set of constants that are set in the final binary to the given value via compiler flags.
// Constants represents a set of constants that are set in the final binary to
// the given value via compiler flags.
type Constants map[string]string
// LDFlags returns the string that can be passed to go build's `-ldflags`.
@@ -276,21 +238,106 @@ func (cs Constants) LDFlags() string {
return strings.Join(l, " ")
}
func main() {
log.SetFlags(0)
// GoVersion is the version of Go used to compile the project.
type GoVersion struct {
Major int
Minor int
Patch int
}
ver := runtime.Version()
if strings.HasPrefix(ver, "go1") && ver < "go1.7" {
log.Fatalf("Go version %s detected, rest-server requires at least Go 1.7\n", ver)
// ParseGoVersion parses the Go version s. If s cannot be parsed, the returned GoVersion is null.
func ParseGoVersion(s string) (v GoVersion) {
if !strings.HasPrefix(s, "go") {
return
}
buildTags := []string{}
s = s[2:]
data := strings.Split(s, ".")
if len(data) < 2 || len(data) > 3 {
// invalid version
return GoVersion{}
}
var err error
v.Major, err = strconv.Atoi(data[0])
if err != nil {
return GoVersion{}
}
// try to parse the minor version while removing an eventual suffix (like
// "rc2" or so)
for s := data[1]; s != ""; s = s[:len(s)-1] {
v.Minor, err = strconv.Atoi(s)
if err == nil {
break
}
}
if v.Minor == 0 {
// no minor version found
return GoVersion{}
}
if len(data) >= 3 {
v.Patch, err = strconv.Atoi(data[2])
if err != nil {
return GoVersion{}
}
}
return
}
// AtLeast returns true if v is at least as new as other. If v is empty, true is returned.
func (v GoVersion) AtLeast(other GoVersion) bool {
var empty GoVersion
// the empty version satisfies all versions
if v == empty {
return true
}
if v.Major < other.Major {
return false
}
if v.Minor < other.Minor {
return false
}
if v.Patch < other.Patch {
return false
}
return true
}
func (v GoVersion) String() string {
return fmt.Sprintf("Go %d.%d.%d", v.Major, v.Minor, v.Patch)
}
func main() {
if !goVersion.AtLeast(GoVersion{1, 12, 0}) {
die("Go version (%v) is too old, restic requires Go >= 1.12\n", goVersion)
}
if !goVersion.AtLeast(config.MinVersion) {
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
os.Exit(1)
}
buildTags := config.DefaultBuildTags
skipNext := false
params := os.Args[1:]
targetGOOS := runtime.GOOS
targetGOARCH := runtime.GOARCH
env := map[string]string{
"GO111MODULE": "on", // make sure we build in Module mode
"GOOS": runtime.GOOS,
"GOARCH": runtime.GOARCH,
"GOARM": "",
}
var outputFilename string
@@ -303,14 +350,12 @@ func main() {
switch arg {
case "-v", "--verbose":
verbose = true
case "-k", "--keep-gopath":
keepGopath = true
case "-t", "-tags", "--tags":
if i+1 >= len(params) {
log.Fatal("-t given but no tag specified")
die("-t given but no tag specified")
}
skipNext = true
buildTags = strings.Split(params[i+1], " ")
buildTags = append(buildTags, strings.Split(params[i+1], " ")...)
case "-o", "--output":
skipNext = true
outputFilename = params[i+1]
@@ -318,104 +363,103 @@ func main() {
runTests = true
case "--enable-cgo":
enableCGO = true
case "--enable-pie":
enablePIE = true
case "--goos":
skipNext = true
targetGOOS = params[i+1]
env["GOOS"] = params[i+1]
case "--goarch":
skipNext = true
targetGOARCH = params[i+1]
env["GOARCH"] = params[i+1]
case "--goarm":
skipNext = true
env["GOARM"] = params[i+1]
case "-h":
showUsage(os.Stdout)
return
default:
log.Printf("Error: unknown option %q\n\n", arg)
fmt.Fprintf(os.Stderr, "Error: unknown option %q\n\n", arg)
showUsage(os.Stderr)
os.Exit(1)
}
}
if len(buildTags) == 0 {
verbosePrintf("adding build-tag release\n")
buildTags = []string{"release"}
}
verbosePrintf("detected Go version %v\n", goVersion)
preserveSymbols := false
for i := range buildTags {
buildTags[i] = strings.TrimSpace(buildTags[i])
if buildTags[i] == "debug" || buildTags[i] == "profile" {
preserveSymbols = true
}
}
verbosePrintf("build tags: %s\n", buildTags)
root, err := os.Getwd()
if err != nil {
log.Fatalf("Getwd(): %v\n", err)
die("Getwd(): %v\n", err)
}
gopath, err := ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
if err != nil {
log.Fatalf("TempDir(): %v\n", err)
}
verbosePrintf("create GOPATH at %v\n", gopath)
if err = updateGopath(gopath, root, config.Namespace); err != nil {
log.Fatalf("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
}
vendor := filepath.Join(root, "vendor")
if directoryExists(vendor) {
if err = updateGopath(gopath, vendor, ""); err != nil {
log.Fatalf("copying files from %v to %v failed: %v\n", root, gopath, err)
}
}
defer func() {
if !keepGopath {
verbosePrintf("remove %v\n", gopath)
if err = os.RemoveAll(gopath); err != nil {
log.Fatalf("remove GOPATH at %s failed: %v\n", gopath, err)
}
} else {
verbosePrintf("leaving temporary GOPATH at %v\n", gopath)
}
}()
if outputFilename == "" {
outputFilename = config.Name
if targetGOOS == "windows" {
if env["GOOS"] == "windows" {
outputFilename += ".exe"
}
}
cwd, err := os.Getwd()
if err != nil {
log.Fatalf("Getwd() returned %v\n", err)
output := outputFilename
if !filepath.IsAbs(output) {
output = filepath.Join(root, output)
}
output := filepath.Join(cwd, outputFilename)
version := getVersion()
constants := Constants{}
if version != "" {
constants["main.version"] = version
}
ldflags := "-s -w " + constants.LDFlags()
ldflags := constants.LDFlags()
if !preserveSymbols {
// Strip debug symbols.
ldflags = "-s -w " + ldflags
}
verbosePrintf("ldflags: %s\n", ldflags)
args := []string{
"-tags", strings.Join(buildTags, " "),
"-ldflags", ldflags,
"-o", output, config.Main,
var (
buildArgs []string
testArgs []string
)
mainPackage := config.Main
if strings.HasPrefix(mainPackage, config.Namespace) {
mainPackage = strings.Replace(mainPackage, config.Namespace, "./", 1)
}
err = build(filepath.Join(gopath, "src"), targetGOOS, targetGOARCH, gopath, args...)
buildTarget := filepath.FromSlash(mainPackage)
buildCWD, err := os.Getwd()
if err != nil {
log.Fatalf("build failed: %v\n", err)
die("unable to determine current working directory: %v\n", err)
}
buildArgs = append(buildArgs,
"-tags", strings.Join(buildTags, " "),
"-ldflags", ldflags,
"-o", output, buildTarget,
)
err = build(buildCWD, env, buildArgs...)
if err != nil {
die("build failed: %v\n", err)
}
if runTests {
verbosePrintf("running tests\n")
err = test(cwd, gopath, config.Tests...)
testArgs = append(testArgs, config.Tests...)
err = test(buildCWD, env, testArgs...)
if err != nil {
log.Fatalf("running tests failed: %v\n", err)
die("running tests failed: %v\n", err)
}
}
}

View File

@@ -0,0 +1,14 @@
Change: Remove vendored dependencies
We've removed the vendored dependencies (in the subdir `vendor/`) similar to
what we did for `restic` itself. When building restic, the Go compiler
automatically fetches the dependencies. It will also cryptographically verify
that the correct code has been fetched by using the hashes in `go.sum` (see the
link to the documentation below).
Building the rest-server now requires Go 1.11 or newer, since we're using Go
Modules for dependency management. Older Go versions are not supported any more.
https://github.com/restic/rest-server/issues/102
https://golang.org/cmd/go/#hdr-Module_downloading_and_verification

View File

@@ -0,0 +1,19 @@
Security: Stricter path sanitization
The framework we're using in rest-server to decode paths to repositories
allowed specifying URL-encoded characters in paths, including sensitive
characters such as `/` (encoded as `%2F`).
We've changed this unintended behavior, such that rest-server now rejects
such paths. In particular, it is no longer possible to specify sub-repositories
for users by encoding the path with `%2F`, such as `http://localhost:8000/foo%2Fbar`,
which means that this will unfortunately be a breaking change in that case.
If using sub-repositories for users is important to you, please let us know in
the forum, so we can learn about your use case and implement this properly. As
it currently stands, the ability to use sub-repositories was an unintentional
feature made possible by the URL decoding framework used, and hence never meant
to be supported in the first place. If we wish to have this feature in
rest-server, we'd like to have it implemented properly and intentionally.
https://github.com/restic/rest-server/issues/117

View File

@@ -0,0 +1,4 @@
Enhancement: Add changelog file
https://github.com/restic/rest-server/issues/44
https://github.com/restic/rest-server/pull/62

View File

@@ -0,0 +1,8 @@
Security: Require auth by default, add --no-auth flag
In order to prevent users from accidentally exposing rest-server without
authentication, rest-server now defaults to requiring a .htpasswd. If you want
to disable authentication, you need to explicitly pass the new --no-auth flag.
https://github.com/restic/rest-server/issues/60
https://github.com/restic/rest-server/pull/61

View File

@@ -0,0 +1,8 @@
Security: Refuse overwriting config file in append-only mode
While working on the `rclone serve restic` command we noticed that is currently
possible to overwrite the config file in a repo even if `--append-only` is
specified. The first commit adds proper tests, and the second commit fixes the
issue.
https://github.com/restic/rest-server/pull/64

View File

@@ -0,0 +1,9 @@
Bugfix: Fix Docker configuration for `DISABLE_AUTHENTICATION`
rest-server 0.10.0 introduced a regression which caused the
`DISABLE_AUTHENTICATION` environment variable to stop working for the Docker
container. This has been fixed by automatically setting the option `--no-auth`
to disable authentication.
https://github.com/restic/rest-server/issues/119
https://github.com/restic/rest-server/pull/124

View File

@@ -0,0 +1,9 @@
Enhancement: Verify uploaded files
The rest-server now by default verifies that the hash of content of uploaded
files matches their filename. This ensures that transmission errors are
detected and forces restic to retry the upload. On low-power devices it can
make sense to disable this check by passing the `--no-verify-upload` flag.
https://github.com/restic/rest-server/issues/122
https://github.com/restic/rest-server/pull/130

View File

@@ -0,0 +1,7 @@
Enhancement: Allow running rest-server via systemd socket activation
We've added the option to have systemd create the listening socket and start the rest-server on demand.
https://github.com/restic/rest-server/issues/126
https://github.com/restic/rest-server/pull/151
https://github.com/restic/rest-server/pull/127

View File

@@ -0,0 +1,16 @@
Security: Prevent loading of usernames containing a slash
"/" is valid char in HTTP authorization headers, but is also used in
rest-server to map usernames to private repos.
This commit prevents loading maliciously composed usernames like
"/foo/config" by restricting the allowed characters to the unicode
character class, numbers, "-", "." and "@".
This prevents requests to other users files like:
curl -v -X DELETE -u foo/config:attack http://localhost:8000/foo/config
https://github.com/restic/rest-server/issues/131
https://github.com/restic/rest-server/pull/132
https://github.com/restic/rest-server/pull/137

View File

@@ -0,0 +1,9 @@
Change: Build rest-server at docker container build time
The Dockerfile now includes a build stage such that the latest rest-server is
always built and packaged. This is done in a standard golang container to
ensure a clean build environment and only the final binary is shipped rather
than the whole build environment.
https://github.com/restic/rest-server/issues/146
https://github.com/restic/rest-server/pull/145

View File

@@ -0,0 +1,8 @@
Enhancement: Expand use of security features in example systemd unit file
The example systemd unit file now enables additional systemd features to
mitigate potential security vulnerabilities in rest-server and the various
packages and operating system components which it relies upon.
https://github.com/restic/rest-server/issues/148
https://github.com/restic/rest-server/pull/149

View File

@@ -0,0 +1,20 @@
Change: Add subrepo support and refactor server code
Support for multi-level repositories has been added, so now each user can have
its own subrepositories. This feature is always enabled.
Authentication for the Prometheus /metrics endpoint can now be disabled with the
new `--prometheus-no-auth` flag.
We have split out all HTTP handling to a separate `repo` subpackage to cleanly
separate the server code from the code that handles a single repository. The new
RepoHandler also makes it easier to reuse rest-server as a Go component in
any other HTTP server.
The refactoring makes the code significantly easier to follow and understand,
which in turn makes it easier to add new features, audit for security and debug
issues.
https://github.com/restic/restic/pull/112
https://github.com/restic/restic/issues/109
https://github.com/restic/restic/issues/107

View File

@@ -0,0 +1,16 @@
Bugfix: Fix possible data loss due to interrupted network connections
When rest-server was run without `--append-only` it was possible to lose uploaded
files in a specific scenario in which a network connection was interrupted.
For the data loss to occur a file upload by restic would have to be interrupted
such that restic notices the interrupted network connection before the
rest-server. Then restic would have to retry the file upload and finish it
before the rest-server notices that the initial upload has failed. Then the
uploaded file would be accidentally removed by rest-server when trying to
cleanup the failed upload.
This has been fixed by always uploading to a temporary file first which is moved
in position only once it was uploaded completely.
https://github.com/restic/rest-server/pull/142

View File

@@ -0,0 +1,8 @@
Bugfix: Use platform-specific temporary directory as default data directory
If no data directory is specificed, then rest-server now uses the Go standard
library functions to retrieve the standard temporary directory path for the
current platform.
https://github.com/restic/rest-server/issues/157
https://github.com/restic/rest-server/pull/158

View File

@@ -0,0 +1,13 @@
Bugfix: Reply "insufficient storage" on disk full or over-quota
When there was no space left on disk, or any other write-related error
occurred, rest-server replied with HTTP status code 400 (Bad request).
This is misleading (restic client will dump the status code to the user).
rest-server now replies with two different status codes in these situations:
* HTTP 507 "Insufficient storage" is the status on disk full or repository
over-quota
* HTTP 500 "Internal server error" is used for other disk-related errors
https://github.com/restic/rest-server/issues/155
https://github.com/restic/rest-server/pull/160

View File

@@ -0,0 +1,9 @@
Enhancement: Cache basic authentication credentials
To speed up the verification of basic auth credentials, rest-server now caches
passwords for a minute in memory. That way the expensive verification of basic
auth credentials can be skipped for most requests issued by a single restic
run. The password is kept in memory in a hashed form and not as plaintext.
https://github.com/restic/rest-server/issues/133
https://github.com/restic/rest-server/pull/138

View File

@@ -0,0 +1,8 @@
Bugfix: Allow usernames containing underscore and more
The security fix in rest-server 0.11.0 (#131) disallowed usernames containing
and underscore "_". The list of allowed characters has now been changed to
include Unicode characters, numbers, "_", "-", "." and "@".
https://github.com/restic/restic/issues/183
https://github.com/restic/restic/pull/184

View File

@@ -0,0 +1,7 @@
Enhancement: Allow configurable location for `.htpasswd` file
It is now possible to specify the location of the `.htpasswd`
file using the `--htpasswd-file` option.
https://github.com/restic/restic/issues/187
https://github.com/restic/restic/pull/188

View File

@@ -0,0 +1,9 @@
Bugfix: Ignore unexpected files in the data/ folder
If the data folder of a repository contained files, this would prevent restic
from retrieving a list of file data files. This has been fixed. As a workaround
remove the files that are directly contained in the data folder (e.g.,
`.DS_Store` files).
https://github.com/restic/rest-server/issues/219
https://github.com/restic/rest-server/pull/221

View File

@@ -0,0 +1,13 @@
Bugfix: Return 500 "Internal server error" if files cannot be read
When files in a repository cannot be read by rest-server, for example after
running `restic prune` directly on the server hosting the repositories in a
way that causes filesystem permissions to be wrong, rest-server previously
returned 404 "Not Found" as status code. This was causing confusing for users.
The error handling has now been fixed to only return 404 "Not Found" if the
file actually does not exist. Otherwise a 500 "Internal server error" is
reported to the client and the underlying error is logged at the server side.
https://github.com/restic/restic/issues/1871
https://github.com/restic/rest-server/pull/195

View File

@@ -0,0 +1,9 @@
Change: Return error if command-line arguments are specified
Command line arguments are ignored by rest-server, but there was previously
no indication of this when they were supplied anyway.
To prevent usage errors an error is now printed when command line arguments
are supplied, instead of them being silently ignored.
https://github.com/restic/rest-server/pull/207

View File

@@ -0,0 +1,7 @@
Change: Update dependencies and require Go 1.17 or newer
Most dependencies have been updated. Since some libraries require newer language
features, support for Go 1.15-1.16 has been dropped, which means that rest-server
now requires at least Go 1.17 to build.
https://github.com/restic/rest-server/pull/208

View File

@@ -0,0 +1,9 @@
Bugfix: Fix erroneous warnings about unsupported fsync
Due to a regression in rest-server 0.12.0, it continuously printed
`WARNING: fsync is not supported by the data storage. This can lead to data loss,
if the system crashes or the storage is unexpectedly disconnected.` for systems
that support fsync. We have fixed the warning.
https://github.com/restic/rest-server/issues/230
https://github.com/restic/rest-server/pull/231

View File

@@ -0,0 +1,8 @@
Bugfix: API: Return empty array when listing empty folders
Rest-server returned `null` when listing an empty folder. This has been changed
to returning an empty array in accordance with the REST protocol specification.
This change has no impact on restic users.
https://github.com/restic/rest-server/issues/238
https://github.com/restic/rest-server/pull/239

View File

@@ -0,0 +1,13 @@
Enhancement: Log to stdout using the `--log -` option
Logging to stdout was possible using `--log /dev/stdout`. However,
when the rest server is run as a different user, for example, using
`sudo -u restic rest-server [...] --log /dev/stdout`
this did not work due to permission issues.
For logging to stdout, the `--log` option now supports the special
filename `-` which also works in these cases.
https://github.com/restic/rest-server/pull/217

View File

@@ -0,0 +1,7 @@
Change: Update dependencies and require Go 1.18 or newer
Most dependencies have been updated. Since some libraries require newer language
features, support for Go 1.17 has been dropped, which means that rest-server
now requires at least Go 1.18 to build.
https://github.com/restic/rest-server/pull/267

View File

@@ -0,0 +1,9 @@
Enhancement: Print listening address after start-up
When started with `--listen :0`, rest-server would print `start server on :0`
The message now also includes the actual address listened on, for example
`start server on 0.0.0.0:37333`. This is useful when starting a server with
an auto-allocated free port number (port 0).
https://github.com/restic/rest-server/pull/271

View File

@@ -0,0 +1,15 @@
Enhancement: Support listening on a unix socket
It is now possible to make rest-server listen on a unix socket by prefixing
the socket filename with `unix:` and passing it to the `--listen` option,
for example `--listen unix:/tmp/foo`.
This is useful in combination with remote port forwarding to enable a remote
server to backup locally, e.g.:
```
rest-server --listen unix:/tmp/foo &
ssh -R /tmp/foo:/tmp/foo user@host restic -r rest:http+unix:///tmp/foo:/repo backup
```
https://github.com/restic/rest-server/pull/272

View File

@@ -0,0 +1,9 @@
Change: Shut down cleanly on TERM and INT signals
Rest-server now listens for TERM and INT signals and cleanly closes down the
http.Server and listener when receiving either of them.
This is particularly useful when listening on a unix socket, as the server
will now remove the socket file when it shuts down.
https://github.com/restic/rest-server/pull/273

View File

@@ -0,0 +1,10 @@
Enhancement: Support group accessible repositories
Rest-server now supports making repositories accessible to the filesystem group
by setting the `--group-accessible-repos` option. Note that permissions of
existing files are not modified. To allow the group to read and write file,
use a umask of `007`. To only grant read access use `027`. To make an existing
repository group-accessible, use `chmod -R g+rwX /path/to/repo`.
https://github.com/restic/rest-server/issues/189
https://github.com/restic/rest-server/pull/308

View File

@@ -0,0 +1,13 @@
Security: Fix world-readable permissions on new `.htpasswd` files
On startup the rest-server Docker container creates an empty `.htpasswd` file
if none exists yet. This file was world-readable by default, which can be
a security risk, even though the file only contains hashed passwords.
This has been fixed such that new `.htpasswd` files are no longer world-readabble.
The permissions of existing `.htpasswd` files must be manually changed if
relevant in your setup.
https://github.com/restic/rest-server/issues/318
https://github.com/restic/rest-server/pull/340

View File

@@ -0,0 +1,7 @@
Enhancement: Add zip archive format for Windows releases
Windows users can now download rest-server binaries in zip archive format (.zip)
in addition to the existing tar.gz archives.
https://github.com/restic/rest-server/issues/321
https://github.com/restic/rest-server/pull/346

View File

@@ -0,0 +1,5 @@
Enhancement: Output status of append-only mode on startup
Rest-server now displays the status of append-only mode during startup.
https://github.com/restic/rest-server/pull/295

View File

@@ -0,0 +1,12 @@
Enhancement: Support proxy-based authentication
Rest-server now supports authentication via HTTP proxy headers. This feature can
be enabled by specifying the username header using the `--proxy-auth-username`
option (e.g., `--proxy-auth-username=X-Forwarded-User`).
When enabled, the server authenticates users based on the specified header and
disables Basic Auth. Note that proxy authentication is disabled when `--no-auth`
is set.
https://github.com/restic/rest-server/issues/174
https://github.com/restic/rest-server/pull/307

View File

@@ -0,0 +1,7 @@
Enhancement: Hardened tls settings
Rest-server now uses a secure TLS cipher suite set by default. The minimum TLS
version is now TLS 1.2 and can be further increased using the new `--tls-min-ver`
option, allowing users to enforce stricter security requirements.
https://github.com/restic/rest-server/pull/315

View File

@@ -0,0 +1,11 @@
Change: Update dependencies and require Go 1.23 or newer
All dependencies have been updated. Rest-server now requires Go 1.23 or newer
to build.
This also disables support for TLS versions older than TLS 1.2. On Windows,
rest-server now requires at least Windows 10 or Windows Server 2016. On macOS,
rest-server now requires at least macOS 11 Big Sur.
https://github.com/restic/rest-server/pull/322
https://github.com/restic/rest-server/pull/338

View File

@@ -0,0 +1,31 @@
{{- range $changes := . }}{{ with $changes -}}
Changelog for rest-server {{ .Version }} ({{ .Date }})
=========================================
The following sections list the changes in rest-server {{ .Version }} relevant to users. The changes are ordered by importance.
Summary
-------
{{ range $entry := .Entries }}{{ with $entry }}
* {{ .TypeShort }} [#{{ .PrimaryID }}]({{ .PrimaryURL }}): {{ .Title }}
{{- end }}{{ end }}
Details
-------
{{ range $entry := .Entries }}{{ with $entry }}
* {{ .Type }} #{{ .PrimaryID }}: {{ .Title }}
{{ range $par := .Paragraphs }}
{{ $par }}
{{ end }}
{{ range $id := .Issues -}}
{{ ` ` }}[#{{ $id }}](https://github.com/restic/rest-server/issues/{{ $id -}})
{{- end -}}
{{ range $id := .PRs -}}
{{ ` ` }}[#{{ $id }}](https://github.com/restic/rest-server/pull/{{ $id -}})
{{- end -}}
{{ ` ` }}{{ range $url := .OtherURLs -}}
{{ $url -}}
{{- end }}
{{ end }}{{ end }}
{{ end }}{{ end -}}

32
changelog/CHANGELOG.tmpl Normal file
View File

@@ -0,0 +1,32 @@
{{- range $changes := . }}{{ with $changes -}}
Changelog for rest-server {{ .Version }} ({{ .Date }})
============================================
The following sections list the changes in rest-server {{ .Version }} relevant
to users. The changes are ordered by importance.
Summary
-------
{{ range $entry := .Entries }}{{ with $entry }}
* {{ .TypeShort }} #{{ .PrimaryID }}: {{ .Title }}
{{- end }}{{ end }}
Details
-------
{{ range $entry := .Entries }}{{ with $entry }}
* {{ .Type }} #{{ .PrimaryID }}: {{ .Title }}
{{ range $par := .Paragraphs }}
{{ wrapIndent $par 80 3 }}
{{ end -}}
{{ range $id := .Issues }}
https://github.com/restic/rest-server/issues/{{ $id -}}
{{ end -}}
{{ range $id := .PRs }}
https://github.com/restic/rest-server/pull/{{ $id -}}
{{ end -}}
{{ range $url := .OtherURLs }}
{{ $url -}}
{{ end }}
{{ end }}{{ end }}
{{ end }}{{ end -}}

20
changelog/TEMPLATE Normal file
View File

@@ -0,0 +1,20 @@
# The first line must start with Bugfix:, Enhancement: or Change:,
# including the colon. Use present tense. Remove lines starting with '#'
# from this template.
Bugfix: Fix behavior for foobar (in present tense)
# Describe the problem in the past tense, the new behavior in the present
# tense. Mention the affected commands, backends, operating systems, etc.
# Focus on user-facing behavior, not the implementation.
We've fixed the behavior for foobar, a long-standing annoyance for rest-server
users.
# The last section is a list of issue, PR and forum URLs.
# The first issue ID determines the filename for the changelog entry:
# changelog/unreleased/issue-1234. If there are no relevant issue links,
# use the PR ID and call the file pull-55555.
https://github.com/restic/rest-server/issues/1234
https://github.com/restic/rest-server/pull/55555
https://forum.restic.net/foo/bar/baz

View File

View File

@@ -0,0 +1,58 @@
//go:build !windows
// +build !windows
package main
import (
"fmt"
"log"
"net"
"strings"
"github.com/coreos/go-systemd/v22/activation"
)
// findListener tries to find a listener via systemd socket activation. If that
// fails, it tries to create a listener on addr.
func findListener(addr string) (listener net.Listener, err error) {
// try systemd socket activation
listeners, err := activation.Listeners()
if err != nil {
panic(err)
}
switch len(listeners) {
case 0:
// no listeners found, listen manually
if strings.HasPrefix(addr, "unix:") { // if we want to listen on a unix socket
unixAddr, err := net.ResolveUnixAddr("unix", strings.TrimPrefix(addr, "unix:"))
if err != nil {
return nil, fmt.Errorf("unable to understand unix address %s: %w", addr, err)
}
listener, err = net.ListenUnix("unix", unixAddr)
if err != nil {
return nil, fmt.Errorf("listen on %v failed: %w", addr, err)
}
} else { // assume tcp
listener, err = net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("listen on %v failed: %w", addr, err)
}
}
log.Printf("start server on %v", listener.Addr())
return listener, nil
case 1:
// one listener supplied by systemd, use that one
//
// for testing, run rest-server with systemd-socket-activate as follows:
//
// systemd-socket-activate -l 8080 ./rest-server
log.Printf("systemd socket activation mode")
return listeners[0], nil
default:
return nil, fmt.Errorf("got %d listeners from systemd, expected one", len(listeners))
}
}

View File

@@ -0,0 +1,78 @@
//go:build !windows
// +build !windows
package main
import (
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"testing"
"time"
)
func TestUnixSocket(t *testing.T) {
td := t.TempDir()
// this is the socket we'll listen on and connect to
tempSocket := filepath.Join(td, "sock")
// create some content and parent dirs
if err := os.MkdirAll(filepath.Join(td, "data", "repo1"), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(td, "data", "repo1", "config"), []byte("foo"), 0700); err != nil {
t.Fatal(err)
}
// run the following twice, to test that the server will
// cleanup its socket file when quitting, which won't happen
// if it doesn't exit gracefully
for i := 0; i < 2; i++ {
err := testServerWithArgs([]string{
"--no-auth",
"--path", filepath.Join(td, "data"),
"--listen", fmt.Sprintf("unix:%s", tempSocket),
}, time.Second, func(ctx context.Context, _ *restServerApp) error {
// custom client that will talk HTTP to unix socket
client := http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", tempSocket)
},
},
}
for _, test := range []struct {
Path string
StatusCode int
}{
{"/repo1/", http.StatusMethodNotAllowed},
{"/repo1/config", http.StatusOK},
{"/repo2/config", http.StatusNotFound},
} {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://ignored"+test.Path, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
err = resp.Body.Close()
if err != nil {
return err
}
if resp.StatusCode != test.StatusCode {
return fmt.Errorf("expected %d from server, instead got %d (path %s)", test.StatusCode, resp.StatusCode, test.Path)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
}

View File

@@ -0,0 +1,19 @@
package main
import (
"fmt"
"log"
"net"
)
// findListener creates a listener.
func findListener(addr string) (listener net.Listener, err error) {
// listen manually
listener, err = net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("listen on %v failed: %w", addr, err)
}
log.Printf("start server on %v", listener.Addr())
return listener, nil
}

240
cmd/rest-server/main.go Normal file
View File

@@ -0,0 +1,240 @@
package main
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/pprof"
"sync"
"syscall"
restserver "github.com/restic/rest-server"
"github.com/spf13/cobra"
)
type restServerApp struct {
CmdRoot *cobra.Command
Server restserver.Server
CPUProfile string
listenerAddressMu sync.Mutex
listenerAddress net.Addr // set after startup
}
// cmdRoot is the base command when no other command has been specified.
func newRestServerApp() *restServerApp {
rv := &restServerApp{
CmdRoot: &cobra.Command{
Use: "rest-server",
Short: "Run a REST server for use with restic",
SilenceErrors: true,
SilenceUsage: true,
Args: func(_ *cobra.Command, args []string) error {
if len(args) != 0 {
return fmt.Errorf("rest-server expects no arguments - unknown argument: %s", args[0])
}
return nil
},
Version: fmt.Sprintf("rest-server %s compiled with %v on %v/%v\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH),
},
Server: restserver.Server{
Path: filepath.Join(os.TempDir(), "restic"),
Listen: ":8000",
TLSMinVer: "1.2",
},
}
rv.CmdRoot.RunE = rv.runRoot
flags := rv.CmdRoot.Flags()
flags.StringVar(&rv.CPUProfile, "cpu-profile", rv.CPUProfile, "write CPU profile to file")
flags.BoolVar(&rv.Server.Debug, "debug", rv.Server.Debug, "output debug messages")
flags.StringVar(&rv.Server.Listen, "listen", rv.Server.Listen, "listen address")
flags.StringVar(&rv.Server.Log, "log", rv.Server.Log, "write HTTP requests in the combined log format to the specified `filename` (use \"-\" for logging to stdout)")
flags.Int64Var(&rv.Server.MaxRepoSize, "max-size", rv.Server.MaxRepoSize, "the maximum size of the repository in bytes")
flags.StringVar(&rv.Server.Path, "path", rv.Server.Path, "data directory")
flags.BoolVar(&rv.Server.TLS, "tls", rv.Server.TLS, "turn on TLS support")
flags.StringVar(&rv.Server.TLSCert, "tls-cert", rv.Server.TLSCert, "TLS certificate path")
flags.StringVar(&rv.Server.TLSKey, "tls-key", rv.Server.TLSKey, "TLS key path")
flags.StringVar(&rv.Server.TLSMinVer, "tls-min-ver", rv.Server.TLSMinVer, "TLS min version, one of (1.2|1.3)")
flags.BoolVar(&rv.Server.NoAuth, "no-auth", rv.Server.NoAuth, "disable authentication")
flags.StringVar(&rv.Server.HtpasswdPath, "htpasswd-file", rv.Server.HtpasswdPath, "location of .htpasswd file (default: \"<data directory>/.htpasswd)\"")
flags.StringVar(&rv.Server.ProxyAuthUsername, "proxy-auth-username", rv.Server.ProxyAuthUsername, "specifies the HTTP header containing the username for proxy-based authentication")
flags.BoolVar(&rv.Server.NoVerifyUpload, "no-verify-upload", rv.Server.NoVerifyUpload,
"do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device")
flags.BoolVar(&rv.Server.AppendOnly, "append-only", rv.Server.AppendOnly, "enable append only mode")
flags.BoolVar(&rv.Server.PrivateRepos, "private-repos", rv.Server.PrivateRepos, "users can only access their private repo")
flags.BoolVar(&rv.Server.Prometheus, "prometheus", rv.Server.Prometheus, "enable Prometheus metrics")
flags.BoolVar(&rv.Server.PrometheusNoAuth, "prometheus-no-auth", rv.Server.PrometheusNoAuth, "disable auth for Prometheus /metrics endpoint")
flags.BoolVar(&rv.Server.GroupAccessibleRepos, "group-accessible-repos", rv.Server.GroupAccessibleRepos, "let filesystem group be able to access repo files")
return rv
}
var version = "0.14.0"
func (app *restServerApp) tlsSettings() (bool, string, string, error) {
var key, cert string
if !app.Server.TLS && (app.Server.TLSKey != "" || app.Server.TLSCert != "") {
return false, "", "", errors.New("requires enabled TLS")
} else if !app.Server.TLS {
return false, "", "", nil
}
if app.Server.TLSKey != "" {
key = app.Server.TLSKey
} else {
key = filepath.Join(app.Server.Path, "private_key")
}
if app.Server.TLSCert != "" {
cert = app.Server.TLSCert
} else {
cert = filepath.Join(app.Server.Path, "public_key")
}
return app.Server.TLS, key, cert, nil
}
// returns the address that the app is listening on.
// returns nil if the application hasn't finished starting yet
func (app *restServerApp) ListenerAddress() net.Addr {
app.listenerAddressMu.Lock()
defer app.listenerAddressMu.Unlock()
return app.listenerAddress
}
func (app *restServerApp) runRoot(_ *cobra.Command, _ []string) error {
log.SetFlags(0)
log.Printf("Data directory: %s", app.Server.Path)
if app.CPUProfile != "" {
f, err := os.Create(app.CPUProfile)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
if err := pprof.StartCPUProfile(f); err != nil {
return err
}
defer pprof.StopCPUProfile()
log.Println("CPU profiling enabled")
defer log.Println("Stopped CPU profiling")
}
if app.Server.NoAuth {
log.Println("Authentication disabled")
} else {
if app.Server.ProxyAuthUsername == "" {
log.Println("Authentication enabled")
} else {
log.Println("Proxy Authentication enabled.")
}
}
handler, err := restserver.NewHandler(&app.Server)
if err != nil {
log.Fatalf("error: %v", err)
}
if app.Server.AppendOnly {
log.Println("Append only mode enabled")
} else {
log.Println("Append only mode disabled")
}
if app.Server.PrivateRepos {
log.Println("Private repositories enabled")
} else {
log.Println("Private repositories disabled")
}
if app.Server.GroupAccessibleRepos {
log.Println("Group accessible repos enabled")
} else {
log.Println("Group accessible repos disabled")
}
enabledTLS, privateKey, publicKey, err := app.tlsSettings()
if err != nil {
return err
}
listener, err := findListener(app.Server.Listen)
if err != nil {
return fmt.Errorf("unable to listen: %w", err)
}
// set listener address, this is useful for tests
app.listenerAddressMu.Lock()
app.listenerAddress = listener.Addr()
app.listenerAddressMu.Unlock()
tlscfg := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
},
}
switch app.Server.TLSMinVer {
case "1.2":
tlscfg.MinVersion = tls.VersionTLS12
case "1.3":
tlscfg.MinVersion = tls.VersionTLS13
default:
return fmt.Errorf("Unsupported TLS min version: %s. Allowed versions are 1.2 or 1.3", app.Server.TLSMinVer)
}
srv := &http.Server{
Handler: handler,
TLSConfig: tlscfg,
}
// run server in background
go func() {
if !enabledTLS {
err = srv.Serve(listener)
} else {
log.Printf("TLS enabled, private key %s, pubkey %v", privateKey, publicKey)
err = srv.ServeTLS(listener, publicKey, privateKey)
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen and serve returned err: %v", err)
}
}()
// wait until done
<-app.CmdRoot.Context().Done()
// gracefully shutdown server
if err := srv.Shutdown(context.Background()); err != nil {
return fmt.Errorf("server shutdown returned an err: %w", err)
}
log.Println("shutdown cleanly")
return nil
}
func main() {
// create context to be notified on interrupt or term signal so that we can shutdown cleanly
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := newRestServerApp().CmdRoot.ExecuteContext(ctx); err != nil {
log.Fatalf("error: %v", err)
}
}

View File

@@ -0,0 +1,284 @@
package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
restserver "github.com/restic/rest-server"
)
func TestTLSSettings(t *testing.T) {
type expected struct {
TLSKey string
TLSCert string
Error bool
}
type passed struct {
Path string
TLS bool
TLSKey string
TLSCert string
}
var tests = []struct {
passed passed
expected expected
}{
{passed{TLS: false}, expected{"", "", false}},
{passed{TLS: true}, expected{
filepath.Join(os.TempDir(), "restic/private_key"),
filepath.Join(os.TempDir(), "restic/public_key"),
false,
}},
{passed{
Path: os.TempDir(),
TLS: true,
}, expected{
filepath.Join(os.TempDir(), "private_key"),
filepath.Join(os.TempDir(), "public_key"),
false,
}},
{passed{Path: os.TempDir(), TLS: true, TLSKey: "/etc/restic/key", TLSCert: "/etc/restic/cert"}, expected{"/etc/restic/key", "/etc/restic/cert", false}},
{passed{Path: os.TempDir(), TLS: false, TLSKey: "/etc/restic/key", TLSCert: "/etc/restic/cert"}, expected{"", "", true}},
{passed{Path: os.TempDir(), TLS: false, TLSKey: "/etc/restic/key"}, expected{"", "", true}},
{passed{Path: os.TempDir(), TLS: false, TLSCert: "/etc/restic/cert"}, expected{"", "", true}},
}
for _, test := range tests {
app := newRestServerApp()
t.Run("", func(t *testing.T) {
// defer func() { restserver.Server = defaultConfig }()
if test.passed.Path != "" {
app.Server.Path = test.passed.Path
}
app.Server.TLS = test.passed.TLS
app.Server.TLSKey = test.passed.TLSKey
app.Server.TLSCert = test.passed.TLSCert
gotTLS, gotKey, gotCert, err := app.tlsSettings()
if err != nil && !test.expected.Error {
t.Fatalf("tls_settings returned err (%v)", err)
}
if test.expected.Error {
if err == nil {
t.Fatalf("Error not returned properly (%v)", test)
} else {
return
}
}
if gotTLS != test.passed.TLS {
t.Errorf("TLS enabled, want (%v), got (%v)", test.passed.TLS, gotTLS)
}
wantKey := test.expected.TLSKey
if gotKey != wantKey {
t.Errorf("wrong TLSPrivPath path, want (%v), got (%v)", wantKey, gotKey)
}
wantCert := test.expected.TLSCert
if gotCert != wantCert {
t.Errorf("wrong TLSCertPath path, want (%v), got (%v)", wantCert, gotCert)
}
})
}
}
func TestGetHandler(t *testing.T) {
dir, err := os.MkdirTemp("", "rest-server-test")
if err != nil {
t.Fatal(err)
}
defer func() {
err := os.Remove(dir)
if err != nil {
t.Fatal(err)
}
}()
getHandler := restserver.NewHandler
// With NoAuth = false and no .htpasswd
_, err = getHandler(&restserver.Server{Path: dir})
if err == nil {
t.Errorf("NoAuth=false: expected error, got nil")
}
// With NoAuth = true and no .htpasswd
_, err = getHandler(&restserver.Server{NoAuth: true, Path: dir})
if err != nil {
t.Errorf("NoAuth=true: expected no error, got %v", err)
}
// With NoAuth = false, no .htpasswd and ProxyAuth = X-Remote-User
_, err = getHandler(&restserver.Server{Path: dir, ProxyAuthUsername: "X-Remote-User"})
if err != nil {
t.Errorf("NoAuth=false, ProxyAuthUsername = X-Remote-User: expected no error, got %v", err)
}
// With NoAuth = false and custom .htpasswd
htpFile, err := os.CreateTemp(dir, "custom")
if err != nil {
t.Fatal(err)
}
defer func() {
err := os.Remove(htpFile.Name())
if err != nil {
t.Fatal(err)
}
}()
_, err = getHandler(&restserver.Server{HtpasswdPath: htpFile.Name()})
if err != nil {
t.Errorf("NoAuth=false with custom htpasswd: expected no error, got %v", err)
}
// Create .htpasswd
htpasswd := filepath.Join(dir, ".htpasswd")
err = os.WriteFile(htpasswd, []byte(""), 0644)
if err != nil {
t.Fatal(err)
}
defer func() {
err := os.Remove(htpasswd)
if err != nil {
t.Fatal(err)
}
}()
// With NoAuth = false and with .htpasswd
_, err = getHandler(&restserver.Server{Path: dir})
if err != nil {
t.Errorf("NoAuth=false with .htpasswd: expected no error, got %v", err)
}
}
// helper method to test the app. Starts app with passed arguments,
// then will call the callback function which can make requests against
// the application. If the callback function fails due to errors returned
// by http.Do() (i.e. *url.Error), then it will be retried until successful,
// or the passed timeout passes.
func testServerWithArgs(args []string, timeout time.Duration, cb func(context.Context, *restServerApp) error) error {
// create the app with passed args
app := newRestServerApp()
app.CmdRoot.SetArgs(args)
// create context that will timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// wait group for our client and server tasks
jobs := &sync.WaitGroup{}
jobs.Add(2)
// run the server, saving the error
var serverErr error
go func() {
defer jobs.Done()
defer cancel() // if the server is stopped, no point keep the client alive
serverErr = app.CmdRoot.ExecuteContext(ctx)
}()
// run the client, saving the error
var clientErr error
go func() {
defer jobs.Done()
defer cancel() // once the client is done, stop the server
var urlError *url.Error
// execute in loop, as we will retry for network errors
// (such as the server hasn't started yet)
for {
clientErr = cb(ctx, app)
switch {
case clientErr == nil:
return // success, we're done
case errors.As(clientErr, &urlError):
// if a network error (url.Error), then wait and retry
// as server may not be ready yet
select {
case <-time.After(time.Millisecond * 100):
continue
case <-ctx.Done(): // unless we run out of time first
clientErr = context.Canceled
return
}
default:
return // other error type, we're done
}
}
}()
// wait for both to complete
jobs.Wait()
// report back if either failed
if clientErr != nil || serverErr != nil {
return fmt.Errorf("client or server error, client: %v, server: %v", clientErr, serverErr)
}
return nil
}
func TestHttpListen(t *testing.T) {
td := t.TempDir()
// create some content and parent dirs
if err := os.MkdirAll(filepath.Join(td, "data", "repo1"), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(td, "data", "repo1", "config"), []byte("foo"), 0700); err != nil {
t.Fatal(err)
}
for _, args := range [][]string{
{"--no-auth", "--path", filepath.Join(td, "data"), "--listen", "127.0.0.1:0"}, // test emphemeral port
{"--no-auth", "--path", filepath.Join(td, "data"), "--listen", "127.0.0.1:9000"}, // test "normal" port
{"--no-auth", "--path", filepath.Join(td, "data"), "--listen", "127.0.0.1:9000"}, // test that server was shutdown cleanly and that we can re-use that port
} {
err := testServerWithArgs(args, time.Second*10, func(ctx context.Context, app *restServerApp) error {
for _, test := range []struct {
Path string
StatusCode int
}{
{"/repo1/", http.StatusMethodNotAllowed},
{"/repo1/config", http.StatusOK},
{"/repo2/config", http.StatusNotFound},
} {
listenAddr := app.ListenerAddress()
if listenAddr == nil {
return &url.Error{} // return this type of err, as we know this will retry
}
port := strings.Split(listenAddr.String(), ":")[1]
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:%s%s", port, test.Path), nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
err = resp.Body.Close()
if err != nil {
return err
}
if resp.StatusCode != test.StatusCode {
return fmt.Errorf("expected %d from server, instead got %d (path %s)", test.StatusCode, resp.StatusCode, test.Path)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
}

16
docker/create_user Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
if [ -z "$1" ]; then
echo "create_user [username]"
echo "or"
echo "create_user [username] [password]"
exit 1
fi
if [ -z "$2" ]; then
# password from prompt
htpasswd -B "$PASSWORD_FILE" "$1"
else
# read password from command line
htpasswd -B -b "$PASSWORD_FILE" "$1" "$2"
fi

8
docker/delete_user Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
if [ -z "$1" ]; then
echo "delete_user [username]"
exit 1
fi
htpasswd -D "$PASSWORD_FILE" "$1"

19
docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
set -e
if [ -n "$DISABLE_AUTHENTICATION" ]; then
OPTIONS="--no-auth $OPTIONS"
else
if [ ! -f "$PASSWORD_FILE" ]; then
( umask 027 && touch "$PASSWORD_FILE" )
fi
if [ ! -s "$PASSWORD_FILE" ]; then
echo
echo "**WARNING** No user exists, please 'docker exec -it \$CONTAINER_ID create_user'"
echo
fi
fi
exec rest-server --path "$DATA_DIRECTORY" --htpasswd-file "$PASSWORD_FILE" $OPTIONS

View File

@@ -1,16 +0,0 @@
[Unit]
Description=Rest Server
After=syslog.target
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/local/bin/rest-server --path /tmp/restic
Restart=always
RestartSec=5
StartLimitInterval=0
[Install]
WantedBy=multi-user.target

26
examples/bsd/freebsd Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
. /etc/rc.subr
name=restserver
rcvar=restserver_enable
start_cmd="${name}_start"
stop_cmd=":"
load_rc_config $name
: ${restserver_enable:=no}
: ${restserver_msg="Nothing started."}
datadir="/backups"
restserver_start()
{
rest-server --path $datadir \
--private-repos \
--tls \
--tls-cert "/etc/ssl/rest-server.crt" \
--tls-key "/etc/ssl/private/rest-server.key" &
}
run_rc_command "$1"

14
examples/bsd/openbsd Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/ksh
#
# $OpenBSD: $
daemon="/usr/local/bin/rest-server"
daemon_flags="--path /var/restic"
daemon_user="_restic"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_cmd $1

View File

@@ -0,0 +1,35 @@
# Rest Server Grafana Dashboard
This is a demo [Docker Compose](https://docs.docker.com/compose/) setup for [Rest Server](https://github.com/restic/rest-server) with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/).
![Grafana dashboard screenshot](screenshot.png)
## Quickstart
Build `rest-server` in Docker:
cd ../..
make docker_build
cd -
Bring up the Docker Compose stack:
docker-compose build
docker-compose up -d
Check if everything is up and running:
docker-compose ps
Grafana will be running on [http://localhost:8030/](http://localhost:8030/) with username "admin" and password "admin". The first time you access it you will be asked to setup a data source. Configure it like this (make sure you name it "prometheus", as this is hardcoded in the example dashboard):
![Add data source](datasource.png)
The Rest Server dashboard can be accessed on [http://localhost:8030/dashboard/file/rest-server.json](http://localhost:8030/dashboard/file/rest-server.json).
Prometheus can be accessed on [http://localhost:8020/](http://localhost:8020/).
If you do a backup like this, some graphs should show up:
restic -r rest:http://127.0.0.1:8010/demo1 -p ./demo-passwd init
restic -r rest:http://127.0.0.1:8010/demo1 -p ./demo-passwd backup .

View File

@@ -0,0 +1,637 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS-INFRA",
"label": "prometheus-infra",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "4.6.0"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"hideControls": false,
"id": null,
"links": [],
"refresh": "10s",
"rows": [
{
"collapse": false,
"height": 244,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS-INFRA}",
"fill": 1,
"id": 1,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(rest_server_blob_write_bytes_total{instance=\"$instance\"}[15s])) by ($group)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{$group}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Blob Write Throughput by $group",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS-INFRA}",
"fill": 1,
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(rest_server_blob_write_total{instance=\"$instance\"}[15s])) by ($group)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{$group}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Blob Write Operations by $group",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "ops",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
},
{
"collapse": false,
"height": 258,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS-INFRA}",
"fill": 1,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(rest_server_blob_read_bytes_total{instance=\"$instance\"}[15s])) by ($group)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{$group}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Blob Read Throughput by $group",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS-INFRA}",
"fill": 1,
"id": 5,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(rest_server_blob_read_total{instance=\"$instance\"}[15s])) by ($group)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{$group}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Blob Read Operations by $group",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "ops",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
},
{
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS-INFRA}",
"fill": 1,
"id": 3,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(rest_server_blob_delete_bytes_total{instance=\"$instance\"}[15s])) by ($group)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{$group}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Blob Delete Throughput by $group",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS-INFRA}",
"fill": 1,
"id": 6,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(rest_server_blob_delete_total{instance=\"$instance\"}[15s])) by ($group)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{$group}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Blob Delete Operations by $group",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "ops",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": false,
"title": "Dashboard Row",
"titleSize": "h6"
}
],
"schemaVersion": 14,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {},
"datasource": "${DS_PROMETHEUS-INFRA}",
"hide": 0,
"includeAll": false,
"label": "Instance",
"multi": false,
"name": "instance",
"options": [],
"query": "label_values(process_start_time_seconds{job=\"rest_server\"}, instance)",
"refresh": 2,
"regex": "",
"sort": 1,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": null,
"current": {
"tags": [],
"text": "type",
"value": "type"
},
"hide": 0,
"includeAll": false,
"label": "Group By",
"multi": false,
"name": "group",
"options": [
{
"selected": true,
"text": "type",
"value": "type"
},
{
"selected": false,
"text": "repo",
"value": "repo"
},
{
"selected": false,
"text": "user",
"value": "user"
}
],
"query": "type,repo,user",
"type": "custom"
}
]
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Restic Rest Server",
"version": 8
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1 @@
demo-passwd

View File

@@ -0,0 +1,59 @@
# Demo of rest-server with prometheus and grafana
version: '2'
services:
restserver:
# NOTE: You must run `make docker_build` in the repo root first
# If you want to run this in production, you want auth and tls!
build:
context: ../..
dockerfile: Dockerfile
volumes:
- data:/data
environment:
DISABLE_AUTHENTICATION: 1
OPTIONS: "--prometheus"
ports:
- "127.0.0.1:8010:8000"
networks:
- net
prometheus:
image: prom/prometheus
ports:
- "127.0.0.1:8020:9090"
volumes:
- prometheusdata:/prometheus
- ./prometheus:/etc/prometheus:ro
depends_on:
- restserver
networks:
- net
grafana:
image: grafana/grafana
volumes:
- grafanadata:/var/lib/grafana
- ./dashboards:/dashboards
- ./grafana.ini:/etc/grafana/grafana.ini
ports:
- "127.0.0.1:8030:3000"
environment:
GF_USERS_DEFAULT_THEME: light
# GF_INSTALL_PLUGINS: grafana-clock-panel,grafana-simple-json-datasource
depends_on:
- prometheus
networks:
- net
networks:
net:
volumes:
data:
driver: local
prometheusdata:
driver: local
grafanadata:
driver: local

View File

@@ -0,0 +1,313 @@
##################### Grafana Configuration Example #####################
#
# Everything has defaults so you only need to uncomment things you want to
# change
# possible values : production, development
; app_mode = production
# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
; instance_name = ${HOSTNAME}
#################################### Paths ####################################
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
#
;data = /var/lib/grafana
#
# Directory where grafana can store logs
#
;logs = /var/log/grafana
#
# Directory where grafana will automatically scan and look for plugins
#
;plugins = /var/lib/grafana/plugins
#
#################################### Server ####################################
[server]
# Protocol (http or https)
;protocol = http
# The ip address to bind to, empty will bind to all interfaces
;http_addr =
# The http port to use
;http_port = 3000
# The public facing domain name used to access grafana from a browser
;domain = localhost
# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
;enforce_domain = false
# The full public facing url
;root_url = %(protocol)s://%(domain)s:%(http_port)s/
# Log web requests
;router_logging = false
# the path relative working path
;static_root_path = public
# enable gzip
;enable_gzip = false
# https certs & key file
;cert_file =
;cert_key =
#################################### Database ####################################
[database]
# Either "mysql", "postgres" or "sqlite3", it's your choice
;type = sqlite3
;host = 127.0.0.1:3306
;name = grafana
;user = root
;password =
# For "postgres" only, either "disable", "require" or "verify-full"
;ssl_mode = disable
# For "sqlite3" only, path relative to data_path setting
;path = grafana.db
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
;provider = file
# Provider config options
# memory: not have any config yet
# file: session dir path, is relative to grafana data_path
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
# postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
;provider_config = sessions
# Session cookie name
;cookie_name = grafana_sess
# If you use session in https only, default is false
;cookie_secure = false
# Session life time, default is 86400
;session_life_time = 86400
#################################### Analytics ####################################
[analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
# No ip addresses are being tracked, only simple counters to track
# running instances, dashboard and error counts. It is very helpful to us.
# Change this option to false to disable reporting.
;reporting_enabled = true
# Set to false to disable all checks to https://grafana.net
# for new vesions (grafana itself and plugins), check is used
# in some UI views to notify that grafana or plugin update exists
# This option does not cause any auto updates, nor send any information
# only a GET request to http://grafana.net to get latest versions
check_for_updates = true
# Google Analytics universal tracking code, only enabled if you specify an id here
;google_analytics_ua_id =
#################################### Security ####################################
[security]
# default admin user, created on startup
;admin_user = admin
# default admin password, can be changed before first start of grafana, or in profile settings
;admin_password = admin
# used for signing
;secret_key = SW2YcwTIb9zpOOhoPsMm
# Auto-login remember days
;login_remember_days = 7
;cookie_username = grafana_user
;cookie_remember_name = grafana_remember
# disable gravatar profile images
;disable_gravatar = false
# data source proxy whitelist (ip_or_domain:port separated by spaces)
;data_source_proxy_whitelist =
[snapshots]
# snapshot sharing options
;external_enabled = true
;external_snapshot_url = https://snapshots-origin.raintank.io
;external_snapshot_name = Publish to snapshot.raintank.io
#################################### Users ####################################
[users]
# disable user signup / registration
;allow_sign_up = true
# Allow non admin users to create organizations
;allow_org_create = true
# Set to true to automatically assign new users to the default organization (id 1)
;auto_assign_org = true
# Default role new users will be automatically assigned (if disabled above is set to true)
;auto_assign_org_role = Viewer
# Background text for the user field on the login page
;login_hint = email or username
# Default UI theme ("dark" or "light")
default_theme = dark
#################################### Anonymous Auth ##########################
[auth.anonymous]
# enable anonymous access
;enabled = false
# specify organization name that should be used for unauthenticated users
;org_name = Main Org.
# specify role for unauthenticated users
;org_role = Viewer
#################################### Github Auth ##########################
[auth.github]
;enabled = false
;allow_sign_up = false
;client_id = some_id
;client_secret = some_secret
;scopes = user:email,read:org
;auth_url = https://github.com/login/oauth/authorize
;token_url = https://github.com/login/oauth/access_token
;api_url = https://api.github.com/user
;team_ids =
;allowed_organizations =
#################################### Google Auth ##########################
[auth.google]
;enabled = false
;allow_sign_up = false
;client_id = some_client_id
;client_secret = some_client_secret
;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
;auth_url = https://accounts.google.com/o/oauth2/auth
;token_url = https://accounts.google.com/o/oauth2/token
;api_url = https://www.googleapis.com/oauth2/v1/userinfo
;allowed_domains =
#################################### Auth Proxy ##########################
[auth.proxy]
;enabled = false
;header_name = X-WEBAUTH-USER
;header_property = username
;auto_sign_up = true
#################################### Basic Auth ##########################
[auth.basic]
;enabled = true
#################################### Auth LDAP ##########################
[auth.ldap]
;enabled = false
;config_file = /etc/grafana/ldap.toml
#################################### SMTP / Emailing ##########################
[smtp]
;enabled = false
;host = localhost:25
;user =
;password =
;cert_file =
;key_file =
;skip_verify = false
;from_address = admin@grafana.localhost
[emails]
;welcome_email_on_sign_up = false
#################################### Logging ##########################
[log]
# Either "console", "file", "syslog". Default is console and file
# Use space to separate multiple modes, e.g. "console file"
;mode = console, file
# Either "trace", "debug", "info", "warn", "error", "critical", default is "info"
;level = info
# For "console" mode only
[log.console]
;level =
# log line format, valid options are text, console and json
;format = console
# For "file" mode only
[log.file]
;level =
# log line format, valid options are text, console and json
;format = text
# This enables automated log rotate(switch of following options), default is true
;log_rotate = true
# Max line number of single file, default is 1000000
;max_lines = 1000000
# Max size shift of single file, default is 28 means 1 << 28, 256MB
;max_size_shift = 28
# Segment log daily, default is true
;daily_rotate = true
# Expired days of log file(delete after max days), default is 7
;max_days = 7
[log.syslog]
;level =
# log line format, valid options are text, console and json
;format = text
# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
;network =
;address =
# Syslog facility. user, daemon and local0 through local7 are valid.
;facility =
# Syslog tag. By default, the process' argv[0] is used.
;tag =
#################################### AMQP Event Publisher ##########################
[event_publisher]
;enabled = false
;rabbitmq_url = amqp://localhost/
;exchange = grafana_events
;#################################### Dashboard JSON files ##########################
[dashboards.json]
enabled = true
path = /dashboards
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /api/metrics
[metrics]
# Disable / Enable internal metrics
;enabled = true
# Publish interval
;interval_seconds = 10
# Send internal metrics to Graphite
; [metrics.graphite]
; address = localhost:2003
; prefix = prod.grafana.%(instance_name)s.
#################################### Internal Grafana Metrics ##########################
# Url used to to import dashboards directly from Grafana.net
[grafana_net]
url = https://grafana.net

View File

@@ -0,0 +1,23 @@
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'restic-rest-server-demo'
scrape_configs:
- job_name: 'prometheus' # monitor self
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']
- job_name: 'rest_server'
scrape_interval: 5s
# Uncomment these if you use auth and/or https
#basic_auth:
# username: test
# password: test
#scheme: https
static_configs:
- targets: ['restserver:8000']

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -0,0 +1,83 @@
[Unit]
Description=Rest Server
After=syslog.target
After=network.target
Requires=rest-server.socket
After=rest-server.socket
[Service]
Type=simple
# You may prefer to use a different user or group on your system.
User=www-data
Group=www-data
ExecStart=/usr/local/bin/rest-server --path /path/to/backups
Restart=always
RestartSec=5
# The following options are available (in systemd v247) to restrict the
# actions of the rest-server.
# As a whole, the purpose of these are to provide an additional layer of
# security by mitigating any unknown security vulnerabilities which may exist
# in rest-server or in the libraries, tools and operating system components
# which it relies upon.
# IMPORTANT!
# The following line must be customised to your individual requirements.
ReadWritePaths=/path/to/backups
# Files in the data repository are only user accessible by default. Default to
# `UMask=077` for consistency. To make created files group-readable, set to
# `UMask=007` and pass `--group-accessible-repos` to rest-server via `ExecStart`.
UMask=077
# If your system doesn't support all of the features below (e.g. because of
# the use of an older version of systemd), you may wish to comment-out
# some of the lines below as appropriate.
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=yes
# As the listen socket is created by systemd via the rest-server.socket unit, it is
# no longer necessary for rest-server to have access to the host network namespace.
PrivateNetwork=yes
PrivateTmp=yes
PrivateDevices=true
PrivateUsers=true
ProtectSystem=strict
ProtectHome=yes
ProtectClock=true
ProtectControlGroups=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectHostname=true
RemoveIPC=true
RestrictNamespaces=true
RestrictAddressFamilies=none
RestrictSUIDSGID=true
RestrictRealtime=true
# if your service crashes with "code=killed, status=31/SYS", you probably tried to run linux_i386 (32bit) binary on a amd64 host
SystemCallArchitectures=native
SystemCallFilter=@system-service
# Additionally, you may wish to use some of the systemd options documented in
# systemd.resource-control(5) to limit the CPU, memory, file-system I/O and
# network I/O that the rest-server is permitted to consume according to the
# individual requirements of your installation.
#CPUQuota=25%
#MemoryHigh=bytes
#MemoryMax=bytes
#MemorySwapMax=bytes
#TasksMax=N
#IOReadBandwidthMax=device bytes
#IOWriteBandwidthMax=device bytes
#IOReadIOPSMax=device IOPS, IOWriteIOPSMax=device IOPS
#IPAccounting=true
#IPAddressAllow=
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,5 @@
[Socket]
ListenStream = 8000
[Install]
WantedBy = sockets.target

25
glide.lock generated
View File

@@ -1,25 +0,0 @@
hash: 2a84da35c7f6887fc08d80cd20da4e38731a81ea9845e1f137f1ba9913d0268d
updated: 2017-05-31T23:28:23.41107346+02:00
imports:
- name: github.com/gorilla/handlers
version: a4043c62cc2329bacda331d33fc908ab11ef0ec3
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/spf13/cobra
version: 8d4ce3549a0bf0e3569df3aae7423b7743cd05a9
- name: github.com/spf13/pflag
version: e57e3eeb33f795204c1ca35f56c44f83227c6e66
- name: goji.io
version: 0d89ff54b2c18c9c4ba530e32496aef902d3c6cd
repo: https://github.com/goji/goji
vcs: git
subpackages:
- internal
- middleware
- pat
- pattern
- name: golang.org/x/net
version: 45e771701b814666a7eb299e6c7a57d0b1799e91
repo: https://github.com/golang/net
vcs: git
testImports: []

View File

@@ -1,12 +0,0 @@
package: github.com/restic/rest-server
import:
- package: goji.io
version: v2.0
repo: https://github.com/goji/goji
vcs: git
- package: golang.org/x/net
version: 45e771701b814666a7eb299e6c7a57d0b1799e91
repo: https://github.com/golang/net
vcs: git
- package: github.com/spf13/cobra
- package: github.com/spf13/pflag

28
go.mod Normal file
View File

@@ -0,0 +1,28 @@
module github.com/restic/rest-server
go 1.23.0
require (
github.com/coreos/go-systemd/v22 v22.5.0
github.com/gorilla/handlers v1.5.2
github.com/minio/sha256-simd v1.0.1
github.com/miolini/datacounter v1.0.3
github.com/prometheus/client_golang v1.22.0
github.com/spf13/cobra v1.9.1
golang.org/x/crypto v0.38.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/sys v0.33.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)

56
go.sum Normal file
View File

@@ -0,0 +1,56 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA=
github.com/miolini/datacounter v1.0.3/go.mod h1:C45dc2hBumHjDpEU64IqPwR6TDyPVpzOqqRTN7zmBUA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,339 +1,203 @@
package main
package restserver
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"errors"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"sync"
"goji.io/middleware"
"goji.io/pat"
"github.com/restic/rest-server/quota"
"github.com/restic/rest-server/repo"
)
func isHashed(dir string) bool {
return dir == "data"
// Server encapsulates the rest-server's settings and repo management logic
type Server struct {
Path string
HtpasswdPath string
Listen string
Log string
CPUProfile string
TLSKey string
TLSCert string
TLSMinVer string
TLS bool
NoAuth bool
ProxyAuthUsername string
AppendOnly bool
PrivateRepos bool
Prometheus bool
PrometheusNoAuth bool
Debug bool
MaxRepoSize int64
PanicOnError bool
NoVerifyUpload bool
GroupAccessibleRepos bool
htpasswdFile *HtpasswdFile
quotaManager *quota.Manager
fsyncWarning sync.Once
}
func getRepo(r *http.Request) string {
if strings.HasPrefix(fmt.Sprintf("%s", middleware.Pattern(r.Context())), "/:repo") {
return filepath.Join(config.path, pat.Param(r, "repo"))
}
// MaxFolderDepth is the maxDepth param passed to splitURLPath.
// A max depth of 2 mean that we accept folders like: '/', '/foo' and '/foo/bar'
// TODO: Move to a Server option
const MaxFolderDepth = 2
return config.path
// httpDefaultError write a HTTP error with the default description
func httpDefaultError(w http.ResponseWriter, code int) {
http.Error(w, http.StatusText(code), code)
}
// AuthHandler wraps h with a http.HandlerFunc that performs basic authentication against the user/passwords pairs
// stored in f and returns the http.HandlerFunc.
func AuthHandler(f *HtpasswdFile, h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username, password, ok := r.BasicAuth(); !ok || !f.Validate(username, password) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
}
}
// CheckConfig checks whether a configuration exists.
func CheckConfig(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("CheckConfig()")
}
cfg := filepath.Join(getRepo(r), "config")
st, err := os.Stat(cfg)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
// ServeHTTP makes this server an http.Handler. It handlers the administrative
// part of the request (figuring out the filesystem location, performing
// authentication, etc) and then passes it on to repo.Handler for actual
// REST API processing.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// First of all, check auth (will always pass if NoAuth is set)
username, ok := s.checkAuth(r)
if !ok {
httpDefaultError(w, http.StatusUnauthorized)
return
}
w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}
// GetConfig allows for a config to be retrieved.
func GetConfig(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("GetConfig()")
}
cfg := filepath.Join(getRepo(r), "config")
bytes, err := ioutil.ReadFile(cfg)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
// Perform the path parsing to determine the repo folder and remainder for the
// repo handler.
folderPath, remainder := splitURLPath(r.URL.Path, MaxFolderDepth)
if !folderPathValid(folderPath) {
log.Printf("Invalid request path: %s", r.URL.Path)
httpDefaultError(w, http.StatusNotFound)
return
}
w.Write(bytes)
}
// SaveConfig allows for a config to be saved.
func SaveConfig(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("SaveConfig()")
}
cfg := filepath.Join(getRepo(r), "config")
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if err := ioutil.WriteFile(cfg, bytes, 0600); err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
// DeleteConfig removes a config.
func DeleteConfig(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("DeleteConfig()")
}
if err := os.Remove(filepath.Join(getRepo(r), "config")); err != nil {
if config.debug {
log.Print(err)
}
if os.IsNotExist(err) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
}
// ListBlobs lists all blobs of a given type in an arbitrary order.
func ListBlobs(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("ListBlobs()")
}
dir := pat.Param(r, "type")
path := filepath.Join(getRepo(r), dir)
items, err := ioutil.ReadDir(path)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
var names []string
for _, i := range items {
if isHashed(dir) {
subpath := filepath.Join(path, i.Name())
subitems, err := ioutil.ReadDir(subpath)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
for _, f := range subitems {
names = append(names, f.Name())
}
} else {
names = append(names, i.Name())
}
}
data, err := json.Marshal(names)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Write(data)
}
// CheckBlob tests whether a blob exists.
func CheckBlob(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("CheckBlob()")
}
dir := pat.Param(r, "type")
name := pat.Param(r, "name")
if isHashed(dir) {
name = filepath.Join(name[:2], name)
}
path := filepath.Join(getRepo(r), dir, name)
st, err := os.Stat(path)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}
// GetBlob retrieves a blob from the repository.
func GetBlob(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("GetBlob()")
}
dir := pat.Param(r, "type")
name := pat.Param(r, "name")
if isHashed(dir) {
name = filepath.Join(name[:2], name)
}
path := filepath.Join(getRepo(r), dir, name)
file, err := os.Open(path)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
http.ServeContent(w, r, "", time.Unix(0, 0), file)
file.Close()
}
// SaveBlob saves a blob to the repository.
func SaveBlob(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("SaveBlob()")
}
repo := getRepo(r)
dir := pat.Param(r, "type")
name := pat.Param(r, "name")
if isHashed(dir) {
name = filepath.Join(name[:2], name)
}
path := filepath.Join(repo, dir, name)
tf, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600)
if err != nil {
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if _, err := io.Copy(tf, r.Body); err != nil {
tf.Close()
os.Remove(path)
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if err := tf.Sync(); err != nil {
tf.Close()
os.Remove(path)
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if err := tf.Close(); err != nil {
os.Remove(path)
if config.debug {
log.Print(err)
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
// DeleteBlob deletes a blob from the repository.
func DeleteBlob(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("DeleteBlob()")
}
dir := pat.Param(r, "type")
name := pat.Param(r, "name")
if isHashed(dir) {
name = filepath.Join(name[:2], name)
}
path := filepath.Join(getRepo(r), dir, name)
if err := os.Remove(path); err != nil {
if config.debug {
log.Print(err)
}
if os.IsNotExist(err) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
}
// CreateRepo creates repository directories.
func CreateRepo(w http.ResponseWriter, r *http.Request) {
if config.debug {
log.Println("CreateRepo()")
}
repo := getRepo(r)
if r.URL.Query().Get("create") != "true" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
log.Printf("Creating repository directories in %s\n", repo)
if err := os.MkdirAll(repo, 0700); err != nil {
log.Print(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, d := range []string{"data", "index", "keys", "locks", "snapshots", "tmp"} {
if err := os.MkdirAll(filepath.Join(repo, d), 0700); err != nil {
log.Print(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
// Check if the current user is allowed to access this path
if !s.NoAuth && s.PrivateRepos {
if len(folderPath) == 0 || folderPath[0] != username {
httpDefaultError(w, http.StatusUnauthorized)
return
}
}
for i := 0; i < 256; i++ {
if err := os.MkdirAll(filepath.Join(repo, "data", fmt.Sprintf("%02x", i)), 0700); err != nil {
log.Print(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
// Determine filesystem path for this repo
fsPath, err := join(s.Path, folderPath...)
if err != nil {
// We did not expect an error at this stage, because we just checked the path
log.Printf("Unexpected join error for path %q", r.URL.Path)
httpDefaultError(w, http.StatusNotFound)
return
}
// Pass the request to the repo.Handler
opt := repo.Options{
AppendOnly: s.AppendOnly,
Debug: s.Debug,
QuotaManager: s.quotaManager, // may be nil
PanicOnError: s.PanicOnError,
NoVerifyUpload: s.NoVerifyUpload,
FsyncWarning: &s.fsyncWarning,
GroupAccessible: s.GroupAccessibleRepos,
}
if s.Prometheus {
opt.BlobMetricFunc = makeBlobMetricFunc(username, folderPath)
}
repoHandler, err := repo.New(fsPath, opt)
if err != nil {
log.Printf("repo.New error: %v", err)
httpDefaultError(w, http.StatusInternalServerError)
return
}
r.URL.Path = remainder // strip folderPath for next handler
repoHandler.ServeHTTP(w, r)
}
func valid(name string) bool {
// taken from net/http.Dir
if strings.Contains(name, "\x00") {
return false
}
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
return false
}
return true
}
func isValidType(name string) bool {
for _, tpe := range repo.ObjectTypes {
if name == tpe {
return true
}
}
for _, tpe := range repo.FileTypes {
if name == tpe {
return true
}
}
return false
}
// join takes a number of path names, sanitizes them, and returns them joined
// with base for the current operating system to use (dirs separated by
// filepath.Separator). The returned path is always either equal to base or a
// subdir of base.
func join(base string, names ...string) (string, error) {
clean := make([]string, 0, len(names)+1)
clean = append(clean, base)
// taken from net/http.Dir
for _, name := range names {
if !valid(name) {
return "", errors.New("invalid character in path")
}
clean = append(clean, filepath.FromSlash(path.Clean("/"+name)))
}
return filepath.Join(clean...), nil
}
// splitURLPath splits the URL path into a folderPath of the subrepo, and
// a remainder that can be passed to repo.Handler.
// Example: /foo/bar/locks/0123... will be split into:
//
// ["foo", "bar"] and "/locks/0123..."
func splitURLPath(urlPath string, maxDepth int) (folderPath []string, remainder string) {
if !strings.HasPrefix(urlPath, "/") {
// Really should start with "/"
return nil, urlPath
}
p := strings.SplitN(urlPath, "/", maxDepth+2)
// Skip the empty first one and the remainder in the last one
for _, name := range p[1 : len(p)-1] {
if isValidType(name) {
// We found a part that is a special repo file or dir
break
}
folderPath = append(folderPath, name)
}
// If the folder path is empty, the whole path is the remainder (do not strip '/')
if len(folderPath) == 0 {
return nil, urlPath
}
// Check that the urlPath starts with the reconstructed path, which should
// always be the case.
fullFolderPath := "/" + strings.Join(folderPath, "/")
if !strings.HasPrefix(urlPath, fullFolderPath) {
return nil, urlPath
}
return folderPath, urlPath[len(fullFolderPath):]
}
// folderPathValid checks if a folderPath returned by splitURLPath is valid and
// safe.
func folderPathValid(folderPath []string) bool {
for _, name := range folderPath {
if name == "" || name == ".." || name == "." || !valid(name) {
return false
}
}
return true
}

592
handlers_test.go Normal file
View File

@@ -0,0 +1,592 @@
package restserver
import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"github.com/minio/sha256-simd"
)
func TestJoin(t *testing.T) {
var tests = []struct {
base string
names []string
result string
}{
{"/", []string{"foo", "bar"}, "/foo/bar"},
{"/srv/server", []string{"foo", "bar"}, "/srv/server/foo/bar"},
{"/srv/server", []string{"foo", "..", "bar"}, "/srv/server/foo/bar"},
{"/srv/server", []string{"..", "bar"}, "/srv/server/bar"},
{"/srv/server", []string{".."}, "/srv/server"},
{"/srv/server", []string{"..", ".."}, "/srv/server"},
{"/srv/server", []string{"repo", "data"}, "/srv/server/repo/data"},
{"/srv/server", []string{"repo", "data", "..", ".."}, "/srv/server/repo/data"},
{"/srv/server", []string{"repo", "data", "..", "data", "..", "..", ".."}, "/srv/server/repo/data/data"},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
got, err := join(filepath.FromSlash(test.base), test.names...)
if err != nil {
t.Fatal(err)
}
want := filepath.FromSlash(test.result)
if got != want {
t.Fatalf("wrong result returned, want %v, got %v", want, got)
}
})
}
}
// declare a few helper functions
// wantFunc tests the HTTP response in res and calls t.Error() if something is incorrect.
type wantFunc func(t testing.TB, res *httptest.ResponseRecorder)
// newRequest returns a new HTTP request with the given params. On error, t.Fatal is called.
func newRequest(t testing.TB, method, path string, body io.Reader) *http.Request {
req, err := http.NewRequest(method, path, body)
if err != nil {
t.Fatal(err)
}
return req
}
// wantCode returns a function which checks that the response has the correct HTTP status code.
func wantCode(code int) wantFunc {
return func(t testing.TB, res *httptest.ResponseRecorder) {
t.Helper()
if res.Code != code {
t.Errorf("wrong response code, want %v, got %v", code, res.Code)
}
}
}
// wantBody returns a function which checks that the response has the data in the body.
func wantBody(body string) wantFunc {
return func(t testing.TB, res *httptest.ResponseRecorder) {
t.Helper()
if res.Body == nil {
t.Errorf("body is nil, want %q", body)
return
}
if !bytes.Equal(res.Body.Bytes(), []byte(body)) {
t.Errorf("wrong response body, want:\n %q\ngot:\n %q", body, res.Body.Bytes())
}
}
}
// checkRequest uses f to process the request and runs the checker functions on the result.
func checkRequest(t testing.TB, f http.HandlerFunc, req *http.Request, want []wantFunc) {
t.Helper()
rr := httptest.NewRecorder()
f(rr, req)
for _, fn := range want {
fn(t, rr)
}
}
// TestRequest is a sequence of HTTP requests with (optional) tests for the response.
type TestRequest struct {
req *http.Request
want []wantFunc
}
// createOverwriteDeleteSeq returns a sequence which will create a new file at
// path, and then try to overwrite and delete it.
func createOverwriteDeleteSeq(t testing.TB, path string, data string) []TestRequest {
// add a file, try to overwrite and delete it
req := []TestRequest{
{
req: newRequest(t, "GET", path, nil),
want: []wantFunc{wantCode(http.StatusNotFound)},
},
}
if !strings.HasSuffix(path, "/config") {
req = append(req, TestRequest{
// broken upload must fail
req: newRequest(t, "POST", path, strings.NewReader(data+"broken")),
want: []wantFunc{wantCode(http.StatusBadRequest)},
})
}
req = append(req,
TestRequest{
req: newRequest(t, "POST", path, strings.NewReader(data)),
want: []wantFunc{wantCode(http.StatusOK)},
},
TestRequest{
req: newRequest(t, "GET", path, nil),
want: []wantFunc{
wantCode(http.StatusOK),
wantBody(data),
},
},
TestRequest{
req: newRequest(t, "POST", path, strings.NewReader(data+"other stuff")),
want: []wantFunc{wantCode(http.StatusForbidden)},
},
TestRequest{
req: newRequest(t, "GET", path, nil),
want: []wantFunc{
wantCode(http.StatusOK),
wantBody(data),
},
},
TestRequest{
req: newRequest(t, "DELETE", path, nil),
want: []wantFunc{wantCode(http.StatusForbidden)},
},
TestRequest{
req: newRequest(t, "GET", path, nil),
want: []wantFunc{
wantCode(http.StatusOK),
wantBody(data),
},
},
)
return req
}
func createTestHandler(t *testing.T, conf *Server) (http.Handler, string, string, string, func()) {
buf := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, buf)
if err != nil {
t.Fatal(err)
}
data := "random data file " + hex.EncodeToString(buf)
dataHash := sha256.Sum256([]byte(data))
fileID := hex.EncodeToString(dataHash[:])
// setup the server with a local backend in a temporary directory
tempdir, err := os.MkdirTemp("", "rest-server-test-")
if err != nil {
t.Fatal(err)
}
// make sure the tempdir is properly removed
cleanup := func() {
err := os.RemoveAll(tempdir)
if err != nil {
t.Fatal(err)
}
}
conf.Path = tempdir
mux, err := NewHandler(conf)
if err != nil {
t.Fatalf("error from NewHandler: %v", err)
}
return mux, data, fileID, tempdir, cleanup
}
// TestResticAppendOnlyHandler runs tests on the restic handler code, especially in append-only mode.
func TestResticAppendOnlyHandler(t *testing.T) {
mux, data, fileID, _, cleanup := createTestHandler(t, &Server{
AppendOnly: true,
NoAuth: true,
Debug: true,
PanicOnError: true,
})
defer cleanup()
var tests = []struct {
seq []TestRequest
}{
{createOverwriteDeleteSeq(t, "/config", data)},
{createOverwriteDeleteSeq(t, "/data/"+fileID, data)},
{
// ensure we can add and remove lock files
[]TestRequest{
{
req: newRequest(t, "GET", "/locks/"+fileID, nil),
want: []wantFunc{wantCode(http.StatusNotFound)},
},
{
req: newRequest(t, "POST", "/locks/"+fileID, strings.NewReader(data+"broken")),
want: []wantFunc{wantCode(http.StatusBadRequest)},
},
{
req: newRequest(t, "POST", "/locks/"+fileID, strings.NewReader(data)),
want: []wantFunc{wantCode(http.StatusOK)},
},
{
req: newRequest(t, "GET", "/locks/"+fileID, nil),
want: []wantFunc{
wantCode(http.StatusOK),
wantBody(data),
},
},
{
req: newRequest(t, "POST", "/locks/"+fileID, strings.NewReader(data+"other data")),
want: []wantFunc{wantCode(http.StatusForbidden)},
},
{
req: newRequest(t, "DELETE", "/locks/"+fileID, nil),
want: []wantFunc{wantCode(http.StatusOK)},
},
{
req: newRequest(t, "GET", "/locks/"+fileID, nil),
want: []wantFunc{wantCode(http.StatusNotFound)},
},
},
},
// Test subrepos
{createOverwriteDeleteSeq(t, "/parent1/sub1/config", "foobar")},
{createOverwriteDeleteSeq(t, "/parent1/sub1/data/"+fileID, data)},
{createOverwriteDeleteSeq(t, "/parent1/config", "foobar")},
{createOverwriteDeleteSeq(t, "/parent1/data/"+fileID, data)},
{createOverwriteDeleteSeq(t, "/parent2/config", "foobar")},
{createOverwriteDeleteSeq(t, "/parent2/data/"+fileID, data)},
}
// create the repos
for _, path := range []string{"/", "/parent1/sub1/", "/parent1/", "/parent2/"} {
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", path+"?create=true", nil),
[]wantFunc{wantCode(http.StatusOK)})
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
for i, seq := range test.seq {
t.Logf("request %v: %v %v", i, seq.req.Method, seq.req.URL.Path)
checkRequest(t, mux.ServeHTTP, seq.req, seq.want)
}
})
}
}
// createOverwriteDeleteSeq returns a sequence which will create a new file at
// path, and then deletes it twice.
func createIdempotentDeleteSeq(t testing.TB, path string, data string) []TestRequest {
return []TestRequest{
{
req: newRequest(t, "POST", path, strings.NewReader(data)),
want: []wantFunc{wantCode(http.StatusOK)},
},
{
req: newRequest(t, "DELETE", path, nil),
want: []wantFunc{wantCode(http.StatusOK)},
},
{
req: newRequest(t, "GET", path, nil),
want: []wantFunc{wantCode(http.StatusNotFound)},
},
{
req: newRequest(t, "DELETE", path, nil),
want: []wantFunc{wantCode(http.StatusOK)},
},
}
}
// TestResticHandler runs tests on the restic handler code, especially in append-only mode.
func TestResticHandler(t *testing.T) {
mux, data, fileID, _, cleanup := createTestHandler(t, &Server{
NoAuth: true,
Debug: true,
PanicOnError: true,
})
defer cleanup()
var tests = []struct {
seq []TestRequest
}{
{createIdempotentDeleteSeq(t, "/config", data)},
{createIdempotentDeleteSeq(t, "/data/"+fileID, data)},
}
// create the repo
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/?create=true", nil),
[]wantFunc{wantCode(http.StatusOK)})
for _, test := range tests {
t.Run("", func(t *testing.T) {
for i, seq := range test.seq {
t.Logf("request %v: %v %v", i, seq.req.Method, seq.req.URL.Path)
checkRequest(t, mux.ServeHTTP, seq.req, seq.want)
}
})
}
}
// TestResticErrorHandler runs tests on the restic handler error handling.
func TestResticErrorHandler(t *testing.T) {
mux, _, _, tempdir, cleanup := createTestHandler(t, &Server{
AppendOnly: true,
NoAuth: true,
Debug: true,
})
defer cleanup()
var tests = []struct {
seq []TestRequest
}{
// Test inaccessible file
{
[]TestRequest{{
req: newRequest(t, "GET", "/config", nil),
want: []wantFunc{wantCode(http.StatusInternalServerError)},
}},
},
{
[]TestRequest{{
req: newRequest(t, "GET", "/parent4/config", nil),
want: []wantFunc{wantCode(http.StatusNotFound)},
}},
},
}
// create the repo
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/?create=true", nil),
[]wantFunc{wantCode(http.StatusOK)})
// create inaccessible config
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/config", strings.NewReader("example")),
[]wantFunc{wantCode(http.StatusOK)})
err := os.Chmod(path.Join(tempdir, "config"), 0o000)
if err != nil {
t.Fatal(err)
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
for i, seq := range test.seq {
t.Logf("request %v: %v %v", i, seq.req.Method, seq.req.URL.Path)
checkRequest(t, mux.ServeHTTP, seq.req, seq.want)
}
})
}
}
func TestEmptyList(t *testing.T) {
mux, _, _, _, cleanup := createTestHandler(t, &Server{
AppendOnly: true,
NoAuth: true,
Debug: true,
})
defer cleanup()
// create the repo
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/?create=true", nil),
[]wantFunc{wantCode(http.StatusOK)})
for i := 1; i <= 2; i++ {
req := newRequest(t, "GET", "/data/", nil)
if i == 2 {
req.Header.Set("Accept", "application/vnd.x.restic.rest.v2")
}
checkRequest(t, mux.ServeHTTP, req,
[]wantFunc{wantCode(http.StatusOK), wantBody("[]")})
}
}
func TestListWithUnexpectedFiles(t *testing.T) {
mux, _, _, tempdir, cleanup := createTestHandler(t, &Server{
AppendOnly: true,
NoAuth: true,
Debug: true,
})
defer cleanup()
// create the repo
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/?create=true", nil),
[]wantFunc{wantCode(http.StatusOK)})
err := os.WriteFile(path.Join(tempdir, "data", "temp"), []byte{}, 0o666)
if err != nil {
t.Fatalf("creating unexpected file failed: %v", err)
}
for i := 1; i <= 2; i++ {
req := newRequest(t, "GET", "/data/", nil)
if i == 2 {
req.Header.Set("Accept", "application/vnd.x.restic.rest.v2")
}
checkRequest(t, mux.ServeHTTP, req,
[]wantFunc{wantCode(http.StatusOK)})
}
}
func TestSplitURLPath(t *testing.T) {
var tests = []struct {
// Params
urlPath string
maxDepth int
// Expected result
folderPath []string
remainder string
}{
{"/", 0, nil, "/"},
{"/", 2, nil, "/"},
{"/foo/bar/locks/0123", 0, nil, "/foo/bar/locks/0123"},
{"/foo/bar/locks/0123", 1, []string{"foo"}, "/bar/locks/0123"},
{"/foo/bar/locks/0123", 2, []string{"foo", "bar"}, "/locks/0123"},
{"/foo/bar/locks/0123", 3, []string{"foo", "bar"}, "/locks/0123"},
{"/foo/bar/zzz/locks/0123", 2, []string{"foo", "bar"}, "/zzz/locks/0123"},
{"/foo/bar/zzz/locks/0123", 3, []string{"foo", "bar", "zzz"}, "/locks/0123"},
{"/foo/bar/locks/", 2, []string{"foo", "bar"}, "/locks/"},
{"/foo/locks/", 2, []string{"foo"}, "/locks/"},
{"/foo/data/", 2, []string{"foo"}, "/data/"},
{"/foo/index/", 2, []string{"foo"}, "/index/"},
{"/foo/keys/", 2, []string{"foo"}, "/keys/"},
{"/foo/snapshots/", 2, []string{"foo"}, "/snapshots/"},
{"/foo/config", 2, []string{"foo"}, "/config"},
{"/foo/", 2, []string{"foo"}, "/"},
{"/foo/bar/", 2, []string{"foo", "bar"}, "/"},
{"/foo/bar", 2, []string{"foo"}, "/bar"},
{"/locks/", 2, nil, "/locks/"},
// This function only splits, it does not check the path components!
{"/././locks/", 2, []string{".", "."}, "/locks/"},
{"/../../locks/", 2, []string{"..", ".."}, "/locks/"},
{"///locks/", 2, []string{"", ""}, "/locks/"},
{"////locks/", 2, []string{"", ""}, "//locks/"},
// Robustness against broken input
{"/", -42, nil, "/"},
{"foo", 2, nil, "foo"},
{"foo/bar", 2, nil, "foo/bar"},
{"", 2, nil, ""},
}
for i, test := range tests {
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
folderPath, remainder := splitURLPath(test.urlPath, test.maxDepth)
var fpEqual bool
if len(test.folderPath) == 0 && len(folderPath) == 0 {
fpEqual = true // this check allows for nil vs empty slice
} else {
fpEqual = reflect.DeepEqual(test.folderPath, folderPath)
}
if !fpEqual {
t.Errorf("wrong folderPath: want %v, got %v", test.folderPath, folderPath)
}
if test.remainder != remainder {
t.Errorf("wrong remainder: want %v, got %v", test.remainder, remainder)
}
})
}
}
// delayErrorReader blocks until Continue is closed, closes the channel FirstRead and then returns Err.
type delayErrorReader struct {
FirstRead chan struct{}
firstReadOnce sync.Once
Err error
Continue chan struct{}
}
func newDelayedErrorReader(err error) *delayErrorReader {
return &delayErrorReader{
Err: err,
Continue: make(chan struct{}),
FirstRead: make(chan struct{}),
}
}
func (d *delayErrorReader) Read(_ []byte) (int, error) {
d.firstReadOnce.Do(func() {
// close the channel to signal that the first read has happened
close(d.FirstRead)
})
<-d.Continue
return 0, d.Err
}
// TestAbortedRequest runs tests with concurrent upload requests for the same file.
func TestAbortedRequest(t *testing.T) {
// the race condition doesn't happen for append-only repositories
mux, _, _, _, cleanup := createTestHandler(t, &Server{
NoAuth: true,
Debug: true,
PanicOnError: true,
})
defer cleanup()
// create the repo
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/?create=true", nil),
[]wantFunc{wantCode(http.StatusOK)})
var (
id = "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
wg sync.WaitGroup
)
// the first request is an upload to a file which blocks while reading the
// body and then after some data returns an error
rd := newDelayedErrorReader(io.ErrUnexpectedEOF)
wg.Add(1)
go func() {
defer wg.Done()
// first, read some string, then read from rd (which blocks and then
// returns an error)
dataReader := io.MultiReader(strings.NewReader("invalid data from aborted request\n"), rd)
t.Logf("start first upload")
req := newRequest(t, "POST", "/data/"+id, dataReader)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
t.Logf("first upload done, response %v (%v)", rr.Code, rr.Result().Status)
}()
// wait until the first request starts reading from the body
<-rd.FirstRead
// then while the first request is blocked we send a second request to
// delete the file and a third request to upload to the file again, only
// then the first request is unblocked.
t.Logf("delete file")
checkRequest(t, mux.ServeHTTP,
newRequest(t, "DELETE", "/data/"+id, nil),
nil) // don't check anything, restic also ignores errors here
t.Logf("upload again")
checkRequest(t, mux.ServeHTTP,
newRequest(t, "POST", "/data/"+id, strings.NewReader("foo\n")),
[]wantFunc{wantCode(http.StatusOK)})
// unblock the reader for the first request now so it can continue
close(rd.Continue)
// wait for the first request to continue
wg.Wait()
// request the file again, it must exist and contain the string from the
// second request
checkRequest(t, mux.ServeHTTP,
newRequest(t, "GET", "/data/"+id, nil),
[]wantFunc{
wantCode(http.StatusOK),
wantBody("foo\n"),
},
)
}

View File

@@ -1,7 +1,7 @@
package main
package restserver
/*
Copied from: github.com/bitly/oauth2_proxy
Original version copied from: github.com/bitly/oauth2_proxy
MIT License
@@ -26,65 +26,259 @@ THE SOFTWARE.
import (
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/csv"
"io"
"log"
"os"
"os/signal"
"regexp"
"sync"
"syscall"
"time"
"golang.org/x/crypto/bcrypt"
)
// CheckInterval represents how often we check for changes in htpasswd file.
const CheckInterval = 30 * time.Second
// PasswordCacheDuration represents how long authentication credentials are
// cached in memory after they were successfully verified. This allows avoiding
// repeatedly verifying the same authentication credentials.
const PasswordCacheDuration = time.Minute
// Lookup passwords in a htpasswd file. The entries must have been created with -s for SHA encryption.
type cacheEntry struct {
expiry time.Time
verifier []byte
}
// HtpasswdFile is a map for usernames to passwords.
type HtpasswdFile struct {
Users map[string]string
mutex sync.Mutex
path string
stat os.FileInfo
throttle chan struct{}
users map[string]string
cache map[string]cacheEntry
}
// NewHtpasswdFromFile reads the users and passwords from a htpasswd file and returns them. If an error is encountered,
// it is returned, together with a nil-Pointer for the HtpasswdFile.
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
r, err := os.Open(path)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
stat, err := os.Stat(path)
if err != nil {
return nil, err
}
defer r.Close()
return NewHtpasswd(r)
h := &HtpasswdFile{
mutex: sync.Mutex{},
path: path,
stat: stat,
throttle: make(chan struct{}),
cache: make(map[string]cacheEntry),
}
if err := h.Reload(); err != nil {
return nil, err
}
// Start a goroutine that limits reload checks to once per CheckInterval
go h.throttleTimer()
go h.expiryTimer()
go func() {
for range c {
err := h.Reload()
if err == nil {
log.Printf("Reloaded htpasswd file")
} else {
log.Printf("Could not reload htpasswd file: %v", err)
}
}
}()
return h, nil
}
// NewHtpasswd reads the users and passwords from a htpasswd datastream in file and returns them. If an error is
// encountered, it is returned, together with a nil-Pointer for the HtpasswdFile.
func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {
cr := csv.NewReader(file)
// throttleTimer sends at most one message per CheckInterval to throttle file change checks.
func (h *HtpasswdFile) throttleTimer() {
var check struct{}
for {
time.Sleep(CheckInterval)
h.throttle <- check
}
}
func (h *HtpasswdFile) expiryTimer() {
for {
time.Sleep(5 * time.Second)
now := time.Now()
h.mutex.Lock()
var zeros [sha256.Size]byte
// try to wipe expired cache entries
for user, entry := range h.cache {
if entry.expiry.After(now) {
copy(entry.verifier, zeros[:])
delete(h.cache, user)
}
}
h.mutex.Unlock()
}
}
var validUsernameRegexp = regexp.MustCompile(`^[\p{L}\d@._-]+$`)
// Reload reloads the htpasswd file. If the reload fails, the Users map is not changed and the error is returned.
func (h *HtpasswdFile) Reload() error {
r, err := os.Open(h.path)
if err != nil {
return err
}
cr := csv.NewReader(r)
cr.Comma = ':'
cr.Comment = '#'
cr.TrimLeadingSpace = true
records, err := cr.ReadAll()
if err != nil {
return nil, err
_ = r.Close()
return err
}
h := &HtpasswdFile{Users: make(map[string]string)}
users := make(map[string]string)
for _, record := range records {
h.Users[record[0]] = record[1]
if !validUsernameRegexp.MatchString(record[0]) {
log.Printf("Ignoring invalid username %q in htpasswd, consists of characters other than letters, numbers, '_', '-', '.' and '@'", record[0])
continue
}
users[record[0]] = record[1]
}
return h, nil
// Replace the Users map
h.mutex.Lock()
var zeros [sha256.Size]byte
// try to wipe the old cache entries
for _, entry := range h.cache {
copy(entry.verifier, zeros[:])
}
h.cache = make(map[string]cacheEntry)
h.users = users
h.mutex.Unlock()
_ = r.Close()
return nil
}
// ReloadCheck checks at most once per CheckInterval if the file changed and will reload the file if it did.
// It logs errors and successful reloads, and returns an error if any was encountered.
func (h *HtpasswdFile) ReloadCheck() error {
select {
case <-h.throttle:
stat, err := os.Stat(h.path)
if err != nil {
log.Printf("Could not stat htpasswd file: %v", err)
return err
}
reload := false
h.mutex.Lock()
if stat.ModTime() != h.stat.ModTime() || stat.Size() != h.stat.Size() {
reload = true
h.stat = stat
}
h.mutex.Unlock()
if reload {
err := h.Reload()
if err == nil {
log.Printf("Reloaded htpasswd file")
} else {
log.Printf("Could not reload htpasswd file: %v", err)
return err
}
}
default:
// No need to check
}
return nil
}
var shaRe = regexp.MustCompile(`^{SHA}`)
var bcrRe = regexp.MustCompile(`^\$2b\$|^\$2a\$|^\$2y\$`)
// Validate returns true if password matches the stored password for user. If no password for user is stored, or the
// password is wrong, false is returned.
func (h *HtpasswdFile) Validate(user string, password string) bool {
realPassword, exists := h.Users[user]
_ = h.ReloadCheck()
hash := sha256.New()
// hash.Write can never fail
_, _ = hash.Write([]byte(user))
_, _ = hash.Write([]byte(":"))
_, _ = hash.Write([]byte(password))
h.mutex.Lock()
// avoid race conditions with cache replacements
cache := h.cache
hashedPassword, exists := h.users[user]
entry, cacheExists := h.cache[user]
h.mutex.Unlock()
if !exists {
return false
}
if realPassword[:5] == "{SHA}" {
if cacheExists && subtle.ConstantTimeCompare(entry.verifier, hash.Sum(nil)) == 1 {
h.mutex.Lock()
// repurpose mutex to prevent concurrent cache updates
// extend cache entry
cache[user] = cacheEntry{
verifier: entry.verifier,
expiry: time.Now().Add(PasswordCacheDuration),
}
h.mutex.Unlock()
return true
}
isValid := isMatchingHashAndPassword(hashedPassword, password)
if !isValid {
log.Printf("Invalid htpasswd entry for %s.", user)
return false
}
h.mutex.Lock()
// repurpose mutex to prevent concurrent cache updates
cache[user] = cacheEntry{
verifier: hash.Sum(nil),
expiry: time.Now().Add(PasswordCacheDuration),
}
h.mutex.Unlock()
return true
}
func isMatchingHashAndPassword(hashedPassword string, password string) bool {
switch {
case shaRe.MatchString(hashedPassword):
d := sha1.New()
d.Write([]byte(password))
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
_, _ = d.Write([]byte(password))
if subtle.ConstantTimeCompare([]byte(hashedPassword[5:]), []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) == 1 {
return true
}
case bcrRe.MatchString(hashedPassword):
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
if err == nil {
return true
}
} else {
log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
}
return false
}

45
htpasswd_test.go Normal file
View File

@@ -0,0 +1,45 @@
package restserver
import (
"os"
"testing"
)
func TestValidate(t *testing.T) {
user := "restic"
pwd := "$2y$05$z/OEmNQamd6m6LSegUErh.r/Owk9Xwmc5lxDheIuHY2Z7XiS6FtJm"
rawPwd := "test"
wrongPwd := "wrong"
tmpfile, err := os.CreateTemp("", "rest-validate-")
if err != nil {
t.Fatal(err)
}
if _, err = tmpfile.Write([]byte(user + ":" + pwd + "\n")); err != nil {
t.Fatal(err)
}
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
htpass, err := NewHtpasswdFromFile(tmpfile.Name())
if err != nil {
t.Fatal(err)
}
for i := 0; i < 10; i++ {
isValid := htpass.Validate(user, rawPwd)
if !isValid {
t.Fatal("correct password not accepted")
}
isValid = htpass.Validate(user, wrongPwd)
if isValid {
t.Fatal("wrong password accepted")
}
}
if err = os.Remove(tmpfile.Name()); err != nil {
t.Fatal(err)
}
}

154
main.go
View File

@@ -1,154 +0,0 @@
package main
import (
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"github.com/gorilla/handlers"
"github.com/spf13/cobra"
"goji.io"
"goji.io/pat"
)
// cmdRoot is the base command when no other command has been specified.
var cmdRoot = &cobra.Command{
Use: "rest-server",
Short: "Run a REST server for use with restic",
SilenceErrors: true,
SilenceUsage: true,
RunE: runRoot,
}
var config = struct {
path string
listen string
tls bool
log string
cpuprofile string
debug bool
}{}
func init() {
flags := cmdRoot.Flags()
flags.StringVar(&config.cpuprofile, "cpuprofile", "", "write CPU profile to file")
flags.BoolVar(&config.debug, "debug", false, "output debug messages")
flags.StringVar(&config.listen, "listen", ":8000", "listen address")
flags.StringVar(&config.log, "log", "", "log HTTP requests in the combined log format")
flags.StringVar(&config.path, "path", "/tmp/restic", "data directory")
flags.BoolVar(&config.tls, "tls", false, "turn on TLS support")
}
func debugHandler(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
next.ServeHTTP(w, r)
})
}
func logHandler(next http.Handler) http.Handler {
accessLog, err := os.OpenFile(config.log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
return handlers.CombinedLoggingHandler(accessLog, next)
}
func setupMux() *goji.Mux {
mux := goji.NewMux()
if config.debug {
mux.Use(debugHandler)
}
if config.log != "" {
mux.Use(logHandler)
}
mux.HandleFunc(pat.Head("/config"), CheckConfig)
mux.HandleFunc(pat.Head("/:repo/config"), CheckConfig)
mux.HandleFunc(pat.Get("/config"), GetConfig)
mux.HandleFunc(pat.Get("/:repo/config"), GetConfig)
mux.HandleFunc(pat.Post("/config"), SaveConfig)
mux.HandleFunc(pat.Post("/:repo/config"), SaveConfig)
mux.HandleFunc(pat.Delete("/config"), DeleteConfig)
mux.HandleFunc(pat.Delete("/:repo/config"), DeleteConfig)
mux.HandleFunc(pat.Get("/:type/"), ListBlobs)
mux.HandleFunc(pat.Get("/:repo/:type/"), ListBlobs)
mux.HandleFunc(pat.Head("/:type/:name"), CheckBlob)
mux.HandleFunc(pat.Head("/:repo/:type/:name"), CheckBlob)
mux.HandleFunc(pat.Get("/:type/:name"), GetBlob)
mux.HandleFunc(pat.Get("/:repo/:type/:name"), GetBlob)
mux.HandleFunc(pat.Post("/:type/:name"), SaveBlob)
mux.HandleFunc(pat.Post("/:repo/:type/:name"), SaveBlob)
mux.HandleFunc(pat.Delete("/:type/:name"), DeleteBlob)
mux.HandleFunc(pat.Delete("/:repo/:type/:name"), DeleteBlob)
mux.HandleFunc(pat.Post("/"), CreateRepo)
mux.HandleFunc(pat.Post("/:repo"), CreateRepo)
mux.HandleFunc(pat.Post("/:repo/"), CreateRepo)
return mux
}
var version = "manually"
func runRoot(cmd *cobra.Command, args []string) error {
log.SetFlags(0)
log.Printf("rest-server %s compiled with %v on %v/%v\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
log.Printf("Data directory: %s", config.path)
if config.cpuprofile != "" {
f, err := os.Create(config.cpuprofile)
if err != nil {
log.Fatal(err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal(err)
}
log.Println("CPU profiling enabled")
defer pprof.StopCPUProfile()
}
mux := setupMux()
var handler http.Handler
htpasswdFile, err := NewHtpasswdFromFile(filepath.Join(config.path, ".htpasswd"))
if err != nil {
handler = mux
log.Println("Authentication disabled")
} else {
handler = AuthHandler(htpasswdFile, mux)
log.Println("Authentication enabled")
}
if !config.tls {
log.Printf("Starting server on %s\n", config.listen)
err = http.ListenAndServe(config.listen, handler)
} else {
privateKey := filepath.Join(config.path, "private_key")
publicKey := filepath.Join(config.path, "public_key")
log.Println("TLS enabled")
log.Printf("Private key: %s", privateKey)
log.Printf("Public key: %s", publicKey)
log.Printf("Starting server on %s\n", config.listen)
err = http.ListenAndServeTLS(config.listen, publicKey, privateKey, handler)
}
if err != nil {
log.Fatal(err)
}
return nil
}
func main() {
if err := cmdRoot.Execute(); err != nil {
log.Fatalf("error: %v", err)
}
}

92
metrics.go Normal file
View File

@@ -0,0 +1,92 @@
package restserver
import (
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/restic/rest-server/repo"
)
var metricLabelList = []string{"user", "repo", "type"}
var metricBlobWriteTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "rest_server_blob_write_total",
Help: "Total number of blobs written",
},
metricLabelList,
)
var metricBlobWriteBytesTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "rest_server_blob_write_bytes_total",
Help: "Total number of bytes written to blobs",
},
metricLabelList,
)
var metricBlobReadTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "rest_server_blob_read_total",
Help: "Total number of blobs read",
},
metricLabelList,
)
var metricBlobReadBytesTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "rest_server_blob_read_bytes_total",
Help: "Total number of bytes read from blobs",
},
metricLabelList,
)
var metricBlobDeleteTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "rest_server_blob_delete_total",
Help: "Total number of blobs deleted",
},
metricLabelList,
)
var metricBlobDeleteBytesTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "rest_server_blob_delete_bytes_total",
Help: "Total number of bytes of blobs deleted",
},
metricLabelList,
)
// makeBlobMetricFunc creates a metrics callback function that increments the
// Prometheus metrics.
func makeBlobMetricFunc(username string, folderPath []string) repo.BlobMetricFunc {
var f repo.BlobMetricFunc = func(objectType string, operation repo.BlobOperation, nBytes uint64) {
labels := prometheus.Labels{
"user": username,
"repo": strings.Join(folderPath, "/"),
"type": objectType,
}
switch operation {
case repo.BlobRead:
metricBlobReadTotal.With(labels).Inc()
metricBlobReadBytesTotal.With(labels).Add(float64(nBytes))
case repo.BlobWrite:
metricBlobWriteTotal.With(labels).Inc()
metricBlobWriteBytesTotal.With(labels).Add(float64(nBytes))
case repo.BlobDelete:
metricBlobDeleteTotal.With(labels).Inc()
metricBlobDeleteBytesTotal.With(labels).Add(float64(nBytes))
}
}
return f
}
func init() {
// These are always initialized, but only updated if Config.Prometheus is set
prometheus.MustRegister(metricBlobWriteTotal)
prometheus.MustRegister(metricBlobWriteBytesTotal)
prometheus.MustRegister(metricBlobReadTotal)
prometheus.MustRegister(metricBlobReadBytesTotal)
prometheus.MustRegister(metricBlobDeleteTotal)
prometheus.MustRegister(metricBlobDeleteBytesTotal)
}

118
mux.go Normal file
View File

@@ -0,0 +1,118 @@
package restserver
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/gorilla/handlers"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/restic/rest-server/quota"
)
func (s *Server) debugHandler(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
next.ServeHTTP(w, r)
})
}
func (s *Server) logHandler(next http.Handler) http.Handler {
var accessLog io.Writer
if s.Log == "-" {
accessLog = os.Stdout
} else {
var err error
accessLog, err = os.OpenFile(s.Log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("error: %v", err)
}
}
return handlers.CombinedLoggingHandler(accessLog, next)
}
func (s *Server) checkAuth(r *http.Request) (username string, ok bool) {
if s.NoAuth {
return username, true
}
if s.ProxyAuthUsername != "" {
username = r.Header.Get(s.ProxyAuthUsername)
if username == "" {
return "", false
}
} else {
var password string
username, password, ok = r.BasicAuth()
if !ok || !s.htpasswdFile.Validate(username, password) {
return "", false
}
}
return username, true
}
func (s *Server) wrapMetricsAuth(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username, ok := s.checkAuth(r)
if !ok {
httpDefaultError(w, http.StatusUnauthorized)
return
}
if s.PrivateRepos && username != "metrics" {
httpDefaultError(w, http.StatusUnauthorized)
return
}
f(w, r)
}
}
// NewHandler returns the master HTTP multiplexer/router.
func NewHandler(server *Server) (http.Handler, error) {
if !server.NoAuth && server.ProxyAuthUsername == "" {
var err error
if server.HtpasswdPath == "" {
server.HtpasswdPath = filepath.Join(server.Path, ".htpasswd")
}
server.htpasswdFile, err = NewHtpasswdFromFile(server.HtpasswdPath)
if err != nil {
return nil, fmt.Errorf("cannot load %s (use --no-auth to disable): %v", server.HtpasswdPath, err)
}
log.Printf("Loaded htpasswd file %s", server.HtpasswdPath)
}
const GiB = 1024 * 1024 * 1024
if server.MaxRepoSize > 0 {
log.Printf("Initializing quota (can take a while)...")
qm, err := quota.New(server.Path, server.MaxRepoSize)
if err != nil {
return nil, err
}
server.quotaManager = qm
log.Printf("Quota initialized, currently using %.2f GiB", float64(qm.SpaceUsed())/GiB)
}
mux := http.NewServeMux()
if server.Prometheus {
if server.PrometheusNoAuth {
mux.Handle("/metrics", promhttp.Handler())
} else {
mux.HandleFunc("/metrics", server.wrapMetricsAuth(promhttp.Handler().ServeHTTP))
}
}
mux.Handle("/", server)
var handler http.Handler = mux
if server.Debug {
handler = server.debugHandler(handler)
}
if server.Log != "" {
handler = server.logHandler(handler)
}
return handler, nil
}

80
mux_test.go Normal file
View File

@@ -0,0 +1,80 @@
package restserver
import (
"net/http/httptest"
"testing"
)
func TestCheckAuth(t *testing.T) {
tests := []struct {
name string
server *Server
requestHeaders map[string]string
basicAuth bool
basicUser string
basicPassword string
expectedUser string
expectedOk bool
}{
{
name: "NoAuth enabled",
server: &Server{
NoAuth: true,
},
expectedOk: true,
},
{
name: "Proxy Auth successful",
server: &Server{
ProxyAuthUsername: "X-Remote-User",
},
requestHeaders: map[string]string{
"X-Remote-User": "restic",
},
expectedUser: "restic",
expectedOk: true,
},
{
name: "Proxy Auth empty header",
server: &Server{
ProxyAuthUsername: "X-Remote-User",
},
requestHeaders: map[string]string{
"X-Remote-User": "",
},
expectedOk: false,
},
{
name: "Proxy Auth missing header",
server: &Server{
ProxyAuthUsername: "X-Remote-User",
},
expectedOk: false,
},
{
name: "Proxy Auth send but not enabled",
server: &Server{},
requestHeaders: map[string]string{
"X-Remote-User": "restic",
},
expectedOk: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
for header, value := range tt.requestHeaders {
req.Header.Set(header, value)
}
if tt.basicAuth {
req.SetBasicAuth(tt.basicUser, tt.basicPassword)
}
username, ok := tt.server.checkAuth(req)
if username != tt.expectedUser || ok != tt.expectedOk {
t.Errorf("expected (%v, %v), got (%v, %v)", tt.expectedUser, tt.expectedOk, username, ok)
}
})
}
}

124
quota/quota.go Normal file
View File

@@ -0,0 +1,124 @@
package quota
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"sync/atomic"
)
// New creates a new quota Manager for given path.
// It will tally the current disk usage before returning.
func New(path string, maxSize int64) (*Manager, error) {
m := &Manager{
path: path,
maxRepoSize: maxSize,
}
if err := m.updateSize(); err != nil {
return nil, err
}
return m, nil
}
// Manager manages the repo quota for given filesystem root path, including subrepos
type Manager struct {
path string
maxRepoSize int64
repoSize int64 // must be accessed using sync/atomic
}
// WrapWriter limits the number of bytes written
// to the space that is currently available as given by
// the server's MaxRepoSize. This type is safe for use
// by multiple goroutines sharing the same *Server.
type maxSizeWriter struct {
io.Writer
m *Manager
}
func (w maxSizeWriter) Write(p []byte) (n int, err error) {
if int64(len(p)) > w.m.SpaceRemaining() {
return 0, fmt.Errorf("repository has reached maximum size (%d bytes)", w.m.maxRepoSize)
}
n, err = w.Writer.Write(p)
w.m.IncUsage(int64(n))
return n, err
}
func (m *Manager) updateSize() error {
// if we haven't yet computed the size of the repo, do so now
initialSize, err := tallySize(m.path)
if err != nil {
return err
}
atomic.StoreInt64(&m.repoSize, initialSize)
return nil
}
// WrapWriter wraps w in a writer that enforces s.MaxRepoSize.
// If there is an error, a status code and the error are returned.
func (m *Manager) WrapWriter(req *http.Request, w io.Writer) (io.Writer, int, error) {
currentSize := atomic.LoadInt64(&m.repoSize)
// if content-length is set and is trustworthy, we can save some time
// and issue a polite error if it declares a size that's too big; since
// we expect the vast majority of clients will be honest, so this check
// can only help save time
if contentLenStr := req.Header.Get("Content-Length"); contentLenStr != "" {
contentLen, err := strconv.ParseInt(contentLenStr, 10, 64)
if err != nil {
return nil, http.StatusLengthRequired, err
}
if currentSize+contentLen > m.maxRepoSize {
err := fmt.Errorf("incoming blob (%d bytes) would exceed maximum size of repository (%d bytes)",
contentLen, m.maxRepoSize)
return nil, http.StatusInsufficientStorage, err
}
}
// since we can't always trust content-length, we will wrap the writer
// in a custom writer that enforces the size limit during writes
return maxSizeWriter{Writer: w, m: m}, 0, nil
}
// SpaceRemaining returns how much space is available in the repo
// according to s.MaxRepoSize. s.repoSize must already be set.
// If there is no limit, -1 is returned.
func (m *Manager) SpaceRemaining() int64 {
if m.maxRepoSize == 0 {
return -1
}
maxSize := m.maxRepoSize
currentSize := atomic.LoadInt64(&m.repoSize)
return maxSize - currentSize
}
// SpaceUsed returns how much space is used in the repo.
func (m *Manager) SpaceUsed() int64 {
return atomic.LoadInt64(&m.repoSize)
}
// IncUsage increments the current repo size (which
// must already be initialized).
func (m *Manager) IncUsage(by int64) {
atomic.AddInt64(&m.repoSize, by)
}
// tallySize counts the size of the contents of path.
func tallySize(path string) (int64, error) {
if path == "" {
path = "."
}
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
size += info.Size()
return nil
})
return size, err
}

823
repo/repo.go Normal file
View File

@@ -0,0 +1,823 @@
package repo
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/minio/sha256-simd"
"github.com/miolini/datacounter"
"github.com/restic/rest-server/quota"
)
// Options are options for the Handler accepted by New
type Options struct {
AppendOnly bool // if set, delete actions are not allowed
Debug bool
NoVerifyUpload bool
// If set, we will panic when an internal server error happens. This
// makes it easier to debug such errors.
PanicOnError bool
BlobMetricFunc BlobMetricFunc
QuotaManager *quota.Manager
FsyncWarning *sync.Once
// If set makes files group accessible
GroupAccessible bool
// Defaults dir and file mode
dirMode os.FileMode
fileMode os.FileMode
}
// DefaultDirMode is the file mode used for directory creation if not
// overridden in the Options
const DefaultDirMode os.FileMode = 0700
// DefaultFileMode is the file mode used for file creation if not
// overridden in the Options
const DefaultFileMode os.FileMode = 0600
// GroupAccessibleDirMode is the file mode used for directory creation when
// group access is enabled
const GroupAccessibleDirMode os.FileMode = 0770
// GroupAccessibleFileMode is the file mode used for file creation when
// group access is enabled
const GroupAccessibleFileMode os.FileMode = 0660
// New creates a new Handler for a single Restic backup repo.
// path is the full filesystem path to this repo directory.
// opt is a set of options.
func New(path string, opt Options) (*Handler, error) {
if path == "" {
return nil, fmt.Errorf("path is required")
}
opt.dirMode = DefaultDirMode
opt.fileMode = DefaultFileMode
if opt.GroupAccessible {
opt.dirMode = GroupAccessibleDirMode
opt.fileMode = GroupAccessibleFileMode
}
h := Handler{
path: path,
opt: opt,
}
return &h, nil
}
// Handler handles all REST API requests for a single Restic backup repo
// Spec: https://restic.readthedocs.io/en/latest/100_references.html#rest-backend
type Handler struct {
path string // filesystem path of repo
opt Options
}
// httpDefaultError write a HTTP error with the default description
func httpDefaultError(w http.ResponseWriter, code int) {
http.Error(w, http.StatusText(code), code)
}
// httpMethodNotAllowed writes a 405 Method Not Allowed HTTP error with
// the required Allow header listing the methods that are allowed.
func httpMethodNotAllowed(w http.ResponseWriter, allowed []string) {
w.Header().Set("Allow", strings.Join(allowed, ", "))
httpDefaultError(w, http.StatusMethodNotAllowed)
}
// errFileContentDoesntMatchHash is the error raised when the file content hash
// doesn't match the hash provided in the URL
var errFileContentDoesntMatchHash = errors.New("file content does not match hash")
// BlobPathRE matches valid blob URI paths with optional object IDs
var BlobPathRE = regexp.MustCompile(`^/(data|index|keys|locks|snapshots)/([0-9a-f]{64})?$`)
// ObjectTypes are subdirs that are used for object storage
var ObjectTypes = []string{"data", "index", "keys", "locks", "snapshots"}
// FileTypes are files stored directly under the repo direct that are accessible
// through a request
var FileTypes = []string{"config"}
func isHashed(objectType string) bool {
return objectType == "data"
}
// BlobOperation describe the current blob operation in the BlobMetricFunc callback.
type BlobOperation byte
// Define all valid operations.
const (
BlobRead = 'R' // A blob has been read
BlobWrite = 'W' // A blob has been written
BlobDelete = 'D' // A blob has been deleted
)
// BlobMetricFunc is the callback signature for blob metrics. Such a callback
// can be passed in the Options to keep track of various metrics.
// objectType: one of ObjectTypes
// operation: one of the BlobOperations above
// nBytes: the number of bytes affected, or 0 if not relevant
// TODO: Perhaps add http.Request for the username so that this can be cached?
type BlobMetricFunc func(objectType string, operation BlobOperation, nBytes uint64)
// ServeHTTP performs strict matching on the repo part of the URL path and
// dispatches the request to the appropriate handler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
if urlPath == "/" {
// TODO: add HEAD and GET
switch r.Method {
case "POST":
h.createRepo(w, r)
default:
httpMethodNotAllowed(w, []string{"POST"})
}
return
} else if urlPath == "/config" {
switch r.Method {
case "HEAD":
h.checkConfig(w, r)
case "GET":
h.getConfig(w, r)
case "POST":
h.saveConfig(w, r)
case "DELETE":
h.deleteConfig(w, r)
default:
httpMethodNotAllowed(w, []string{"HEAD", "GET", "POST", "DELETE"})
}
return
} else if objectType, objectID := h.getObject(urlPath); objectType != "" {
if objectID == "" {
// TODO: add HEAD
switch r.Method {
case "GET":
h.listBlobs(w, r)
default:
httpMethodNotAllowed(w, []string{"GET"})
}
return
}
switch r.Method {
case "HEAD":
h.checkBlob(w, r)
case "GET":
h.getBlob(w, r)
case "POST":
h.saveBlob(w, r)
case "DELETE":
h.deleteBlob(w, r)
default:
httpMethodNotAllowed(w, []string{"HEAD", "GET", "POST", "DELETE"})
}
return
}
httpDefaultError(w, http.StatusNotFound)
}
// getObject parses the URL path and returns the objectType and objectID,
// if any. The objectID is optional.
func (h *Handler) getObject(urlPath string) (objectType, objectID string) {
m := BlobPathRE.FindStringSubmatch(urlPath)
if len(m) == 0 {
return "", "" // no match
}
if len(m) == 2 || m[2] == "" {
return m[1], "" // no objectID
}
return m[1], m[2]
}
// getSubPath returns the path for a file or subdir in the root of the repo.
func (h *Handler) getSubPath(name string) string {
return filepath.Join(h.path, name)
}
// getObjectPath returns the path for an object file in the repo.
// The passed in objectType and objectID must be valid due to earlier validation
func (h *Handler) getObjectPath(objectType, objectID string) string {
// If we hit an error, this is a programming error, because all of these
// must have been validated before. We still check them here as a safeguard.
if objectType == "" || objectID == "" {
panic("invalid objectType or objectID")
}
if isHashed(objectType) {
if len(objectID) < 2 {
// Should never happen, because BlobPathRE checked this
panic("getObjectPath: objectID shorter than 2 chars")
}
// Added another dir in between with the first two characters of the hash
return filepath.Join(h.path, objectType, objectID[:2], objectID)
}
return filepath.Join(h.path, objectType, objectID)
}
// sendMetric calls op.BlobMetricFunc if set. See its signature for details.
func (h *Handler) sendMetric(objectType string, operation BlobOperation, nBytes uint64) {
if f := h.opt.BlobMetricFunc; f != nil {
f(objectType, operation, nBytes)
}
}
// needSize tells you if we need the file size for metrics of quota accounting
func (h *Handler) needSize() bool {
return h.opt.BlobMetricFunc != nil || h.opt.QuotaManager != nil
}
// incrementRepoSpaceUsage increments the repo space usage if quota are enabled
func (h *Handler) incrementRepoSpaceUsage(by int64) {
if h.opt.QuotaManager != nil {
h.opt.QuotaManager.IncUsage(by)
}
}
// wrapFileWriter wraps the file writer if repo quota are enabled, and returns it
// as is if not.
// If an error occurs, it returns both an error and the appropriate HTTP error code.
func (h *Handler) wrapFileWriter(r *http.Request, w io.Writer) (io.Writer, int, error) {
if h.opt.QuotaManager == nil {
return w, 0, nil // unmodified
}
return h.opt.QuotaManager.WrapWriter(r, w)
}
// checkConfig checks whether a configuration exists.
func (h *Handler) checkConfig(w http.ResponseWriter, _ *http.Request) {
if h.opt.Debug {
log.Println("checkConfig()")
}
cfg := h.getSubPath("config")
st, err := os.Stat(cfg)
if err != nil {
h.fileAccessError(w, err)
return
}
w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}
// getConfig allows for a config to be retrieved.
func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
if h.opt.Debug {
log.Println("getConfig()")
}
cfg := h.getSubPath("config")
bytes, err := os.ReadFile(cfg)
if err != nil {
h.fileAccessError(w, err)
return
}
_, _ = w.Write(bytes)
}
// saveConfig allows for a config to be saved.
func (h *Handler) saveConfig(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("saveConfig()")
}
cfg := h.getSubPath("config")
f, err := os.OpenFile(cfg, os.O_CREATE|os.O_WRONLY|os.O_EXCL, h.opt.fileMode)
if err != nil && os.IsExist(err) {
if h.opt.Debug {
log.Print(err)
}
httpDefaultError(w, http.StatusForbidden)
return
}
_, err = io.Copy(f, r.Body)
if err != nil {
h.internalServerError(w, err)
return
}
err = f.Close()
if err != nil {
h.internalServerError(w, err)
return
}
_ = r.Body.Close()
}
// deleteConfig removes a config.
func (h *Handler) deleteConfig(w http.ResponseWriter, _ *http.Request) {
if h.opt.Debug {
log.Println("deleteConfig()")
}
if h.opt.AppendOnly {
httpDefaultError(w, http.StatusForbidden)
return
}
cfg := h.getSubPath("config")
if err := os.Remove(cfg); err != nil {
// ignore not exist errors to make deleting idempotent, which is
// necessary to properly handle request retries
if !errors.Is(err, os.ErrNotExist) {
h.fileAccessError(w, err)
}
return
}
}
const (
mimeTypeAPIV1 = "application/vnd.x.restic.rest.v1"
mimeTypeAPIV2 = "application/vnd.x.restic.rest.v2"
)
// listBlobs lists all blobs of a given type in an arbitrary order.
func (h *Handler) listBlobs(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("listBlobs()")
}
switch r.Header.Get("Accept") {
case mimeTypeAPIV2:
h.listBlobsV2(w, r)
default:
h.listBlobsV1(w, r)
}
}
// listBlobsV1 lists all blobs of a given type in an arbitrary order.
// TODO: unify listBlobsV1 and listBlobsV2
func (h *Handler) listBlobsV1(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("listBlobsV1()")
}
objectType, _ := h.getObject(r.URL.Path)
if objectType == "" {
h.internalServerError(w, fmt.Errorf(
"cannot determine object type: %s", r.URL.Path))
return
}
path := h.getSubPath(objectType)
items, err := os.ReadDir(path)
if err != nil {
h.fileAccessError(w, err)
return
}
names := []string{}
for _, i := range items {
if isHashed(objectType) {
if !i.IsDir() {
// ignore files in intermediate directories
continue
}
subpath := filepath.Join(path, i.Name())
var subitems []os.DirEntry
subitems, err = os.ReadDir(subpath)
if err != nil {
h.fileAccessError(w, err)
return
}
for _, f := range subitems {
names = append(names, f.Name())
}
} else {
names = append(names, i.Name())
}
}
data, err := json.Marshal(names)
if err != nil {
h.internalServerError(w, err)
return
}
w.Header().Set("Content-Type", mimeTypeAPIV1)
_, _ = w.Write(data)
}
// Blob represents a single blob, its name and its size.
type Blob struct {
Name string `json:"name"`
Size int64 `json:"size"`
}
// listBlobsV2 lists all blobs of a given type, together with their sizes, in an arbitrary order.
// TODO: unify listBlobsV1 and listBlobsV2
func (h *Handler) listBlobsV2(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("listBlobsV2()")
}
objectType, _ := h.getObject(r.URL.Path)
if objectType == "" {
h.internalServerError(w, fmt.Errorf(
"cannot determine object type: %s", r.URL.Path))
return
}
path := h.getSubPath(objectType)
items, err := os.ReadDir(path)
if err != nil {
h.fileAccessError(w, err)
return
}
blobs := []Blob{}
for _, i := range items {
if isHashed(objectType) {
if !i.IsDir() {
// ignore files in intermediate directories
continue
}
subpath := filepath.Join(path, i.Name())
var subitems []os.DirEntry
subitems, err = os.ReadDir(subpath)
if err != nil {
h.fileAccessError(w, err)
return
}
for _, f := range subitems {
fi, err := f.Info()
if err != nil {
h.fileAccessError(w, err)
return
}
blobs = append(blobs, Blob{Name: f.Name(), Size: fi.Size()})
}
} else {
fi, err := i.Info()
if err != nil {
h.fileAccessError(w, err)
return
}
blobs = append(blobs, Blob{Name: i.Name(), Size: fi.Size()})
}
}
data, err := json.Marshal(blobs)
if err != nil {
h.internalServerError(w, err)
return
}
w.Header().Set("Content-Type", mimeTypeAPIV2)
_, _ = w.Write(data)
}
// checkBlob tests whether a blob exists.
func (h *Handler) checkBlob(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("checkBlob()")
}
objectType, objectID := h.getObject(r.URL.Path)
if objectType == "" || objectID == "" {
h.internalServerError(w, fmt.Errorf(
"cannot determine object type or id: %s", r.URL.Path))
return
}
path := h.getObjectPath(objectType, objectID)
st, err := os.Stat(path)
if err != nil {
h.fileAccessError(w, err)
return
}
w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}
// getBlob retrieves a blob from the repository.
func (h *Handler) getBlob(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("getBlob()")
}
objectType, objectID := h.getObject(r.URL.Path)
if objectType == "" || objectID == "" {
h.internalServerError(w, fmt.Errorf(
"cannot determine object type or id: %s", r.URL.Path))
return
}
path := h.getObjectPath(objectType, objectID)
file, err := os.Open(path)
if err != nil {
h.fileAccessError(w, err)
return
}
wc := datacounter.NewResponseWriterCounter(w)
http.ServeContent(wc, r, "", time.Unix(0, 0), file)
if err = file.Close(); err != nil {
h.internalServerError(w, err)
return
}
h.sendMetric(objectType, BlobRead, wc.Count())
}
// saveBlob saves a blob to the repository.
func (h *Handler) saveBlob(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("saveBlob()")
}
objectType, objectID := h.getObject(r.URL.Path)
if objectType == "" || objectID == "" {
h.internalServerError(w, fmt.Errorf(
"cannot determine object type or id: %s", r.URL.Path))
return
}
path := h.getObjectPath(objectType, objectID)
_, err := os.Stat(path)
if err == nil {
httpDefaultError(w, http.StatusForbidden)
return
}
if !os.IsNotExist(err) {
h.internalServerError(w, err)
return
}
tmpFn := filepath.Join(filepath.Dir(path), objectID+".rest-server-temp")
tf, err := tempFile(tmpFn, h.opt.fileMode)
if os.IsNotExist(err) {
// the error is caused by a missing directory, create it and retry
mkdirErr := os.MkdirAll(filepath.Dir(path), h.opt.dirMode)
if mkdirErr != nil {
log.Print(mkdirErr)
} else {
// try again
tf, err = tempFile(tmpFn, h.opt.fileMode)
}
}
if err != nil {
h.internalServerError(w, err)
return
}
// ensure this blob does not put us over the quota size limit (if there is one)
outFile, errCode, err := h.wrapFileWriter(r, tf)
if err != nil {
if h.opt.Debug {
log.Println(err)
}
httpDefaultError(w, errCode)
return
}
var written int64
if h.opt.NoVerifyUpload {
// just write the file without checking the contents
written, err = io.Copy(outFile, r.Body)
} else {
// calculate hash for current request
hasher := sha256.New()
written, err = io.Copy(outFile, io.TeeReader(r.Body, hasher))
// reject if file content doesn't match file name
if err == nil && hex.EncodeToString(hasher.Sum(nil)) != objectID {
err = errFileContentDoesntMatchHash
}
}
if err != nil {
_ = tf.Close()
_ = os.Remove(tf.Name())
h.incrementRepoSpaceUsage(-written)
if h.opt.Debug {
log.Print(err)
}
var pathError *os.PathError
if errors.As(err, &pathError) && (pathError.Err == syscall.ENOSPC ||
pathError.Err == syscall.EDQUOT) {
// The error is disk-related (no space left, no quota left),
// notify the client using the correct HTTP status
httpDefaultError(w, http.StatusInsufficientStorage)
} else if errors.Is(err, errFileContentDoesntMatchHash) ||
errors.Is(err, io.ErrUnexpectedEOF) ||
errors.Is(err, http.ErrMissingBoundary) ||
errors.Is(err, http.ErrNotMultipart) {
// The error is connection-related, send a client-side HTTP status
httpDefaultError(w, http.StatusBadRequest)
} else {
// Otherwise we have a different internal error, reply with
// server-side HTTP status
h.internalServerError(w, err)
}
return
}
syncNotSup, err := syncFile(tf)
if err != nil {
_ = tf.Close()
_ = os.Remove(tf.Name())
h.incrementRepoSpaceUsage(-written)
h.internalServerError(w, err)
return
}
if err := tf.Close(); err != nil {
_ = os.Remove(tf.Name())
h.incrementRepoSpaceUsage(-written)
h.internalServerError(w, err)
return
}
if err := os.Rename(tf.Name(), path); err != nil {
_ = os.Remove(tf.Name())
h.incrementRepoSpaceUsage(-written)
h.internalServerError(w, err)
return
}
if syncNotSup {
h.opt.FsyncWarning.Do(func() {
log.Print("WARNING: fsync is not supported by the data storage. This can lead to data loss, if the system crashes or the storage is unexpectedly disconnected.")
})
} else {
if err := syncDir(filepath.Dir(path)); err != nil {
// Don't call os.Remove(path) as this is prone to race conditions with parallel upload retries
h.internalServerError(w, err)
return
}
}
h.sendMetric(objectType, BlobWrite, uint64(written))
}
// tempFile implements a custom version of os.CreateTemp which allows modifying the file permissions
func tempFile(fn string, perm os.FileMode) (f *os.File, err error) {
for i := 0; i < 10; i++ {
name := fn + strconv.FormatInt(rand.Int63(), 10)
f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
if os.IsExist(err) {
continue
}
break
}
return
}
func syncFile(f *os.File) (bool, error) {
err := f.Sync()
// Ignore error if filesystem does not support fsync.
syncNotSup := err != nil && (errors.Is(err, syscall.ENOTSUP) || isMacENOTTY(err))
if syncNotSup {
err = nil
}
return syncNotSup, err
}
func syncDir(dirname string) error {
if runtime.GOOS == "windows" {
// syncing a directory is not possible on windows
return nil
}
dir, err := os.Open(dirname)
if err != nil {
return err
}
err = dir.Sync()
// Ignore error if filesystem does not support fsync.
if errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.EINVAL) {
err = nil
}
if err != nil {
_ = dir.Close()
return err
}
return dir.Close()
}
// deleteBlob deletes a blob from the repository.
func (h *Handler) deleteBlob(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("deleteBlob()")
}
objectType, objectID := h.getObject(r.URL.Path)
if objectType == "" || objectID == "" {
h.internalServerError(w, fmt.Errorf(
"cannot determine object type or id: %s", r.URL.Path))
return
}
if h.opt.AppendOnly && objectType != "locks" {
httpDefaultError(w, http.StatusForbidden)
return
}
path := h.getObjectPath(objectType, objectID)
var size int64
if h.needSize() {
stat, err := os.Stat(path)
if err == nil {
size = stat.Size()
}
}
if err := os.Remove(path); err != nil {
// ignore not exist errors to make deleting idempotent, which is
// necessary to properly handle request retries
if !errors.Is(err, os.ErrNotExist) {
h.fileAccessError(w, err)
}
return
}
h.incrementRepoSpaceUsage(-size)
h.sendMetric(objectType, BlobDelete, uint64(size))
}
// createRepo creates repository directories.
func (h *Handler) createRepo(w http.ResponseWriter, r *http.Request) {
if h.opt.Debug {
log.Println("createRepo()")
}
if r.URL.Query().Get("create") != "true" {
httpDefaultError(w, http.StatusBadRequest)
return
}
log.Printf("Creating repository directories in %s\n", h.path)
if err := os.MkdirAll(h.path, h.opt.dirMode); err != nil {
h.internalServerError(w, err)
return
}
for _, d := range ObjectTypes {
if err := os.Mkdir(filepath.Join(h.path, d), h.opt.dirMode); err != nil && !os.IsExist(err) {
h.internalServerError(w, err)
return
}
}
for i := 0; i < 256; i++ {
dirPath := filepath.Join(h.path, "data", fmt.Sprintf("%02x", i))
if err := os.Mkdir(dirPath, h.opt.dirMode); err != nil && !os.IsExist(err) {
h.internalServerError(w, err)
return
}
}
}
// internalServerError is called to report an internal server error.
// The error message will be reported in the server logs. If PanicOnError
// is set, this will panic instead, which makes debugging easier.
func (h *Handler) internalServerError(w http.ResponseWriter, err error) {
log.Printf("ERROR: %v", err)
if h.opt.PanicOnError {
panic(fmt.Sprintf("internal server error: %v", err))
}
httpDefaultError(w, http.StatusInternalServerError)
}
// internalServerError is called to report an error that occurred while
// accessing a file. If the does not exist, the corresponding http status code
// will be returned to the client. All other errors are passed on to
// internalServerError
func (h *Handler) fileAccessError(w http.ResponseWriter, err error) {
if h.opt.Debug {
log.Print(err)
}
if errors.Is(err, os.ErrNotExist) {
httpDefaultError(w, http.StatusNotFound)
} else {
h.internalServerError(w, err)
}
}

19
repo/repo_unix.go Normal file
View File

@@ -0,0 +1,19 @@
//go:build !windows
// +build !windows
package repo
import (
"errors"
"runtime"
"syscall"
)
// The ExFAT driver on some versions of macOS can return ENOTTY,
// "inappropriate ioctl for device", for fsync.
//
// https://github.com/restic/restic/issues/4016
// https://github.com/realm/realm-core/issues/5789
func isMacENOTTY(err error) bool {
return runtime.GOOS == "darwin" && errors.Is(err, syscall.ENOTTY)
}

4
repo/repo_windows.go Normal file
View File

@@ -0,0 +1,4 @@
package repo
// Windows is not macOS.
func isMacENOTTY(err error) bool { return false }

View File

@@ -1,18 +0,0 @@
language: go
sudo: false
matrix:
include:
- go: 1.4
- go: 1.5
- go: 1.6
- go: 1.7
- go: tip
allow_failures:
- go: tip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...

View File

@@ -1,22 +0,0 @@
Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,55 +0,0 @@
gorilla/handlers
================
[![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers) [![Build Status](https://travis-ci.org/gorilla/handlers.svg?branch=master)](https://travis-ci.org/gorilla/handlers)
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/handlers/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/handlers?badge)
Package handlers is a collection of handlers (aka "HTTP middleware") for use
with Go's `net/http` package (or any framework supporting `http.Handler`), including:
* [**LoggingHandler**](https://godoc.org/github.com/gorilla/handlers#LoggingHandler) for logging HTTP requests in the Apache [Common Log
Format](http://httpd.apache.org/docs/2.2/logs.html#common).
* [**CombinedLoggingHandler**](https://godoc.org/github.com/gorilla/handlers#CombinedLoggingHandler) for logging HTTP requests in the Apache [Combined Log
Format](http://httpd.apache.org/docs/2.2/logs.html#combined) commonly used by
both Apache and nginx.
* [**CompressHandler**](https://godoc.org/github.com/gorilla/handlers#CompressHandler) for gzipping responses.
* [**ContentTypeHandler**](https://godoc.org/github.com/gorilla/handlers#ContentTypeHandler) for validating requests against a list of accepted
content types.
* [**MethodHandler**](https://godoc.org/github.com/gorilla/handlers#MethodHandler) for matching HTTP methods against handlers in a
`map[string]http.Handler`
* [**ProxyHeaders**](https://godoc.org/github.com/gorilla/handlers#ProxyHeaders) for populating `r.RemoteAddr` and `r.URL.Scheme` based on the
`X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and RFC7239 `Forwarded`
headers when running a Go server behind a HTTP reverse proxy.
* [**CanonicalHost**](https://godoc.org/github.com/gorilla/handlers#CanonicalHost) for re-directing to the preferred host when handling multiple
domains (i.e. multiple CNAME aliases).
* [**RecoveryHandler**](https://godoc.org/github.com/gorilla/handlers#RecoveryHandler) for recovering from unexpected panics.
Other handlers are documented [on the Gorilla
website](http://www.gorillatoolkit.org/pkg/handlers).
## Example
A simple example using `handlers.LoggingHandler` and `handlers.CompressHandler`:
```go
import (
"net/http"
"github.com/gorilla/handlers"
)
func main() {
r := http.NewServeMux()
// Only log requests to our admin dashboard to stdout
r.Handle("/admin", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(ShowAdminDashboard)))
r.HandleFunc("/", ShowIndex)
// Wrap our server with our gzip handler to gzip compress all responses.
http.ListenAndServe(":8000", handlers.CompressHandler(r))
}
```
## License
BSD licensed. See the included LICENSE file for details.

View File

@@ -1,74 +0,0 @@
package handlers
import (
"net/http"
"net/url"
"strings"
)
type canonical struct {
h http.Handler
domain string
code int
}
// CanonicalHost is HTTP middleware that re-directs requests to the canonical
// domain. It accepts a domain and a status code (e.g. 301 or 302) and
// re-directs clients to this domain. The existing request path is maintained.
//
// Note: If the provided domain is considered invalid by url.Parse or otherwise
// returns an empty scheme or host, clients are not re-directed.
//
// Example:
//
// r := mux.NewRouter()
// canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302)
// r.HandleFunc("/route", YourHandler)
//
// log.Fatal(http.ListenAndServe(":7000", canonical(r)))
//
func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {
fn := func(h http.Handler) http.Handler {
return canonical{h, domain, code}
}
return fn
}
func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dest, err := url.Parse(c.domain)
if err != nil {
// Call the next handler if the provided domain fails to parse.
c.h.ServeHTTP(w, r)
return
}
if dest.Scheme == "" || dest.Host == "" {
// Call the next handler if the scheme or host are empty.
// Note that url.Parse won't fail on in this case.
c.h.ServeHTTP(w, r)
return
}
if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
// Re-build the destination URL
dest := dest.Scheme + "://" + dest.Host + r.URL.Path
if r.URL.RawQuery != "" {
dest += "?" + r.URL.RawQuery
}
http.Redirect(w, r, dest, c.code)
return
}
c.h.ServeHTTP(w, r)
}
// cleanHost cleans invalid Host headers by stripping anything after '/' or ' '.
// This is backported from Go 1.5 (in response to issue #11206) and attempts to
// mitigate malformed Host headers that do not match the format in RFC7230.
func cleanHost(in string) string {
if i := strings.IndexAny(in, " /"); i != -1 {
return in[:i]
}
return in
}

Some files were not shown because too many files have changed in this diff Show More