mirror of
https://github.com/restic/rest-server.git
synced 2025-12-07 09:36:13 -08:00
Compare commits
443 Commits
v0.9.5
...
2f31e10ceb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f31e10ceb | ||
|
|
ad130de021 | ||
|
|
2aaa048aba | ||
|
|
b6ec6f45cc | ||
|
|
2a77536ce5 | ||
|
|
0adcfa2619 | ||
|
|
9f8bb0c87c | ||
|
|
5faeedf050 | ||
|
|
7294612990 | ||
|
|
25066228ee | ||
|
|
72a7319fae | ||
|
|
df5330773f | ||
|
|
2bb4d251e2 | ||
|
|
f018e99109 | ||
|
|
95538fe956 | ||
|
|
4e6193ceee | ||
|
|
4c368ae1fb | ||
|
|
0ed9de379e | ||
|
|
451c4831f9 | ||
|
|
1610cf6cef | ||
|
|
3d35116b3c | ||
|
|
eee73d3bc1 | ||
|
|
df7b13e18f | ||
|
|
2d3e02017b | ||
|
|
2b6f0b39fc | ||
|
|
dbf5253ac2 | ||
|
|
fa15677855 | ||
|
|
19aa0845c0 | ||
|
|
04b52b0cee | ||
|
|
f2d406ff2e | ||
|
|
8ad7cfa60a | ||
|
|
4f17744d6c | ||
|
|
68ae5d1c0b | ||
|
|
0dfc772cdb | ||
|
|
b0a9a0452e | ||
|
|
f053e33486 | ||
|
|
10a06dcbf1 | ||
|
|
b05b44cb2c | ||
|
|
a976a2145b | ||
|
|
751d2841f3 | ||
|
|
5938f9aacd | ||
|
|
376392a89c | ||
|
|
13f461740d | ||
|
|
e9e6529345 | ||
|
|
a0110bb902 | ||
|
|
82c5a314f9 | ||
|
|
9fc5066fc4 | ||
|
|
3e0737a8bd | ||
|
|
9195526406 | ||
|
|
2513a698f3 | ||
|
|
bdfb047edf | ||
|
|
e35c6e39d9 | ||
|
|
da5bb66030 | ||
|
|
664d997006 | ||
|
|
5d7b581db6 | ||
|
|
37b4327012 | ||
|
|
238eceb2a1 | ||
|
|
9fb75a71cb | ||
|
|
38f29da143 | ||
|
|
eb9f8cfa1f | ||
|
|
e8a9fbc88f | ||
|
|
7b2639b21e | ||
|
|
40fd34e7c8 | ||
|
|
a6323b5e98 | ||
|
|
318f0c6a89 | ||
|
|
add9b066f9 | ||
|
|
e35f649f07 | ||
|
|
e309a92895 | ||
|
|
848e75c753 | ||
|
|
d3fedbefe5 | ||
|
|
d8cbfd1f63 | ||
|
|
154efcee43 | ||
|
|
9d5b956858 | ||
|
|
705c83a714 | ||
|
|
421c542e31 | ||
|
|
c510ac2e41 | ||
|
|
fed9c4b1e3 | ||
|
|
a2b39539a3 | ||
|
|
16262def42 | ||
|
|
0c735187ba | ||
|
|
b4430d3607 | ||
|
|
e0674c6150 | ||
|
|
55f43b815c | ||
|
|
b3522ae2ac | ||
|
|
6faaaa713e | ||
|
|
a4f2823515 | ||
|
|
13083acc1a | ||
|
|
ce15402f74 | ||
|
|
b28bea1de4 | ||
|
|
4bfb2eaa81 | ||
|
|
8becd574cb | ||
|
|
03ad2420db | ||
|
|
3ce6aaf2b6 | ||
|
|
e388fcddde | ||
|
|
2d456efbe8 | ||
|
|
14f70457cf | ||
|
|
29753193ac | ||
|
|
e2d5251cbf | ||
|
|
f36b6504fc | ||
|
|
04418c721f | ||
|
|
c615b9dbdd | ||
|
|
292556a8bc | ||
|
|
ef8998a1cc | ||
|
|
0c905fd64c | ||
|
|
45f71173c8 | ||
|
|
ce63a80646 | ||
|
|
6f91a8638d | ||
|
|
59d9e20cbd | ||
|
|
6aa3c966b6 | ||
|
|
228082e5ea | ||
|
|
8523f0c968 | ||
|
|
74b74ed325 | ||
|
|
2dbad90967 | ||
|
|
04eacee642 | ||
|
|
1b1b127490 | ||
|
|
f8669a2eba | ||
|
|
fb5d49c631 | ||
|
|
e6efaaf65f | ||
|
|
8b90047951 | ||
|
|
29fea36169 | ||
|
|
0bb8cd41d1 | ||
|
|
ec2ce8cd27 | ||
|
|
c38e18b708 | ||
|
|
76759aa52e | ||
|
|
4231ef6f20 | ||
|
|
5b288d488a | ||
|
|
46e3c7248b | ||
|
|
fdf65f66e7 | ||
|
|
32ab845151 | ||
|
|
4bf4b62379 | ||
|
|
bb99f5a426 | ||
|
|
38b712c714 | ||
|
|
5f3faad48d | ||
|
|
d6a268ca01 | ||
|
|
d890dbad69 | ||
|
|
3130a4bcdf | ||
|
|
ff81311a98 | ||
|
|
9557efad55 | ||
|
|
84a8b210f5 | ||
|
|
ab45fb59ff | ||
|
|
3284c4ab73 | ||
|
|
0cd077f4ab | ||
|
|
64ab92761c | ||
|
|
8ec316cea3 | ||
|
|
420c4c6683 | ||
|
|
be14687a9c | ||
|
|
dbb2d4690c | ||
|
|
0a09c8d633 | ||
|
|
dedcb846df | ||
|
|
781d2241e0 | ||
|
|
b2e8044fbd | ||
|
|
30ec84fcb7 | ||
|
|
2a3bca1633 | ||
|
|
aeb5e2982f | ||
|
|
20c4cdedfc | ||
|
|
c064e4c1ed | ||
|
|
66fe4afb7d | ||
|
|
4576e1bc12 | ||
|
|
e8a839673f | ||
|
|
b34b9f0780 | ||
|
|
543d313f7e | ||
|
|
a98ed25c7b | ||
|
|
efe070c66d | ||
|
|
834a3378e9 | ||
|
|
9f074d8b3a | ||
|
|
94d5861c50 | ||
|
|
253bebb096 | ||
|
|
337035c414 | ||
|
|
399f7f1d49 | ||
|
|
afcdb2f312 | ||
|
|
f97c48d92e | ||
|
|
da9ce53a95 | ||
|
|
d795b70b9f | ||
|
|
3a23555594 | ||
|
|
22a6412b81 | ||
|
|
11c5a548e8 | ||
|
|
c3d3bfbc12 | ||
|
|
f2f2d29064 | ||
|
|
dd0f03c9bb | ||
|
|
ea461a2d76 | ||
|
|
40ad68bc5f | ||
|
|
8f3b92325f | ||
|
|
fb6a3a62cf | ||
|
|
667ce6e26b | ||
|
|
091e70d903 | ||
|
|
f5c06a2e45 | ||
|
|
40e2a8b1e4 | ||
|
|
fa795cc66a | ||
|
|
99ec5e2dbb | ||
|
|
f299c735df | ||
|
|
8ce88b24e7 | ||
|
|
2eae8c9266 | ||
|
|
30fbd043b9 | ||
|
|
7f29dcbd69 | ||
|
|
43c96fb6f2 | ||
|
|
c1c48c62e9 | ||
|
|
a8cd3f218d | ||
|
|
80babf98e7 | ||
|
|
408dcab92e | ||
|
|
2dd87ced0a | ||
|
|
f763db8934 | ||
|
|
a8fdca3b9f | ||
|
|
b562edefd1 | ||
|
|
cb2afaa4c0 | ||
|
|
991c459be3 | ||
|
|
9f4fba2c21 | ||
|
|
65fd8be3f8 | ||
|
|
274f29fee8 | ||
|
|
98f0aaca1c | ||
|
|
5a6ed2ffdf | ||
|
|
46b020fd9e | ||
|
|
df9eb337d3 | ||
|
|
1eeca53812 | ||
|
|
0bdc420e75 | ||
|
|
b0036d006b | ||
|
|
6bc87b8e95 | ||
|
|
d24ffc13d8 | ||
|
|
a87a50ad11 | ||
|
|
8538ce7859 | ||
|
|
87cef8f159 | ||
|
|
bc1545c717 | ||
|
|
3903ed000c | ||
|
|
cb85fb38c0 | ||
|
|
1fd9538f73 | ||
|
|
af36b77ece | ||
|
|
48067dc896 | ||
|
|
096ac5a9c8 | ||
|
|
4741eec619 | ||
|
|
057ef39525 | ||
|
|
b739e22b04 | ||
|
|
e9900b7a00 | ||
|
|
959250f543 | ||
|
|
9e4442805e | ||
|
|
228d5f6051 | ||
|
|
6f386876e7 | ||
|
|
7a3b9e4000 | ||
|
|
aaf4f4b92a | ||
|
|
bf42a509ab | ||
|
|
a7eecf40bd | ||
|
|
421da62900 | ||
|
|
12cf4bbc05 | ||
|
|
51ab8e98e2 | ||
|
|
9f8c31b968 | ||
|
|
f952bc7344 | ||
|
|
223520b964 | ||
|
|
1172d7e068 | ||
|
|
fb5d63435a | ||
|
|
8729a699a1 | ||
|
|
f373e45bc8 | ||
|
|
8642729a51 | ||
|
|
f61292b00d | ||
|
|
a09ba203bd | ||
|
|
ac8e95c8a4 | ||
|
|
f8db131226 | ||
|
|
101ad07999 | ||
|
|
dbf002296b | ||
|
|
5be12cecbf | ||
|
|
9b31f17188 | ||
|
|
173bfb5371 | ||
|
|
20edfb87ee | ||
|
|
d2813ea61b | ||
|
|
f90205eefe | ||
|
|
32784a3072 | ||
|
|
05773795dd | ||
|
|
0bd1f612d2 | ||
|
|
a5b306e65b | ||
|
|
64a43228de | ||
|
|
28f569c0df | ||
|
|
04d206303c | ||
|
|
e6cc79a2ec | ||
|
|
7fe16b69b2 | ||
|
|
ec0766cddd | ||
|
|
2175029c9e | ||
|
|
82816c67e1 | ||
|
|
4db46a5d3d | ||
|
|
39839cfac4 | ||
|
|
5e71f61ae8 | ||
|
|
4c8a076976 | ||
|
|
16889717c6 | ||
|
|
54adcb1fc7 | ||
|
|
96a6f0a5c4 | ||
|
|
28c0b95b8a | ||
|
|
d39bc8e6cf | ||
|
|
034302de95 | ||
|
|
ba9ee5c625 | ||
|
|
4e36854cd4 | ||
|
|
e3b1c5d612 | ||
|
|
aef955c513 | ||
|
|
ff88e6812d | ||
|
|
9db2d52fbe | ||
|
|
32c138aa84 | ||
|
|
73a6000f10 | ||
|
|
e957e42336 | ||
|
|
c2958906ea | ||
|
|
63c8797ba3 | ||
|
|
d4cd47e503 | ||
|
|
1f593fafaf | ||
|
|
55e549e92c | ||
|
|
79a8785e26 | ||
|
|
7f14414363 | ||
|
|
bcbfff7b62 | ||
|
|
c36ae5fe03 | ||
|
|
2bf01df6bf | ||
|
|
d1e56e80ee | ||
|
|
4967dcbf74 | ||
|
|
8a1535ba0c | ||
|
|
1ca9ca7e50 | ||
|
|
766f1e0c00 | ||
|
|
cec241e5e1 | ||
|
|
a44c025cd3 | ||
|
|
73fb6419ff | ||
|
|
05a5d1f94e | ||
|
|
3a4d901f6d | ||
|
|
10dc7a4a86 | ||
|
|
e3aad65559 | ||
|
|
980bff189e | ||
|
|
a659f3d879 | ||
|
|
83e78c6cd7 | ||
|
|
0a6e0dbdf9 | ||
|
|
4c5077769c | ||
|
|
33c41b55bb | ||
|
|
ba581f22ed | ||
|
|
0155ba8a1b | ||
|
|
037fe06973 | ||
|
|
a994d347ca | ||
|
|
14bbb9c18a | ||
|
|
53576a1454 | ||
|
|
4b8ef2cbd7 | ||
|
|
65fb54cbca | ||
|
|
9313f19441 | ||
|
|
d4b929ef35 | ||
|
|
13f56bbb3c | ||
|
|
ecfa514256 | ||
|
|
c7a44dd1a2 | ||
|
|
4171164a39 | ||
|
|
fa516da2c4 | ||
|
|
6e44dd8eae | ||
|
|
20603b1622 | ||
|
|
1488830de1 | ||
|
|
723f29e594 | ||
|
|
f8e774393c | ||
|
|
6367043b2c | ||
|
|
6e44ec0763 | ||
|
|
06f8484400 | ||
|
|
1629c824c9 | ||
|
|
fd635e3965 | ||
|
|
8300e75c77 | ||
|
|
f9fcc40305 | ||
|
|
fcf9220630 | ||
|
|
c74c36e175 | ||
|
|
b7b5d32538 | ||
|
|
3fcbbc7b65 | ||
|
|
27264c0a7a | ||
|
|
c69d473fa5 | ||
|
|
687804a02b | ||
|
|
59afaed1a6 | ||
|
|
9ae066589d | ||
|
|
46fd57c36e | ||
|
|
0cfe4320c0 | ||
|
|
f3408b3e46 | ||
|
|
d9757b2022 | ||
|
|
35e3a30949 | ||
|
|
947374fbfb | ||
|
|
13cae78c8f | ||
|
|
a48d6947d9 | ||
|
|
9a62754e15 | ||
|
|
527c7ab1c8 | ||
|
|
6ebedcc0b2 | ||
|
|
f18a5c16be | ||
|
|
a87d968870 | ||
|
|
6f412e6a8a | ||
|
|
420b1d3ee8 | ||
|
|
9cda1814b6 | ||
|
|
df3b6aa1cf | ||
|
|
b98c171644 | ||
|
|
7dd5483ea3 | ||
|
|
0f4f747b74 | ||
|
|
0f72176ddd | ||
|
|
8dad5a5f41 | ||
|
|
899ef655ef | ||
|
|
7126dfec79 | ||
|
|
9107b94367 | ||
|
|
9d6316bd6e | ||
|
|
897d5a026c | ||
|
|
4d2493388a | ||
|
|
02196a18d8 | ||
|
|
cbafb98113 | ||
|
|
a6961e877b | ||
|
|
ec7289235c | ||
|
|
698b6331b9 | ||
|
|
69ed06aa66 | ||
|
|
cda126a99c | ||
|
|
4019e3f45e | ||
|
|
ef5733293f | ||
|
|
6f6f570b31 | ||
|
|
496f8ea1f6 | ||
|
|
dfe9755ed0 | ||
|
|
2209f1437e | ||
|
|
c3b57a177c | ||
|
|
668c992035 | ||
|
|
16a4d01ac1 | ||
|
|
36012fd7b3 | ||
|
|
facfd2437e | ||
|
|
bdaa1ae345 | ||
|
|
733c8da8fc | ||
|
|
03958beb57 | ||
|
|
aee26a5045 | ||
|
|
f25bf989dc | ||
|
|
05c6a03ad7 | ||
|
|
bf34e9d62d | ||
|
|
cd4d054887 | ||
|
|
55134ae37a | ||
|
|
b91d38076a | ||
|
|
97835b4cfd | ||
|
|
0f85243f5a | ||
|
|
75578acd66 | ||
|
|
6c846f856c | ||
|
|
f99197dcf9 | ||
|
|
c48c660678 | ||
|
|
9ef84dcdea | ||
|
|
0a5606e954 | ||
|
|
5a2c70c9e1 | ||
|
|
cbe5cf5c74 | ||
|
|
cd62c2bceb | ||
|
|
f02ee7386a | ||
|
|
b786c5d1cc | ||
|
|
1bc1698f4e | ||
|
|
8d8ecd7b0e | ||
|
|
213ff91b05 | ||
|
|
d1504d7d66 | ||
|
|
14f8cd5bca | ||
|
|
791bb572f8 | ||
|
|
d056b85432 | ||
|
|
10951e4540 | ||
|
|
0f9ea68a2e | ||
|
|
9b89df0842 | ||
|
|
b213d2a274 | ||
|
|
ff6270ab62 | ||
|
|
4cd82b6802 | ||
|
|
ca0e09261f | ||
|
|
526a2b3837 | ||
|
|
67a0f63773 | ||
|
|
3e4edd3dd8 |
85
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal file
85
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal 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
57
.github/ISSUE_TEMPLATE/FEATURE.md
vendored
Normal 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
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
41
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
13
.github/dependabot.yml
vendored
Normal 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
119
.github/workflows/tests.yml
vendored
Normal 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) }}
|
||||||
56
.golangci.yml
Normal file
56
.golangci.yml
Normal 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
244
.goreleaser.yml
Normal 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"
|
||||||
36
.travis.yml
36
.travis.yml
@@ -1,36 +0,0 @@
|
|||||||
os: linux
|
|
||||||
sudo: false
|
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.7.x
|
|
||||||
- 1.8.x
|
|
||||||
- 1.9.x
|
|
||||||
- master
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- go: master
|
|
||||||
|
|
||||||
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
16
AUTHORS
Normal 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
525
CHANGELOG.md
Normal 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
|
||||||
|
|
||||||
|
|
||||||
21
Dockerfile
21
Dockerfile
@@ -1,16 +1,27 @@
|
|||||||
FROM alpine:3.6
|
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 DATA_DIRECTORY /data
|
||||||
ENV PASSWORD_FILE /data/.htpasswd
|
ENV PASSWORD_FILE /data/.htpasswd
|
||||||
|
|
||||||
RUN apk add --no-cache --update apache2-utils
|
RUN apk add --no-cache --update apache2-utils
|
||||||
|
|
||||||
COPY rest-server docker/*_user /usr/bin/
|
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
|
VOLUME /data
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
|
||||||
|
|
||||||
CMD [ "/entrypoint.sh" ]
|
CMD [ "/entrypoint.sh" ]
|
||||||
|
|||||||
16
Dockerfile.goreleaser
Normal file
16
Dockerfile.goreleaser
Normal 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" ]
|
||||||
41
Gopkg.lock
generated
41
Gopkg.lock
generated
@@ -1,41 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/gorilla/handlers"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "a4043c62cc2329bacda331d33fc908ab11ef0ec3"
|
|
||||||
source = "https://github.com/gorilla/handlers"
|
|
||||||
version = "v1.2.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/inconshreveable/mousetrap"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
|
||||||
version = "v1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/cobra"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/spf13/pflag"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "goji.io"
|
|
||||||
packages = [".","internal","middleware","pat","pattern"]
|
|
||||||
revision = "0d89ff54b2c18c9c4ba530e32496aef902d3c6cd"
|
|
||||||
source = "https://github.com/goji/goji"
|
|
||||||
version = "v2.0"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
inputs-digest = "ded9eeab22bffb0bbc4513634907b9399f964d8de2276e18e00363245f980c55"
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[[constraint]]
|
|
||||||
name = "github.com/gorilla/handlers"
|
|
||||||
source = "https://github.com/gorilla/handlers"
|
|
||||||
version = "1.2.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "goji.io"
|
|
||||||
source = "https://github.com/goji/goji"
|
|
||||||
version = "2.0.0"
|
|
||||||
1
LICENSE
1
LICENSE
@@ -2,6 +2,7 @@ The BSD 2-Clause License
|
|||||||
|
|
||||||
Copyright © 2015, Bertil Chapuis
|
Copyright © 2015, Bertil Chapuis
|
||||||
Copyright © 2016, Zlatko Čalušić, Alexander Neumann
|
Copyright © 2016, Zlatko Čalušić, Alexander Neumann
|
||||||
|
Copyright © 2017, The Rest Server Authors
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|||||||
35
Makefile
35
Makefile
@@ -1,35 +0,0 @@
|
|||||||
# Copyright © 2017 Zlatko Čalušić
|
|
||||||
#
|
|
||||||
# Use of this source code is governed by an MIT-style license that can be found in the LICENSE file.
|
|
||||||
#
|
|
||||||
|
|
||||||
DOCKER_IMAGE ?= restic/rest-server
|
|
||||||
|
|
||||||
.PHONY: default rest-server install uninstall docker_build docker_push clean
|
|
||||||
|
|
||||||
default: rest-server
|
|
||||||
|
|
||||||
rest-server:
|
|
||||||
@go run build.go
|
|
||||||
|
|
||||||
install: rest-server
|
|
||||||
sudo /usr/bin/install -m 755 rest-server /usr/local/bin/rest-server
|
|
||||||
|
|
||||||
uninstall:
|
|
||||||
sudo rm -f /usr/local/bin/rest-server
|
|
||||||
|
|
||||||
docker_build:
|
|
||||||
docker pull golang:1.9.1-alpine
|
|
||||||
docker run --rm -it \
|
|
||||||
-v $(CURDIR):/go/src/github.com/restic/rest-server \
|
|
||||||
-w /go/src/github.com/restic/rest-server \
|
|
||||||
golang:1.9.1-alpine \
|
|
||||||
go run build.go
|
|
||||||
docker pull alpine:3.6
|
|
||||||
docker build -t $(DOCKER_IMAGE) .
|
|
||||||
|
|
||||||
docker_push:
|
|
||||||
docker push $(DOCKER_IMAGE):latest
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f rest-server
|
|
||||||
205
README.md
205
README.md
@@ -1,63 +1,30 @@
|
|||||||
# Rest Server
|
# Rest Server
|
||||||
|
|
||||||
[](https://travis-ci.org/restic/rest-server)
|
|
||||||
|
[](https://github.com/restic/rest-server/actions?query=workflow%3Atest)
|
||||||
[](https://goreportcard.com/report/github.com/restic/rest-server)
|
[](https://goreportcard.com/report/github.com/restic/rest-server)
|
||||||
[](https://godoc.org/github.com/restic/rest-server)
|
[](https://godoc.org/github.com/restic/rest-server)
|
||||||
[](https://github.com/restic/rest-server/blob/master/LICENSE)
|
[](https://github.com/restic/rest-server/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
|
|
||||||
Rest Server is a high performance HTTP server that implements restic's [REST backend API](http://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](http://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server).
|
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
|
## 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.7.1](https://github.com/restic/restic/releases/tag/v0.7.1) or higher.
|
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.
|
||||||
|
|
||||||
If you have a local repository created with an older version of restic client, which you would now like to serve via Rest Server, you need to first create missing subdirectories in the data directory. Run this simple one-liner in the repository directory:
|
## Build
|
||||||
|
|
||||||
```for i in {0..255}; do mkdir -p $(printf "data/%02x" $i); done```
|
For building the `rest-server` binary run `CGO_ENABLED=0 go build -o rest-server ./cmd/rest-server`
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### From source
|
|
||||||
|
|
||||||
#### Build
|
|
||||||
|
|
||||||
```make```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```go run build.go```
|
|
||||||
|
|
||||||
If all goes well, you'll find the binary in the current directory.
|
|
||||||
|
|
||||||
Alternatively, you can compile and install it in your $GOBIN with a standard `go install ./cmd/rest-server`. But, beware, you won't have version info built into binary when compiled that way!
|
|
||||||
|
|
||||||
#### Install
|
|
||||||
|
|
||||||
```make install```
|
|
||||||
|
|
||||||
Installs the binary as `/usr/local/bin/rest-server`.
|
|
||||||
|
|
||||||
Alternatively, you can install it manually anywhere you want. It's a single binary, there are no dependencies.
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
#### Build image
|
|
||||||
|
|
||||||
```make docker_build```
|
|
||||||
|
|
||||||
#### Pull image
|
|
||||||
|
|
||||||
```docker pull restic/rest-server```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To learn how to use restic backup client with REST backend, please consult [restic manual](http://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server).
|
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
|
Run a REST server for use with restic
|
||||||
|
|
||||||
@@ -65,120 +32,148 @@ Usage:
|
|||||||
rest-server [flags]
|
rest-server [flags]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--append-only enable append only mode
|
--append-only enable append only mode
|
||||||
--cpuprofile string write CPU profile to file
|
--cpu-profile string write CPU profile to file
|
||||||
--debug output debug messages
|
--debug output debug messages
|
||||||
-h, --help help for rest-server
|
--group-accessible-repos let filesystem group be able to access repo files
|
||||||
--listen string listen address (default ":8000")
|
-h, --help help for rest-server
|
||||||
--log string log HTTP requests in the combined log format
|
--htpasswd-file string location of .htpasswd file (default: "<data directory>/.htpasswd)"
|
||||||
--path string data directory (default "/tmp/restic")
|
--listen string listen address (default ":8000")
|
||||||
--tls turn on TLS support
|
--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
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the server persists backup data in `/tmp/restic`. Start the server with a custom persistence directory:
|
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:
|
||||||
|
|
||||||
```
|
```sh
|
||||||
rest-server --path /user/home/backup
|
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.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
htpasswd -s -c .htpasswd username
|
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.
|
||||||
|
|
||||||
```
|
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`.
|
||||||
openssl genrsa -out private_key 2048
|
|
||||||
openssl req -new -x509 -key private_key -out public_key -days 365
|
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"
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
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.
|
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
|
### Systemd
|
||||||
|
|
||||||
There's an example [systemd service file](https://github.com/restic/rest-server/blob/master/etc/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.
|
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
|
### Docker
|
||||||
|
|
||||||
By default, image uses authentication. To turn it off, set environment variable `DISABLE_AUTHENTICATION` to any value.
|
Rest Server works well inside a container, images are [published to Docker Hub](https://hub.docker.com/r/restic/rest-server).
|
||||||
|
|
||||||
Persistent data volume is located to `/data`.
|
|
||||||
|
|
||||||
#### Start server
|
#### Start server
|
||||||
|
|
||||||
```
|
You can run the server with any container runtime, like Docker:
|
||||||
docker run -p 8000:8000 -v /my/data:/data --name rest_server restic/rest-server
|
|
||||||
|
```sh
|
||||||
|
docker pull restic/rest-server:latest
|
||||||
|
docker run -p 8000:8000 -v /my/data:/data --name rest_server restic/rest-server
|
||||||
```
|
```
|
||||||
|
|
||||||
It's suggested to set a container name to more easily manage users (see next section).
|
Note that:
|
||||||
|
|
||||||
You can set environment variable `OPTIONS` to any extra flags you'd like to pass to rest-server.
|
- **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
|
#### Manage users
|
||||||
|
|
||||||
##### Add user
|
##### Add user
|
||||||
|
|
||||||
```
|
```sh
|
||||||
docker exec -it rest_server create_user myuser
|
docker exec -it rest_server create_user myuser
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```
|
```sh
|
||||||
docker exec -it rest_server create_user myuser mypassword
|
docker exec -it rest_server create_user myuser mypassword
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Delete user
|
##### Delete user
|
||||||
|
|
||||||
```
|
```sh
|
||||||
docker exec -it rest_server delete_user myuser
|
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?
|
## 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).
|
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).
|
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.
|
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
|
||||||
|
|
||||||
Contributors are welcome, just open a new issue / pull request.
|
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
43
Release.md
Normal 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
|
||||||
480
build.go
480
build.go
@@ -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
|
package main
|
||||||
|
|
||||||
@@ -6,147 +44,46 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"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 (
|
var (
|
||||||
verbose bool
|
verbose bool
|
||||||
keepGopath bool
|
runTests bool
|
||||||
runTests bool
|
enableCGO bool
|
||||||
enableCGO bool
|
enablePIE bool
|
||||||
|
goVersion = ParseGoVersion(runtime.Version())
|
||||||
)
|
)
|
||||||
|
|
||||||
var config = struct {
|
// die prints the message with fmt.Fprintf() to stderr and exits with an error
|
||||||
Name string
|
// code.
|
||||||
Namespace string
|
func die(message string, args ...interface{}) {
|
||||||
Main string
|
fmt.Fprintf(os.Stderr, message, args...)
|
||||||
Tests []string
|
os.Exit(1)
|
||||||
}{
|
|
||||||
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{"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showUsage(output io.Writer) {
|
func showUsage(output io.Writer) {
|
||||||
@@ -155,12 +92,13 @@ func showUsage(output io.Writer) {
|
|||||||
fmt.Fprintf(output, "OPTIONS:\n")
|
fmt.Fprintf(output, "OPTIONS:\n")
|
||||||
fmt.Fprintf(output, " -v --verbose output more messages\n")
|
fmt.Fprintf(output, " -v --verbose output more messages\n")
|
||||||
fmt.Fprintf(output, " -t --tags specify additional build tags\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, " -T --test run tests\n")
|
||||||
fmt.Fprintf(output, " -o --output set output file name\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-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, " --goos value set GOOS for cross-compilation\n")
|
||||||
fmt.Fprintf(output, " --goarch value set GOARCH 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{}) {
|
func verbosePrintf(message string, args ...interface{}) {
|
||||||
@@ -171,53 +109,74 @@ func verbosePrintf(message string, args ...interface{}) {
|
|||||||
fmt.Printf("build: "+message, args...)
|
fmt.Printf("build: "+message, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanEnv returns a clean environment with GOPATH and GOBIN removed (if present).
|
// printEnv prints Go-relevant environment variables in a nice way using verbosePrintf.
|
||||||
func cleanEnv() (env []string) {
|
func printEnv(env []string) {
|
||||||
for _, v := range os.Environ() {
|
verbosePrintf("environment (GO*):\n")
|
||||||
if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
|
for _, v := range env {
|
||||||
|
// ignore environment variables which do not start with GO*.
|
||||||
|
if !strings.HasPrefix(v, "GO") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
verbosePrintf(" %s\n", v)
|
||||||
env = append(env, v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return env
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build runs "go build args..." with GOPATH set to gopath.
|
// build runs "go build args..." with GOPATH set to gopath.
|
||||||
func build(cwd, goos, goarch, gopath string, args ...string) error {
|
func build(cwd string, env map[string]string, args ...string) error {
|
||||||
a := []string{"build"}
|
// -trimpath removes all absolute paths from the binary.
|
||||||
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
|
a := []string{"build", "-trimpath"}
|
||||||
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
|
|
||||||
|
if enablePIE {
|
||||||
|
a = append(a, "-buildmode=pie")
|
||||||
|
}
|
||||||
|
|
||||||
a = append(a, args...)
|
a = append(a, args...)
|
||||||
cmd := exec.Command("go", a...)
|
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 {
|
if !enableCGO {
|
||||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printEnv(cmd.Env)
|
||||||
|
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
verbosePrintf("go %s\n", args)
|
|
||||||
|
verbosePrintf("chdir %q\n", cwd)
|
||||||
|
verbosePrintf("go %q\n", a)
|
||||||
|
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// test runs "go test args..." with GOPATH set to gopath.
|
// test runs "go test args..." with GOPATH set to gopath.
|
||||||
func test(cwd, gopath string, args ...string) error {
|
func test(cwd string, env map[string]string, args ...string) error {
|
||||||
args = append([]string{"test"}, args...)
|
args = append([]string{"test", "-count", "1"}, args...)
|
||||||
cmd := exec.Command("go", 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.Dir = cwd
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
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()
|
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 {
|
func getVersionFromFile() string {
|
||||||
buf, err := ioutil.ReadFile("VERSION")
|
buf, err := ioutil.ReadFile("VERSION")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -228,8 +187,9 @@ func getVersionFromFile() string {
|
|||||||
return strings.TrimSpace(string(buf))
|
return strings.TrimSpace(string(buf))
|
||||||
}
|
}
|
||||||
|
|
||||||
// getVersion returns a version string which is a combination of the contents of the file VERSION in the current
|
// getVersion returns a version string which is a combination of the contents
|
||||||
// directory and the version from git (if available).
|
// of the file VERSION in the current directory and the version from git (if
|
||||||
|
// available).
|
||||||
func getVersion() string {
|
func getVersion() string {
|
||||||
versionFile := getVersionFromFile()
|
versionFile := getVersionFromFile()
|
||||||
versionGit := getVersionFromGit()
|
versionGit := getVersionFromGit()
|
||||||
@@ -247,7 +207,8 @@ func getVersion() string {
|
|||||||
return fmt.Sprintf("%s (%s)", versionFile, versionGit)
|
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 {
|
func getVersionFromGit() string {
|
||||||
cmd := exec.Command("git", "describe",
|
cmd := exec.Command("git", "describe",
|
||||||
"--long", "--tags", "--dirty", "--always")
|
"--long", "--tags", "--dirty", "--always")
|
||||||
@@ -262,7 +223,8 @@ func getVersionFromGit() string {
|
|||||||
return version
|
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
|
type Constants map[string]string
|
||||||
|
|
||||||
// LDFlags returns the string that can be passed to go build's `-ldflags`.
|
// 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, " ")
|
return strings.Join(l, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
// GoVersion is the version of Go used to compile the project.
|
||||||
log.SetFlags(0)
|
type GoVersion struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
}
|
||||||
|
|
||||||
ver := runtime.Version()
|
// ParseGoVersion parses the Go version s. If s cannot be parsed, the returned GoVersion is null.
|
||||||
if strings.HasPrefix(ver, "go1") && ver < "go1.7" {
|
func ParseGoVersion(s string) (v GoVersion) {
|
||||||
log.Fatalf("Go version %s detected, rest-server requires at least Go 1.7\n", ver)
|
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
|
skipNext := false
|
||||||
params := os.Args[1:]
|
params := os.Args[1:]
|
||||||
|
|
||||||
targetGOOS := runtime.GOOS
|
env := map[string]string{
|
||||||
targetGOARCH := runtime.GOARCH
|
"GO111MODULE": "on", // make sure we build in Module mode
|
||||||
|
"GOOS": runtime.GOOS,
|
||||||
|
"GOARCH": runtime.GOARCH,
|
||||||
|
"GOARM": "",
|
||||||
|
}
|
||||||
|
|
||||||
var outputFilename string
|
var outputFilename string
|
||||||
|
|
||||||
@@ -303,14 +350,12 @@ func main() {
|
|||||||
switch arg {
|
switch arg {
|
||||||
case "-v", "--verbose":
|
case "-v", "--verbose":
|
||||||
verbose = true
|
verbose = true
|
||||||
case "-k", "--keep-gopath":
|
|
||||||
keepGopath = true
|
|
||||||
case "-t", "-tags", "--tags":
|
case "-t", "-tags", "--tags":
|
||||||
if i+1 >= len(params) {
|
if i+1 >= len(params) {
|
||||||
log.Fatal("-t given but no tag specified")
|
die("-t given but no tag specified")
|
||||||
}
|
}
|
||||||
skipNext = true
|
skipNext = true
|
||||||
buildTags = strings.Split(params[i+1], " ")
|
buildTags = append(buildTags, strings.Split(params[i+1], " ")...)
|
||||||
case "-o", "--output":
|
case "-o", "--output":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
outputFilename = params[i+1]
|
outputFilename = params[i+1]
|
||||||
@@ -318,104 +363,103 @@ func main() {
|
|||||||
runTests = true
|
runTests = true
|
||||||
case "--enable-cgo":
|
case "--enable-cgo":
|
||||||
enableCGO = true
|
enableCGO = true
|
||||||
|
case "--enable-pie":
|
||||||
|
enablePIE = true
|
||||||
case "--goos":
|
case "--goos":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
targetGOOS = params[i+1]
|
env["GOOS"] = params[i+1]
|
||||||
case "--goarch":
|
case "--goarch":
|
||||||
skipNext = true
|
skipNext = true
|
||||||
targetGOARCH = params[i+1]
|
env["GOARCH"] = params[i+1]
|
||||||
|
case "--goarm":
|
||||||
|
skipNext = true
|
||||||
|
env["GOARM"] = params[i+1]
|
||||||
case "-h":
|
case "-h":
|
||||||
showUsage(os.Stdout)
|
showUsage(os.Stdout)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
log.Printf("Error: unknown option %q\n\n", arg)
|
fmt.Fprintf(os.Stderr, "Error: unknown option %q\n\n", arg)
|
||||||
showUsage(os.Stderr)
|
showUsage(os.Stderr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(buildTags) == 0 {
|
verbosePrintf("detected Go version %v\n", goVersion)
|
||||||
verbosePrintf("adding build-tag release\n")
|
|
||||||
buildTags = []string{"release"}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
preserveSymbols := false
|
||||||
for i := range buildTags {
|
for i := range buildTags {
|
||||||
buildTags[i] = strings.TrimSpace(buildTags[i])
|
buildTags[i] = strings.TrimSpace(buildTags[i])
|
||||||
|
if buildTags[i] == "debug" || buildTags[i] == "profile" {
|
||||||
|
preserveSymbols = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verbosePrintf("build tags: %s\n", buildTags)
|
verbosePrintf("build tags: %s\n", buildTags)
|
||||||
|
|
||||||
root, err := os.Getwd()
|
root, err := os.Getwd()
|
||||||
if err != nil {
|
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 == "" {
|
if outputFilename == "" {
|
||||||
outputFilename = config.Name
|
outputFilename = config.Name
|
||||||
if targetGOOS == "windows" {
|
if env["GOOS"] == "windows" {
|
||||||
outputFilename += ".exe"
|
outputFilename += ".exe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
output := outputFilename
|
||||||
if err != nil {
|
if !filepath.IsAbs(output) {
|
||||||
log.Fatalf("Getwd() returned %v\n", err)
|
output = filepath.Join(root, output)
|
||||||
}
|
}
|
||||||
output := filepath.Join(cwd, outputFilename)
|
|
||||||
|
|
||||||
version := getVersion()
|
version := getVersion()
|
||||||
constants := Constants{}
|
constants := Constants{}
|
||||||
if version != "" {
|
if version != "" {
|
||||||
constants["main.version"] = 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)
|
verbosePrintf("ldflags: %s\n", ldflags)
|
||||||
|
|
||||||
args := []string{
|
var (
|
||||||
"-tags", strings.Join(buildTags, " "),
|
buildArgs []string
|
||||||
"-ldflags", ldflags,
|
testArgs []string
|
||||||
"-o", output, config.Main,
|
)
|
||||||
|
|
||||||
|
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 {
|
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 {
|
if runTests {
|
||||||
verbosePrintf("running tests\n")
|
verbosePrintf("running tests\n")
|
||||||
|
|
||||||
err = test(cwd, gopath, config.Tests...)
|
testArgs = append(testArgs, config.Tests...)
|
||||||
|
|
||||||
|
err = test(buildCWD, env, testArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("running tests failed: %v\n", err)
|
die("running tests failed: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
changelog/0.10.0_2020-09-13/issue-102
Normal file
14
changelog/0.10.0_2020-09-13/issue-102
Normal 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
|
||||||
|
|
||||||
19
changelog/0.10.0_2020-09-13/issue-117
Normal file
19
changelog/0.10.0_2020-09-13/issue-117
Normal 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
|
||||||
4
changelog/0.10.0_2020-09-13/issue-44
Normal file
4
changelog/0.10.0_2020-09-13/issue-44
Normal 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
|
||||||
8
changelog/0.10.0_2020-09-13/issue-60
Normal file
8
changelog/0.10.0_2020-09-13/issue-60
Normal 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
|
||||||
8
changelog/0.10.0_2020-09-13/pull-64
Normal file
8
changelog/0.10.0_2020-09-13/pull-64
Normal 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
|
||||||
9
changelog/0.11.0_2022-02-10/issue-119
Normal file
9
changelog/0.11.0_2022-02-10/issue-119
Normal 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
|
||||||
9
changelog/0.11.0_2022-02-10/issue-122
Normal file
9
changelog/0.11.0_2022-02-10/issue-122
Normal 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
|
||||||
7
changelog/0.11.0_2022-02-10/issue-126
Normal file
7
changelog/0.11.0_2022-02-10/issue-126
Normal 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
|
||||||
16
changelog/0.11.0_2022-02-10/issue-131
Normal file
16
changelog/0.11.0_2022-02-10/issue-131
Normal 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
|
||||||
9
changelog/0.11.0_2022-02-10/issue-146
Normal file
9
changelog/0.11.0_2022-02-10/issue-146
Normal 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
|
||||||
8
changelog/0.11.0_2022-02-10/issue-148
Normal file
8
changelog/0.11.0_2022-02-10/issue-148
Normal 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
|
||||||
20
changelog/0.11.0_2022-02-10/pull-112
Normal file
20
changelog/0.11.0_2022-02-10/pull-112
Normal 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
|
||||||
16
changelog/0.11.0_2022-02-10/pull-142
Normal file
16
changelog/0.11.0_2022-02-10/pull-142
Normal 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
|
||||||
8
changelog/0.11.0_2022-02-10/pull-158
Normal file
8
changelog/0.11.0_2022-02-10/pull-158
Normal 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
|
||||||
13
changelog/0.11.0_2022-02-10/pull-160
Normal file
13
changelog/0.11.0_2022-02-10/pull-160
Normal 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
|
||||||
9
changelog/0.12.0_2023-04-24/issue-133
Normal file
9
changelog/0.12.0_2023-04-24/issue-133
Normal 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
|
||||||
8
changelog/0.12.0_2023-04-24/issue-182
Normal file
8
changelog/0.12.0_2023-04-24/issue-182
Normal 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
|
||||||
7
changelog/0.12.0_2023-04-24/issue-187
Normal file
7
changelog/0.12.0_2023-04-24/issue-187
Normal 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
|
||||||
9
changelog/0.12.0_2023-04-24/issue-219
Normal file
9
changelog/0.12.0_2023-04-24/issue-219
Normal 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
|
||||||
13
changelog/0.12.0_2023-04-24/pull-194
Normal file
13
changelog/0.12.0_2023-04-24/pull-194
Normal 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
|
||||||
9
changelog/0.12.0_2023-04-24/pull-207
Normal file
9
changelog/0.12.0_2023-04-24/pull-207
Normal 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
|
||||||
7
changelog/0.12.0_2023-04-24/pull-208
Normal file
7
changelog/0.12.0_2023-04-24/pull-208
Normal 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
|
||||||
9
changelog/0.12.1_2023-07-09/issue-230
Normal file
9
changelog/0.12.1_2023-07-09/issue-230
Normal 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
|
||||||
8
changelog/0.12.1_2023-07-09/issue-238
Normal file
8
changelog/0.12.1_2023-07-09/issue-238
Normal 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
|
||||||
13
changelog/0.12.1_2023-07-09/pull-217
Normal file
13
changelog/0.12.1_2023-07-09/pull-217
Normal 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
|
||||||
7
changelog/0.13.0_2024-07-26/pull-267
Normal file
7
changelog/0.13.0_2024-07-26/pull-267
Normal 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
|
||||||
9
changelog/0.13.0_2024-07-26/pull-271
Normal file
9
changelog/0.13.0_2024-07-26/pull-271
Normal 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
|
||||||
15
changelog/0.13.0_2024-07-26/pull-272
Normal file
15
changelog/0.13.0_2024-07-26/pull-272
Normal 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
|
||||||
9
changelog/0.13.0_2024-07-26/pull-273
Normal file
9
changelog/0.13.0_2024-07-26/pull-273
Normal 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
|
||||||
10
changelog/0.14.0_2025-05-31/issue-189
Normal file
10
changelog/0.14.0_2025-05-31/issue-189
Normal 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
|
||||||
13
changelog/0.14.0_2025-05-31/issue-318
Normal file
13
changelog/0.14.0_2025-05-31/issue-318
Normal 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
|
||||||
7
changelog/0.14.0_2025-05-31/issue-321
Normal file
7
changelog/0.14.0_2025-05-31/issue-321
Normal 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
|
||||||
5
changelog/0.14.0_2025-05-31/pull-295
Normal file
5
changelog/0.14.0_2025-05-31/pull-295
Normal 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
|
||||||
12
changelog/0.14.0_2025-05-31/pull-307
Normal file
12
changelog/0.14.0_2025-05-31/pull-307
Normal 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
|
||||||
7
changelog/0.14.0_2025-05-31/pull-315
Normal file
7
changelog/0.14.0_2025-05-31/pull-315
Normal 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
|
||||||
11
changelog/0.14.0_2025-05-31/pull-322
Normal file
11
changelog/0.14.0_2025-05-31/pull-322
Normal 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
|
||||||
31
changelog/CHANGELOG-GitHub.tmpl
Normal file
31
changelog/CHANGELOG-GitHub.tmpl
Normal 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
32
changelog/CHANGELOG.tmpl
Normal 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
20
changelog/TEMPLATE
Normal 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
|
||||||
0
changelog/unreleased/.gitkeep
Normal file
0
changelog/unreleased/.gitkeep
Normal file
58
cmd/rest-server/listener_unix.go
Normal file
58
cmd/rest-server/listener_unix.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
78
cmd/rest-server/listener_unix_test.go
Normal file
78
cmd/rest-server/listener_unix_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
cmd/rest-server/listener_windows.go
Normal file
19
cmd/rest-server/listener_windows.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,87 +1,240 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
restserver "github.com/restic/rest-server"
|
restserver "github.com/restic/rest-server"
|
||||||
"github.com/spf13/cobra"
|
"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.
|
// cmdRoot is the base command when no other command has been specified.
|
||||||
var cmdRoot = &cobra.Command{
|
func newRestServerApp() *restServerApp {
|
||||||
Use: "rest-server",
|
rv := &restServerApp{
|
||||||
Short: "Run a REST server for use with restic",
|
CmdRoot: &cobra.Command{
|
||||||
SilenceErrors: true,
|
Use: "rest-server",
|
||||||
SilenceUsage: true,
|
Short: "Run a REST server for use with restic",
|
||||||
RunE: runRoot,
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
var version = "0.14.0-dev"
|
||||||
flags := cmdRoot.Flags()
|
|
||||||
flags.StringVar(&restserver.Config.CPUProfile, "cpuprofile", restserver.Config.CPUProfile, "write CPU profile to file")
|
func (app *restServerApp) tlsSettings() (bool, string, string, error) {
|
||||||
flags.BoolVar(&restserver.Config.Debug, "debug", restserver.Config.Debug, "output debug messages")
|
var key, cert string
|
||||||
flags.StringVar(&restserver.Config.Listen, "listen", restserver.Config.Listen, "listen address")
|
if !app.Server.TLS && (app.Server.TLSKey != "" || app.Server.TLSCert != "") {
|
||||||
flags.StringVar(&restserver.Config.Log, "log", restserver.Config.Log, "log HTTP requests in the combined log format")
|
return false, "", "", errors.New("requires enabled TLS")
|
||||||
flags.StringVar(&restserver.Config.Path, "path", restserver.Config.Path, "data directory")
|
} else if !app.Server.TLS {
|
||||||
flags.BoolVar(&restserver.Config.TLS, "tls", restserver.Config.TLS, "turn on TLS support")
|
return false, "", "", nil
|
||||||
flags.BoolVar(&restserver.Config.AppendOnly, "append-only", restserver.Config.AppendOnly, "enable append only mode")
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = "manually"
|
// 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 runRoot(cmd *cobra.Command, args []string) error {
|
func (app *restServerApp) runRoot(_ *cobra.Command, _ []string) error {
|
||||||
log.SetFlags(0)
|
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", app.Server.Path)
|
||||||
log.Printf("Data directory: %s", restserver.Config.Path)
|
|
||||||
|
|
||||||
if restserver.Config.CPUProfile != "" {
|
if app.CPUProfile != "" {
|
||||||
f, err := os.Create(restserver.Config.CPUProfile)
|
f, err := os.Create(app.CPUProfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = f.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if err := pprof.StartCPUProfile(f); err != nil {
|
if err := pprof.StartCPUProfile(f); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("CPU profiling enabled")
|
|
||||||
defer pprof.StopCPUProfile()
|
defer pprof.StopCPUProfile()
|
||||||
|
|
||||||
|
log.Println("CPU profiling enabled")
|
||||||
|
defer log.Println("Stopped CPU profiling")
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := restserver.NewMux()
|
if app.Server.NoAuth {
|
||||||
|
|
||||||
var handler http.Handler
|
|
||||||
htpasswdFile, err := restserver.NewHtpasswdFromFile(filepath.Join(restserver.Config.Path, ".htpasswd"))
|
|
||||||
if err != nil {
|
|
||||||
handler = mux
|
|
||||||
log.Println("Authentication disabled")
|
log.Println("Authentication disabled")
|
||||||
} else {
|
} else {
|
||||||
handler = restserver.AuthHandler(htpasswdFile, mux)
|
if app.Server.ProxyAuthUsername == "" {
|
||||||
log.Println("Authentication enabled")
|
log.Println("Authentication enabled")
|
||||||
|
} else {
|
||||||
|
log.Println("Proxy Authentication enabled.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !restserver.Config.TLS {
|
handler, err := restserver.NewHandler(&app.Server)
|
||||||
log.Printf("Starting server on %s\n", restserver.Config.Listen)
|
if err != nil {
|
||||||
err = http.ListenAndServe(restserver.Config.Listen, handler)
|
log.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Server.AppendOnly {
|
||||||
|
log.Println("Append only mode enabled")
|
||||||
} else {
|
} else {
|
||||||
privateKey := filepath.Join(restserver.Config.Path, "private_key")
|
log.Println("Append only mode disabled")
|
||||||
publicKey := filepath.Join(restserver.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", restserver.Config.Listen)
|
|
||||||
err = http.ListenAndServeTLS(restserver.Config.Listen, publicKey, privateKey, handler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
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() {
|
func main() {
|
||||||
if err := cmdRoot.Execute(); err != nil {
|
// 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)
|
log.Fatalf("error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
284
cmd/rest-server/main_test.go
Normal file
284
cmd/rest-server/main_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,10 @@ if [ -z "$1" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
htpasswd -s $PASSWORD_FILE $1 $2
|
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
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ if [ -z "$1" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
htpasswd -D $PASSWORD_FILE $1
|
htpasswd -D "$PASSWORD_FILE" "$1"
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ -z "$DISABLE_AUTHENTICATION" ]; then
|
if [ -n "$DISABLE_AUTHENTICATION" ]; then
|
||||||
|
OPTIONS="--no-auth $OPTIONS"
|
||||||
|
else
|
||||||
if [ ! -f "$PASSWORD_FILE" ]; then
|
if [ ! -f "$PASSWORD_FILE" ]; then
|
||||||
touch "$PASSWORD_FILE"
|
( umask 027 && touch "$PASSWORD_FILE" )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s "$PASSWORD_FILE" ]; then
|
if [ ! -s "$PASSWORD_FILE" ]; then
|
||||||
@@ -12,8 +14,6 @@ if [ -z "$DISABLE_AUTHENTICATION" ]; then
|
|||||||
echo "**WARNING** No user exists, please 'docker exec -it \$CONTAINER_ID create_user'"
|
echo "**WARNING** No user exists, please 'docker exec -it \$CONTAINER_ID create_user'"
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
rm -f "$PASSWORD_FILE"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec rest-server --path "$DATA_DIRECTORY" $OPTIONS
|
exec rest-server --path "$DATA_DIRECTORY" --htpasswd-file "$PASSWORD_FILE" $OPTIONS
|
||||||
|
|||||||
@@ -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
26
examples/bsd/freebsd
Normal 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
14
examples/bsd/openbsd
Normal 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
|
||||||
35
examples/compose-with-grafana/README.md
Normal file
35
examples/compose-with-grafana/README.md
Normal 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/).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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 .
|
||||||
637
examples/compose-with-grafana/dashboards/rest-server.json
Normal file
637
examples/compose-with-grafana/dashboards/rest-server.json
Normal 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
|
||||||
|
}
|
||||||
BIN
examples/compose-with-grafana/datasource.png
Normal file
BIN
examples/compose-with-grafana/datasource.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
1
examples/compose-with-grafana/demo-passwd
Normal file
1
examples/compose-with-grafana/demo-passwd
Normal file
@@ -0,0 +1 @@
|
|||||||
|
demo-passwd
|
||||||
59
examples/compose-with-grafana/docker-compose.yml
Normal file
59
examples/compose-with-grafana/docker-compose.yml
Normal 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
|
||||||
|
|
||||||
313
examples/compose-with-grafana/grafana.ini
Normal file
313
examples/compose-with-grafana/grafana.ini
Normal 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
|
||||||
23
examples/compose-with-grafana/prometheus/prometheus.yml
Normal file
23
examples/compose-with-grafana/prometheus/prometheus.yml
Normal 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']
|
||||||
BIN
examples/compose-with-grafana/screenshot.png
Normal file
BIN
examples/compose-with-grafana/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
83
examples/systemd/rest-server.service
Normal file
83
examples/systemd/rest-server.service
Normal 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
|
||||||
5
examples/systemd/rest-server.socket
Normal file
5
examples/systemd/rest-server.socket
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[Socket]
|
||||||
|
ListenStream = 8000
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy = sockets.target
|
||||||
28
go.mod
Normal file
28
go.mod
Normal 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
56
go.sum
Normal 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=
|
||||||
532
handlers.go
532
handlers.go
@@ -1,25 +1,115 @@
|
|||||||
package restserver
|
package restserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"sync"
|
||||||
|
|
||||||
"goji.io/middleware"
|
"github.com/restic/rest-server/quota"
|
||||||
"goji.io/pat"
|
"github.com/restic/rest-server/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isHashed(dir string) bool {
|
// Server encapsulates the rest-server's settings and repo management logic
|
||||||
return dir == "data"
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// httpDefaultError write a HTTP error with the default description
|
||||||
|
func httpDefaultError(w http.ResponseWriter, code int) {
|
||||||
|
http.Error(w, http.StatusText(code), code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func valid(name string) bool {
|
||||||
@@ -35,15 +125,17 @@ func valid(name string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var validTypes = []string{"data", "index", "keys", "locks", "snapshots", "config"}
|
|
||||||
|
|
||||||
func isValidType(name string) bool {
|
func isValidType(name string) bool {
|
||||||
for _, tpe := range validTypes {
|
for _, tpe := range repo.ObjectTypes {
|
||||||
|
if name == tpe {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tpe := range repo.FileTypes {
|
||||||
if name == tpe {
|
if name == tpe {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,383 +159,45 @@ func join(base string, names ...string) (string, error) {
|
|||||||
return filepath.Join(clean...), nil
|
return filepath.Join(clean...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRepo returns the repository location, relative to Config.Path.
|
// splitURLPath splits the URL path into a folderPath of the subrepo, and
|
||||||
func getRepo(r *http.Request) string {
|
// a remainder that can be passed to repo.Handler.
|
||||||
if strings.HasPrefix(fmt.Sprintf("%s", middleware.Pattern(r.Context())), "/:repo") {
|
// Example: /foo/bar/locks/0123... will be split into:
|
||||||
return pat.Param(r, "repo")
|
//
|
||||||
|
// ["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)
|
||||||
return "."
|
// 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):]
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPath returns the path for a file type in the repo.
|
// folderPathValid checks if a folderPath returned by splitURLPath is valid and
|
||||||
func getPath(r *http.Request, fileType string) (string, error) {
|
// safe.
|
||||||
if !isValidType(fileType) {
|
func folderPathValid(folderPath []string) bool {
|
||||||
return "", errors.New("invalid file type")
|
for _, name := range folderPath {
|
||||||
}
|
if name == "" || name == ".." || name == "." || !valid(name) {
|
||||||
return join(Config.Path, getRepo(r), fileType)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFilePath returns the path for a file in the repo.
|
|
||||||
func getFilePath(r *http.Request, fileType, name string) (string, error) {
|
|
||||||
if !isValidType(fileType) {
|
|
||||||
return "", errors.New("invalid file type")
|
|
||||||
}
|
|
||||||
|
|
||||||
if isHashed(fileType) {
|
|
||||||
if len(name) < 2 {
|
|
||||||
return "", errors.New("file name is too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
return join(Config.Path, getRepo(r), fileType, name[:2], name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return join(Config.Path, getRepo(r), fileType, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, err := getPath(r, "config")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
st, err := os.Stat(cfg)
|
|
||||||
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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig allows for a config to be retrieved.
|
|
||||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if Config.Debug {
|
|
||||||
log.Println("GetConfig()")
|
|
||||||
}
|
|
||||||
cfg, err := getPath(r, "config")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := ioutil.ReadFile(cfg)
|
|
||||||
if err != nil {
|
|
||||||
if Config.Debug {
|
|
||||||
log.Print(err)
|
|
||||||
}
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), 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, err := getPath(r, "config")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Config.AppendOnly {
|
|
||||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := getPath(r, "config")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Remove(cfg); 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()")
|
|
||||||
}
|
|
||||||
fileType := pat.Param(r, "type")
|
|
||||||
path, err := getPath(r, fileType)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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(fileType) {
|
|
||||||
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()")
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := getFilePath(r, pat.Param(r, "type"), pat.Param(r, "name"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()")
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := getFilePath(r, pat.Param(r, "type"), pat.Param(r, "name"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()")
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := getFilePath(r, pat.Param(r, "type"), pat.Param(r, "name"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()")
|
|
||||||
}
|
|
||||||
|
|
||||||
if Config.AppendOnly && pat.Param(r, "type") != "locks" {
|
|
||||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := getFilePath(r, pat.Param(r, "type"), pat.Param(r, "name"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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, err := join(Config.Path, getRepo(r))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 validTypes {
|
|
||||||
if d == "config" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Join(repo, d), 0700); err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
581
handlers_test.go
581
handlers_test.go
@@ -1,31 +1,44 @@
|
|||||||
package restserver
|
package restserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/sha256-simd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJoin(t *testing.T) {
|
func TestJoin(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
base, name string
|
base string
|
||||||
result string
|
names []string
|
||||||
|
result string
|
||||||
}{
|
}{
|
||||||
{"/", "foo/bar", "/foo/bar"},
|
{"/", []string{"foo", "bar"}, "/foo/bar"},
|
||||||
{"/srv/server", "foo/bar", "/srv/server/foo/bar"},
|
{"/srv/server", []string{"foo", "bar"}, "/srv/server/foo/bar"},
|
||||||
{"/srv/server", "/foo/bar", "/srv/server/foo/bar"},
|
{"/srv/server", []string{"foo", "..", "bar"}, "/srv/server/foo/bar"},
|
||||||
{"/srv/server", "foo/../bar", "/srv/server/bar"},
|
{"/srv/server", []string{"..", "bar"}, "/srv/server/bar"},
|
||||||
{"/srv/server", "../bar", "/srv/server/bar"},
|
{"/srv/server", []string{".."}, "/srv/server"},
|
||||||
{"/srv/server", "..", "/srv/server"},
|
{"/srv/server", []string{"..", ".."}, "/srv/server"},
|
||||||
{"/srv/server", "../..", "/srv/server"},
|
{"/srv/server", []string{"repo", "data"}, "/srv/server/repo/data"},
|
||||||
{"/srv/server", "/repo/data/", "/srv/server/repo/data"},
|
{"/srv/server", []string{"repo", "data", "..", ".."}, "/srv/server/repo/data"},
|
||||||
{"/srv/server", "/repo/data/../..", "/srv/server"},
|
{"/srv/server", []string{"repo", "data", "..", "data", "..", "..", ".."}, "/srv/server/repo/data/data"},
|
||||||
{"/srv/server", "/repo/data/../data/../../..", "/srv/server"},
|
|
||||||
{"/srv/server", "/repo/data/../data/../../..", "/srv/server"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
got, err := join(filepath.FromSlash(test.base), test.name)
|
got, err := join(filepath.FromSlash(test.base), test.names...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -37,3 +50,543 @@ func TestJoin(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
234
htpasswd.go
234
htpasswd.go
@@ -1,7 +1,7 @@
|
|||||||
package restserver
|
package restserver
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Copied from: github.com/bitly/oauth2_proxy
|
Original version copied from: github.com/bitly/oauth2_proxy
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
@@ -26,65 +26,259 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"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.
|
// 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.
|
// HtpasswdFile is a map for usernames to passwords.
|
||||||
type HtpasswdFile struct {
|
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,
|
// 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.
|
// it is returned, together with a nil-Pointer for the HtpasswdFile.
|
||||||
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// throttleTimer sends at most one message per CheckInterval to throttle file change checks.
|
||||||
// encountered, it is returned, together with a nil-Pointer for the HtpasswdFile.
|
func (h *HtpasswdFile) throttleTimer() {
|
||||||
func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {
|
var check struct{}
|
||||||
cr := csv.NewReader(file)
|
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.Comma = ':'
|
||||||
cr.Comment = '#'
|
cr.Comment = '#'
|
||||||
cr.TrimLeadingSpace = true
|
cr.TrimLeadingSpace = true
|
||||||
|
|
||||||
records, err := cr.ReadAll()
|
records, err := cr.ReadAll()
|
||||||
if err != nil {
|
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 {
|
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
|
// 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.
|
// password is wrong, false is returned.
|
||||||
func (h *HtpasswdFile) Validate(user string, password string) bool {
|
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 {
|
if !exists {
|
||||||
return false
|
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 := sha1.New()
|
||||||
d.Write([]byte(password))
|
_, _ = d.Write([]byte(password))
|
||||||
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
45
htpasswd_test.go
Normal file
45
htpasswd_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
92
metrics.go
Normal file
92
metrics.go
Normal 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)
|
||||||
|
}
|
||||||
147
mux.go
147
mux.go
@@ -1,31 +1,19 @@
|
|||||||
package restserver
|
package restserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
goji "goji.io"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"goji.io/pat"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/restic/rest-server/quota"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Config = struct {
|
func (s *Server) debugHandler(next http.Handler) http.Handler {
|
||||||
Path string
|
|
||||||
Listen string
|
|
||||||
TLS bool
|
|
||||||
Log string
|
|
||||||
CPUProfile string
|
|
||||||
Debug bool
|
|
||||||
AppendOnly bool
|
|
||||||
}{
|
|
||||||
Path: "/tmp/restic",
|
|
||||||
Listen: ":8000",
|
|
||||||
AppendOnly: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
func debugHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("%s %s", r.Method, r.URL)
|
log.Printf("%s %s", r.Method, r.URL)
|
||||||
@@ -33,47 +21,98 @@ func debugHandler(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func logHandler(next http.Handler) http.Handler {
|
func (s *Server) logHandler(next http.Handler) http.Handler {
|
||||||
accessLog, err := os.OpenFile(Config.Log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
var accessLog io.Writer
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error: %v", err)
|
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)
|
return handlers.CombinedLoggingHandler(accessLog, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMux() *goji.Mux {
|
func (s *Server) checkAuth(r *http.Request) (username string, ok bool) {
|
||||||
mux := goji.NewMux()
|
if s.NoAuth {
|
||||||
|
return username, true
|
||||||
if Config.Debug {
|
|
||||||
mux.Use(debugHandler)
|
|
||||||
}
|
}
|
||||||
|
if s.ProxyAuthUsername != "" {
|
||||||
if Config.Log != "" {
|
username = r.Header.Get(s.ProxyAuthUsername)
|
||||||
mux.Use(logHandler)
|
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
|
||||||
mux.HandleFunc(pat.Head("/config"), CheckConfig)
|
}
|
||||||
mux.HandleFunc(pat.Head("/:repo/config"), CheckConfig)
|
|
||||||
mux.HandleFunc(pat.Get("/config"), GetConfig)
|
func (s *Server) wrapMetricsAuth(f http.HandlerFunc) http.HandlerFunc {
|
||||||
mux.HandleFunc(pat.Get("/:repo/config"), GetConfig)
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
mux.HandleFunc(pat.Post("/config"), SaveConfig)
|
username, ok := s.checkAuth(r)
|
||||||
mux.HandleFunc(pat.Post("/:repo/config"), SaveConfig)
|
if !ok {
|
||||||
mux.HandleFunc(pat.Delete("/config"), DeleteConfig)
|
httpDefaultError(w, http.StatusUnauthorized)
|
||||||
mux.HandleFunc(pat.Delete("/:repo/config"), DeleteConfig)
|
return
|
||||||
mux.HandleFunc(pat.Get("/:type/"), ListBlobs)
|
}
|
||||||
mux.HandleFunc(pat.Get("/:repo/:type/"), ListBlobs)
|
if s.PrivateRepos && username != "metrics" {
|
||||||
mux.HandleFunc(pat.Head("/:type/:name"), CheckBlob)
|
httpDefaultError(w, http.StatusUnauthorized)
|
||||||
mux.HandleFunc(pat.Head("/:repo/:type/:name"), CheckBlob)
|
return
|
||||||
mux.HandleFunc(pat.Get("/:type/:name"), GetBlob)
|
}
|
||||||
mux.HandleFunc(pat.Get("/:repo/:type/:name"), GetBlob)
|
f(w, r)
|
||||||
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)
|
// NewHandler returns the master HTTP multiplexer/router.
|
||||||
mux.HandleFunc(pat.Post("/"), CreateRepo)
|
func NewHandler(server *Server) (http.Handler, error) {
|
||||||
mux.HandleFunc(pat.Post("/:repo"), CreateRepo)
|
if !server.NoAuth && server.ProxyAuthUsername == "" {
|
||||||
mux.HandleFunc(pat.Post("/:repo/"), CreateRepo)
|
var err error
|
||||||
|
if server.HtpasswdPath == "" {
|
||||||
return mux
|
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
80
mux_test.go
Normal 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
124
quota/quota.go
Normal 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
823
repo/repo.go
Normal 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
19
repo/repo_unix.go
Normal 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
4
repo/repo_windows.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
// Windows is not macOS.
|
||||||
|
func isMacENOTTY(err error) bool { return false }
|
||||||
18
vendor/github.com/gorilla/handlers/.travis.yml
generated
vendored
18
vendor/github.com/gorilla/handlers/.travis.yml
generated
vendored
@@ -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 ./...
|
|
||||||
22
vendor/github.com/gorilla/handlers/LICENSE
generated
vendored
22
vendor/github.com/gorilla/handlers/LICENSE
generated
vendored
@@ -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.
|
|
||||||
55
vendor/github.com/gorilla/handlers/README.md
generated
vendored
55
vendor/github.com/gorilla/handlers/README.md
generated
vendored
@@ -1,55 +0,0 @@
|
|||||||
gorilla/handlers
|
|
||||||
================
|
|
||||||
[](https://godoc.org/github.com/gorilla/handlers) [](https://travis-ci.org/gorilla/handlers)
|
|
||||||
[](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.
|
|
||||||
|
|
||||||
74
vendor/github.com/gorilla/handlers/canonical.go
generated
vendored
74
vendor/github.com/gorilla/handlers/canonical.go
generated
vendored
@@ -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
|
|
||||||
}
|
|
||||||
127
vendor/github.com/gorilla/handlers/canonical_test.go
generated
vendored
127
vendor/github.com/gorilla/handlers/canonical_test.go
generated
vendored
@@ -1,127 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCleanHost(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
in, want string
|
|
||||||
}{
|
|
||||||
{"www.google.com", "www.google.com"},
|
|
||||||
{"www.google.com foo", "www.google.com"},
|
|
||||||
{"www.google.com/foo", "www.google.com"},
|
|
||||||
{" first character is a space", ""},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
got := cleanHost(tt.in)
|
|
||||||
if tt.want != got {
|
|
||||||
t.Errorf("cleanHost(%q) = %q, want %q", tt.in, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCanonicalHost(t *testing.T) {
|
|
||||||
gorilla := "http://www.gorillatoolkit.org"
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
r := newRequest("GET", "http://www.example.com/")
|
|
||||||
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
|
|
||||||
// Test a re-direct: should return a 302 Found.
|
|
||||||
CanonicalHost(gorilla, http.StatusFound)(testHandler).ServeHTTP(rr, r)
|
|
||||||
|
|
||||||
if rr.Code != http.StatusFound {
|
|
||||||
t.Fatalf("bad status: got %v want %v", rr.Code, http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rr.Header().Get("Location") != gorilla+r.URL.Path {
|
|
||||||
t.Fatalf("bad re-direct: got %q want %q", rr.Header().Get("Location"), gorilla+r.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeepsQueryString(t *testing.T) {
|
|
||||||
google := "https://www.google.com"
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
querystring := url.Values{"q": {"golang"}, "format": {"json"}}.Encode()
|
|
||||||
r := newRequest("GET", "http://www.example.com/search?"+querystring)
|
|
||||||
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
CanonicalHost(google, http.StatusFound)(testHandler).ServeHTTP(rr, r)
|
|
||||||
|
|
||||||
want := google + r.URL.Path + "?" + querystring
|
|
||||||
if rr.Header().Get("Location") != want {
|
|
||||||
t.Fatalf("bad re-direct: got %q want %q", rr.Header().Get("Location"), want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBadDomain(t *testing.T) {
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
r := newRequest("GET", "http://www.example.com/")
|
|
||||||
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
|
|
||||||
// Test a bad domain - should return 200 OK.
|
|
||||||
CanonicalHost("%", http.StatusFound)(testHandler).ServeHTTP(rr, r)
|
|
||||||
|
|
||||||
if rr.Code != http.StatusOK {
|
|
||||||
t.Fatalf("bad status: got %v want %v", rr.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptyHost(t *testing.T) {
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
r := newRequest("GET", "http://www.example.com/")
|
|
||||||
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
|
|
||||||
// Test a domain that returns an empty url.Host from url.Parse.
|
|
||||||
CanonicalHost("hello.com", http.StatusFound)(testHandler).ServeHTTP(rr, r)
|
|
||||||
|
|
||||||
if rr.Code != http.StatusOK {
|
|
||||||
t.Fatalf("bad status: got %v want %v", rr.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeaderWrites(t *testing.T) {
|
|
||||||
gorilla := "http://www.gorillatoolkit.org"
|
|
||||||
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Catch the log output to ensure we don't write multiple headers.
|
|
||||||
var b bytes.Buffer
|
|
||||||
buf := bufio.NewWriter(&b)
|
|
||||||
tl := log.New(buf, "test: ", log.Lshortfile)
|
|
||||||
|
|
||||||
srv := httptest.NewServer(
|
|
||||||
CanonicalHost(gorilla, http.StatusFound)(testHandler))
|
|
||||||
defer srv.Close()
|
|
||||||
srv.Config.ErrorLog = tl
|
|
||||||
|
|
||||||
_, err := http.Get(srv.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = buf.Flush()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We rely on the error not changing: net/http does not export it.
|
|
||||||
if strings.Contains(b.String(), "multiple response.WriteHeader calls") {
|
|
||||||
t.Fatalf("re-direct did not return early: multiple header writes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user