mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
580 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1456f15f9a | |||
| fdfe2ae53b | |||
| 1c190b001b | |||
| 3634c4593f | |||
| 7ca15861dd | |||
| 8ff330c555 | |||
| 626f19a264 | |||
| 6ca5828221 | |||
| 6fe04a30b1 | |||
| 19b45546a7 | |||
| d322de6b42 | |||
| ce3ca541d8 | |||
| 581f1defcb | |||
| 0d2a3511dc | |||
| 73643ea736 | |||
| 809e72792c | |||
| 9fb0b1e838 | |||
| 244b839f98 | |||
| 904d9cab39 | |||
| ac65f690ae | |||
| 37aa516a6e | |||
| 105acfa086 | |||
| deba26d225 | |||
| 178ba024fe | |||
| e207240f9a | |||
| 397e04ebd9 | |||
| d2c15bea1b | |||
| 8da9eaee34 | |||
| ea3688e1c0 | |||
| c87f82f0ce | |||
| 5c55e5d53f | |||
| 7ee3ab7baa | |||
| ba08833b2a | |||
| 9eecd698da | |||
| 0fa1a3b630 | |||
| 673d3d00f2 | |||
| 2acb208e32 | |||
| e02117cb8a | |||
| 95b2863df2 | |||
| 341d4fb805 | |||
| 745cb0e9e6 | |||
| 9af05719bc | |||
| d08cbefff8 | |||
| 2eede58b3a | |||
| 235357abc8 | |||
| 4b4e16edaf | |||
| ee64719d93 | |||
| 2491336c11 | |||
| 1698838685 | |||
| 4c43bf8cc8 | |||
| 348cb798e2 | |||
| e211491407 | |||
| 6e2fabb2a4 | |||
| 8cc60e6896 | |||
| bea8dedfb2 | |||
| f2ce81cc8b | |||
| 2cab475ba5 | |||
| c32f383a01 | |||
| 37093befd5 | |||
| d692d503a3 | |||
| 3c1def2430 | |||
| b583007c49 | |||
| 6b60a301c0 | |||
| d6632e2145 | |||
| 903776238e | |||
| f741ab3463 | |||
| 76ac28a624 | |||
| 61b427fa47 | |||
| 42a6628935 | |||
| 6a4d638c1e | |||
| aa6c5fde07 | |||
| 31c6ac097e | |||
| 406df22a16 | |||
| afb2ca27c1 | |||
| ce45353e61 | |||
| 89124aa570 | |||
| ab2fc9d066 | |||
| fc7340e11a | |||
| 3f48a2eb45 | |||
| f192ae5ea5 | |||
| b62f8e0582 | |||
| ae86f6dd91 | |||
| b550ea433b | |||
| e42514ad4a | |||
| f596fd77bb | |||
| 0433f9d075 | |||
| c67c8e60cc | |||
| 8f8ecd2e2a | |||
| 115b877e1a | |||
| 2ce3deb540 | |||
| acf4dde1dd | |||
| 7a4548c582 | |||
| 6cbd93736f | |||
| c447236357 | |||
| 5a19db5dc2 | |||
| cfe85a9fe6 | |||
| 90f1f7bce7 | |||
| 2762f8f058 | |||
| 99d34f1c1d | |||
| 36a6c7daf0 | |||
| ca6e54bbb8 | |||
| fb5168d3b4 | |||
| 217419f6d9 | |||
| 4d18587192 | |||
| b216d285df | |||
| b8cba62643 | |||
| 3f5d27cd5d | |||
| 26fb8b3efd | |||
| e6c6210772 | |||
| 1324da2241 | |||
| 71e81d262b | |||
| 5fe69ac4ab | |||
| e717028f83 | |||
| a60da8e7ab | |||
| 00e99df209 | |||
| c83d40ccd4 | |||
| e4ec08e977 | |||
| 03ab55b51a | |||
| cef6e098bb | |||
| 260982b2df | |||
| 0130b699df | |||
| ca5c679880 | |||
| e2d41ee761 | |||
| 86b785e51c | |||
| f6ae092507 | |||
| a2a41a5bdf | |||
| 6fb98ba188 | |||
| 063ed1e7f9 | |||
| 2de0acc11f | |||
| 5d97522d18 | |||
| f6b9cb7122 | |||
| 78760c0ddc | |||
| b0a491aec8 | |||
| 45b171ff3a | |||
| 623a1c588e | |||
| 7cca291d62 | |||
| e3591009dc | |||
| 30c14084ab | |||
| 99f91c4c6f | |||
| 0005e3acdc | |||
| 0b09b070e5 | |||
| 7f9cfcc0f2 | |||
| 87a742c1e5 | |||
| 57c6f22684 | |||
| dd103a6787 | |||
| 23cc26d585 | |||
| bc2e406572 | |||
| bf776e7de7 | |||
| f42b138fb1 | |||
| 2cc5d2227d | |||
| 15bf9c196c | |||
| eb80165583 | |||
| 17d938fc54 | |||
| 98bbc54fdc | |||
| 9bdd6caa0b | |||
| f7f6e371ef | |||
| b8cf4d5897 | |||
| 04ec3c5f05 | |||
| 8b28c36d48 | |||
| 4a07a5d41e | |||
| b81ae38686 | |||
| 5c7ca7d96e | |||
| ec56c25708 | |||
| c0f827e0bd | |||
| 490cd02f82 | |||
| 9639fe7d28 | |||
| 3592e59399 | |||
| f74fed3f54 | |||
| 8b2ad61220 | |||
| 6614d1c495 | |||
| c6bddbfbe2 | |||
| 0742530d3d | |||
| 6b6cd934d0 | |||
| 5b878d5bd3 | |||
| 2105d59936 | |||
| 9a1370c2c8 | |||
| d810637a9f | |||
| 5d3ccf1eb7 | |||
| aad9f90cad | |||
| 07ef4b0c7d | |||
| 2bfaf8e896 | |||
| 372540f0ee | |||
| 793a405810 | |||
| 85ff0e3604 | |||
| e51e56a494 | |||
| 35174a8ba8 | |||
| 21643a007a | |||
| 2466ed1484 | |||
| a66f461201 | |||
| 07ad4655db | |||
| 271b5af148 | |||
| 99e2b56519 | |||
| 64f0173948 | |||
| fe5a531c58 | |||
| 8c0c1a7b88 | |||
| 25dea2903e | |||
| d876de61e5 | |||
| 8be1f0ea66 | |||
| 2eda21ec6d | |||
| d418e319ab | |||
| ba514f9660 | |||
| 3dcc34d341 | |||
| 871abf1053 | |||
| 29315847a8 | |||
| 994b9033e9 | |||
| 590480513a | |||
| 7527c01705 | |||
| 8aef859a55 | |||
| a5ebec0041 | |||
| 7c419d5349 | |||
| 3828a3aaac | |||
| 8bae8f5f5a | |||
| 21f1f95e7b | |||
| 78e98c40d3 | |||
| 5c99267dd8 | |||
| a6df4cdbbc | |||
| dff78d82ce | |||
| 8c7c2e4af2 | |||
| 3d9f8eac08 | |||
| 06ea0a5295 | |||
| 788462bd4c | |||
| 5a0603ed72 | |||
| 984d384d14 | |||
| fdabac51a8 | |||
| 95d944613b | |||
| 2b33d9a5e5 | |||
| 5c8b502964 | |||
| 82bebfab8a | |||
| be3849c267 | |||
| 16ee985c22 | |||
| 95ed603de7 | |||
| cbb405f6aa | |||
| 724c728678 | |||
| 21408212da | |||
| fe516575db | |||
| 080a62d5c5 | |||
| dae4913fe3 | |||
| 6455efa5d3 | |||
| 5ab17a3a37 | |||
| c3bcd967bd | |||
| 6ea121ddf8 | |||
| 8005b7ab73 | |||
| b1a456cfe3 | |||
| 5e9d81b507 | |||
| 09a8517065 | |||
| 87b6cf470b | |||
| f935458e3e | |||
| 2e0615270d | |||
| fab5e4372a | |||
| 3c90e370a4 | |||
| a8533e5630 | |||
| b07f6958ac | |||
| 33a318d173 | |||
| 68adfdc559 | |||
| a841688cc0 | |||
| 52ae5f70d2 | |||
| 44f23a67bb | |||
| 8b7d6a9ee8 | |||
| 7c7ef8d40e | |||
| 14d3fd7d03 | |||
| 512b004332 | |||
| db4293cb5f | |||
| 6e10586303 | |||
| 8de1a76227 | |||
| 9fe54e1c60 | |||
| b43e986a52 | |||
| 1228dd7d93 | |||
| af26a03da1 | |||
| 8025ad9107 | |||
| 6cdb2392d7 | |||
| 0ca109db4a | |||
| 0fc97211ab | |||
| ad90b273db | |||
| 93bc1b72e3 | |||
| a19da07b72 | |||
| 16782d9988 | |||
| dfdddcfacb | |||
| 7ff02f37b6 | |||
| e4a2add73f | |||
| 95615f5377 | |||
| 8e515289cb | |||
| 6e95477224 | |||
| 97d918df3e | |||
| f5c6a8553c | |||
| 263ffbfaec | |||
| bf363f061d | |||
| 7129f6c1c0 | |||
| cb25dd72ab | |||
| d55fa68902 | |||
| b1f41d0ff1 | |||
| 6011ce120a | |||
| 27e288ab19 | |||
| 35f70c98fa | |||
| fb06c041c4 | |||
| 8ef0a0b4f8 | |||
| 8d3c64932e | |||
| 0dd9243478 | |||
| 432b94239d | |||
| 4611537f06 | |||
| 76c22c7b38 | |||
| c7da6175bc | |||
| 11a2733dc2 | |||
| 1be121cec7 | |||
| dccba71276 | |||
| be36aade9a | |||
| ba0000678d | |||
| c4c45f8e01 | |||
| 54e458b756 | |||
| d803561212 | |||
| 813fff0584 | |||
| d2e7baed8d | |||
| d6dad04e96 | |||
| 442fd748f6 | |||
| b00dfd3965 | |||
| 6c533558a3 | |||
| 2fbe2ff40b | |||
| faf67b1067 | |||
| 208f2ff93c | |||
| 19e834cf36 | |||
| bce2edd22d | |||
| a458544d9f | |||
| 2f91b44587 | |||
| e3726588b4 | |||
| abf5ab340e | |||
| acf7dea68f | |||
| bc738991b6 | |||
| fcd8869f51 | |||
| 1e31be8de0 | |||
| 4aa3af4b78 | |||
| 8715a28320 | |||
| 715e6ddf51 | |||
| 9c0bf311f9 | |||
| 5300949e0d | |||
| 411152016e | |||
| 5c7640a8d9 | |||
| f8366c2f09 | |||
| fe36d26b63 | |||
| b38365ff3b | |||
| 26cc883708 | |||
| 93943a6ac2 | |||
| 85ce15a5ad | |||
| dedcfd4e3d | |||
| 20fe9cf024 | |||
| bcbe1c220d | |||
| a53b27c62e | |||
| 03306e646e | |||
| 53dd600b4d | |||
| ce1205239a | |||
| bc3e44c1a6 | |||
| 8c55167f71 | |||
| be7abda7d4 | |||
| 6fd28b81dc | |||
| 65c060f56e | |||
| 44cb804b9e | |||
| c11e3bffd6 | |||
| f29a9eee0d | |||
| 370b78c5c7 | |||
| 1ecb216001 | |||
| 94f98c0733 | |||
| 2c3657bb8a | |||
| 5b36424cf0 | |||
| 0006df6026 | |||
| c95db3551d | |||
| 8eb2c37251 | |||
| 1e66226217 | |||
| 7b4aa108c7 | |||
| 8b11ed347b | |||
| b249b45d10 | |||
| c12bf4054c | |||
| 735d6ce405 | |||
| 7b33c8db31 | |||
| 11696793bd | |||
| 3e8bff594a | |||
| 2f684e42d5 | |||
| ba29f9d41d | |||
| 40e05e5a01 | |||
| 39d61cad2d | |||
| bc9f944837 | |||
| 4c289fc6ad | |||
| 19f36667f7 | |||
| 484cee1ac1 | |||
| d030bfdae0 | |||
| db4c73dd58 | |||
| f15f0d5839 | |||
| e73b117332 | |||
| 2fd22139c6 | |||
| 5c9ebe3af1 | |||
| 2ab2d5bf9e | |||
| c09e86fddc | |||
| 46aaf02371 | |||
| 3b80c505fb | |||
| 1d1e194229 | |||
| 839507e24e | |||
| 833d67446f | |||
| d0c1756fc5 | |||
| ed40a5dcab | |||
| 7799554baa | |||
| 2cb01d43cf | |||
| 758269124e | |||
| b4dce74e59 | |||
| fe389fcbd7 | |||
| 005a11cf4b | |||
| 194df652eb | |||
| 53bbdf1766 | |||
| e48d83452e | |||
| 2459c292a4 | |||
| 0cf592fa2e | |||
| d9136fb0a0 | |||
| c32b7e8865 | |||
| 1ce10b453f | |||
| 0c8ad52be1 | |||
| d67d8cf5a8 | |||
| 44b7ce9850 | |||
| b4f4fcd437 | |||
| 50e62d06bc | |||
| 9169cd43d4 | |||
| e12c62e60b | |||
| 3e9e7555ef | |||
| f6126acf37 | |||
| 97ace2a39e | |||
| 4bd9496525 | |||
| 14f9662f9c | |||
| 21d7b662e7 | |||
| 3ba9e143a2 | |||
| d2e46c2be0 | |||
| 80b54f3b9d | |||
| 0830fbad03 | |||
| a60d54dbfd | |||
| acb8f0e0c2 | |||
| 652460e03e | |||
| 4a1e1649bc | |||
| ccfb12347b | |||
| 50961ecc77 | |||
| 026df7c5cb | |||
| 8e821b5039 | |||
| 9d8bff28c2 | |||
| d242f10eda | |||
| 2dc4fcc62b | |||
| afd154119a | |||
| e34ff21a71 | |||
| af25f0254e | |||
| a0fd2b6c0a | |||
| c0da7d487a | |||
| 8420a2f250 | |||
| 59910923d1 | |||
| 0544f0266a | |||
| b2aa679c33 | |||
| fa334c4bdf | |||
| d73b650c26 | |||
| c9980fd367 | |||
| 42f75a4ca9 | |||
| c4159ef76d | |||
| ab885f07b8 | |||
| 4950ce485f | |||
| c8b0a97b1c | |||
| 95a447de9c | |||
| d98f2faef9 | |||
| b855e66170 | |||
| 0d3f99e85a | |||
| 28df6cedfe | |||
| dd6aa91d72 | |||
| b44a22a9d4 | |||
| bdf92ee84e | |||
| f217181293 | |||
| ccb5d19c25 | |||
| b780f0f49b | |||
| 2141626269 | |||
| 63674ba081 | |||
| 9722dbe18a | |||
| 4698352b20 | |||
| eb8625f774 | |||
| 9343403358 | |||
| 4a3a418156 | |||
| 6dfba5fda8 | |||
| d25008d2c8 | |||
| 4eb5fc541b | |||
| 42acdad9e5 | |||
| 84f9f7cd60 | |||
| 79216d356c | |||
| 9429c843c8 | |||
| 6bcba91fbe | |||
| ab101d75d0 | |||
| 7512ea1a64 | |||
| 902ec37062 | |||
| bed05f2450 | |||
| fdd871e177 | |||
| 94c28a2574 | |||
| 42386a7272 | |||
| 5e858a15f7 | |||
| 533d1afb4b | |||
| 9f8d3611eb | |||
| 3177ee8010 | |||
| 7a7c5f00c0 | |||
| fee0b38b48 | |||
| d5ae3a4966 | |||
| 31ab737bf2 | |||
| a4bdf249db | |||
| 006dc1792f | |||
| a63cb3e3fd | |||
| 2b22d2e6ea | |||
| a524bcfe78 | |||
| 91b03dccb0 | |||
| 6000855c82 | |||
| 38677aaa58 | |||
| d49f762f6d | |||
| 81a9e125b5 | |||
| 70c788ce0c | |||
| 1c443beb9c | |||
| 269b1e9aa3 | |||
| 6d0350d04e | |||
| 15647bdfb7 | |||
| 2663dd176d | |||
| 6706c9225a | |||
| 5137859e47 | |||
| b8e7453fef | |||
| f93dab755b | |||
| 0c8763a728 | |||
| f5b4f268dc | |||
| ef5f29cfb2 | |||
| 8947ae0cc1 | |||
| 878ae0002a | |||
| 37da91cfe7 | |||
| b79f86f256 | |||
| 613aecb898 | |||
| 39db06d9c4 | |||
| f064889a4f | |||
| 3439933235 | |||
| 1b6b422c63 | |||
| 2265db9028 | |||
| bf54615efc | |||
| da6a8cfc86 | |||
| 9cd6f35e9d | |||
| 210d0cf7f1 | |||
| 5a4a1421de | |||
| 34a25dd558 | |||
| 9e576c76e7 | |||
| c24a3e389f | |||
| f976451d19 | |||
| 869fbac632 | |||
| 284fb3a98c | |||
| bc00d840e8 | |||
| be9b6e7b57 | |||
| 2fd98cb040 | |||
| 67d32e6779 | |||
| 9d54f655aa | |||
| 65195a726d | |||
| b84cb05848 | |||
| a969872850 | |||
| aaacab1bc3 | |||
| d22f64e6d4 | |||
| 22995e5655 | |||
| 043eb1d9e5 | |||
| fec7fa8bfd | |||
| 1a20fe330e | |||
| 1f0c061ce3 | |||
| ff5b4639d5 | |||
| f9d93ead4e | |||
| 8ae0d6a509 | |||
| 48b5a80320 | |||
| ad3d408067 | |||
| e40bbecb16 | |||
| 8eba582efe | |||
| fbea3374e9 | |||
| 2eb3593327 | |||
| 1136e2cfee | |||
| 5859cd8dad | |||
| 43961b542b | |||
| 2d056fbe66 | |||
| 545f28008e | |||
| d42529348f | |||
| 27ecc7f384 | |||
| 402f423693 | |||
| 3eae6d43b6 | |||
| 59a5d0db28 | |||
| f976aa7443 | |||
| 6621406fa8 | |||
| 27ff6aeccb | |||
| a8dc73b4d9 | |||
| 86e2d1b0a4 | |||
| 859b5d7ea3 |
@@ -0,0 +1,126 @@
|
||||
# Used as inspiration: https://github.com/mvdan/github-actions-golang
|
||||
|
||||
name: Cross-Platform
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
go-version: [ 1.14.x ]
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||
SUCCESS: 0
|
||||
|
||||
- os: macos-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||
SUCCESS: 0
|
||||
|
||||
- os: windows-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
|
||||
SUCCESS: 'True'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# These tools would be useful if we later decide to reinvestigate
|
||||
# publishing test/coverage reports to some tool for easier consumption
|
||||
# - name: Install test and coverage analysis tools
|
||||
# run: |
|
||||
# go get github.com/axw/gocov/gocov
|
||||
# go get github.com/AlekSi/gocov-xml
|
||||
# go get -u github.com/jstemmer/go-junit-report
|
||||
# echo "::add-path::$(go env GOPATH)/bin"
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
# Calculate the short SHA1 hash of the git commit
|
||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
# mkdir test-results
|
||||
|
||||
- name: Build Caddy
|
||||
working-directory: ./cmd/caddy
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
go build -trimpath -a -ldflags="-w -s" -v
|
||||
|
||||
- name: Publish Build Artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: caddy_v2_${{ runner.os }}_${{ steps.vars.outputs.short_sha }}
|
||||
path: ${{ matrix.CADDY_BIN_PATH }}
|
||||
|
||||
# Commented bits below were useful to allow the job to continue
|
||||
# even if the tests fail, so we can publish the report separately
|
||||
# For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status
|
||||
- name: Run tests
|
||||
# id: step_test
|
||||
# continue-on-error: true
|
||||
run: |
|
||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
# echo "::set-output name=status::$?"
|
||||
|
||||
# Relevant step if we reinvestigate publishing test/coverage reports
|
||||
# - name: Prepare coverage reports
|
||||
# run: |
|
||||
# mkdir coverage
|
||||
# gocov convert cover-profile.out > coverage/coverage.json
|
||||
# # Because Windows doesn't work with input redirection like *nix, but output redirection works.
|
||||
# (cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
|
||||
|
||||
# To return the correct result even though we set 'continue-on-error: true'
|
||||
# - name: Coerce correct build result
|
||||
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
|
||||
# run: |
|
||||
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
||||
# exit 1
|
||||
|
||||
# From https://github.com/reviewdog/action-golangci-lint
|
||||
golangci-lint:
|
||||
name: runner / golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: reviewdog/action-golangci-lint@v1
|
||||
# uses: docker://reviewdog/action-golangci-lint:v1 # pre-build docker image
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Fuzzing
|
||||
|
||||
on:
|
||||
# Daily midnight fuzzing
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
fuzzing:
|
||||
name: Fuzzing
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
go-version: [ 1.14.x ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
|
||||
# If we decide we need to prevent this from running on forks, we can use this line:
|
||||
# if: github.repository == 'caddyserver/caddy'
|
||||
run: |
|
||||
|
||||
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
|
||||
chmod a+x fuzzit
|
||||
mv fuzzit $(go env GOPATH)/bin
|
||||
echo "::add-path::$(go env GOPATH)/bin"
|
||||
|
||||
- name: Generate fuzzers & submit them to Fuzzit
|
||||
continue-on-error: true
|
||||
env:
|
||||
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
|
||||
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
|
||||
BUILD_SOURCEVERSION: ${{ github.sha }}
|
||||
run: |
|
||||
# debug
|
||||
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
|
||||
echo "Source version: $BUILD_SOURCEVERSION"
|
||||
|
||||
declare -A fuzzers_funcs=(\
|
||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
|
||||
["./caddyconfig/caddyfile/parse_fuzz.go"]="FuzzParseCaddyfile" \
|
||||
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
|
||||
["./replacer_fuzz.go"]="FuzzReplacer" \
|
||||
)
|
||||
|
||||
declare -A fuzzers_targets=(\
|
||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
|
||||
["./caddyconfig/caddyfile/parse_fuzz.go"]="parse-caddyfile" \
|
||||
["./listeners_fuzz.go"]="parse-network-address" \
|
||||
["./replacer_fuzz.go"]="replacer" \
|
||||
)
|
||||
|
||||
fuzz_type="fuzzing"
|
||||
|
||||
for f in $(find . -name \*_fuzz.go); do
|
||||
FUZZER_DIRECTORY=$(dirname "$f")
|
||||
|
||||
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
|
||||
|
||||
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
|
||||
|
||||
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
|
||||
|
||||
echo "Completed $f"
|
||||
done
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
go-version: [ 1.14.x ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# So GoReleaser can generate the changelog properly
|
||||
- name: Unshallowify the repo clone
|
||||
run: git fetch --prune --unshallow
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
|
||||
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
|
||||
|
||||
# GoReleaser will take care of publishing those artifacts into the release
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.get_version.outputs.version_tag }}
|
||||
+20
-13
@@ -1,16 +1,23 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
_gitignore/
|
||||
Vagrantfile
|
||||
.vagrant/
|
||||
|
||||
dist/builds/
|
||||
dist/release/
|
||||
|
||||
error.log
|
||||
access.log
|
||||
|
||||
/*.conf
|
||||
*.log
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
og_static/
|
||||
# artifacts from pprof tooling
|
||||
*.prof
|
||||
*.test
|
||||
|
||||
# build artifacts
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
|
||||
# mac specific
|
||||
.DS_Store
|
||||
|
||||
# go modules
|
||||
vendor
|
||||
|
||||
# goreleaser artifacts
|
||||
dist
|
||||
caddy-build
|
||||
caddy-dist
|
||||
@@ -0,0 +1,49 @@
|
||||
linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule
|
||||
ignoretests: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- ineffassign
|
||||
- misspell
|
||||
|
||||
run:
|
||||
# default concurrency is a available CPU number.
|
||||
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
||||
deadline: 5m
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
format: 'colored-line-number'
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# we aren't calling unknown URL
|
||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
||||
linters:
|
||||
- gosec
|
||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||
- text: "G203" # G203: Use of unescaped data in HTML templates
|
||||
linters:
|
||||
- gosec
|
||||
# we're shelling out to known commands, not relying on user-defined input.
|
||||
- text: "G204" # G204: Audit use of command execution
|
||||
linters:
|
||||
- gosec
|
||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
@@ -0,0 +1,67 @@
|
||||
before:
|
||||
hooks:
|
||||
- mkdir -p caddy-build
|
||||
- cp cmd/caddy/main.go caddy-build/main.go
|
||||
- cp ./go.mod caddy-build/go.mod
|
||||
- sed -i.bkp s/github.com\/caddyserver\/caddy\/v2/caddy/g ./caddy-build/go.mod
|
||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
||||
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
||||
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
||||
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
|
||||
- go mod download
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
- GO111MODULE=on
|
||||
main: main.go
|
||||
dir: ./caddy-build
|
||||
binary: caddy
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
archives:
|
||||
- format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
replacements:
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
algorithm: sha512
|
||||
release:
|
||||
github:
|
||||
owner: caddyserver
|
||||
name: caddy
|
||||
draft: true
|
||||
prerelease: auto
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^ci:'
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^chore:'
|
||||
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
- tip
|
||||
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
install:
|
||||
- go get -t ./...
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
@@ -0,0 +1,10 @@
|
||||
# This is the official list of Caddy Authors for copyright purposes.
|
||||
# Authors may be either individual people or legal entities.
|
||||
#
|
||||
# Not all individual contributors are authors. For the full list of
|
||||
# contributors, refer to the project's page on GitHub or the repo's
|
||||
# commit history.
|
||||
|
||||
Matthew Holt <Matthew.Holt@gmail.com>
|
||||
Light Code Labs <sales@lightcodelabs.com>
|
||||
Ardan Labs <info@ardanlabs.com>
|
||||
@@ -1,46 +0,0 @@
|
||||
## Contributing to Caddy
|
||||
|
||||
**[Join our dev chat on Gitter](https://gitter.im/mholt/caddy)** to chat with
|
||||
other Caddy developers! (Dev chat only; try our
|
||||
[support room](https://gitter.im/caddyserver/support) for help or
|
||||
[general](https://gitter.im/caddyserver/general) for anything else.)
|
||||
|
||||
This project gladly accepts contributions and we encourage interested users to
|
||||
get involved!
|
||||
|
||||
|
||||
#### For small tweaks, bug fixes, and tests
|
||||
|
||||
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time.
|
||||
Bug fixes should be under test to assert correct behavior. Thank you for
|
||||
helping out in simple ways!
|
||||
|
||||
|
||||
#### Ideas, questions, bug reports
|
||||
|
||||
Feel free to [open an issue](https://github.com/mholt/caddy/issues) with your
|
||||
ideas, questions, and bug reports, if one does not already exist for it. Bug
|
||||
reports should state expected behavior and contain clear instructions for
|
||||
isolating and reproducing the problem.
|
||||
See [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||
|
||||
|
||||
#### New features
|
||||
|
||||
Before submitting a pull request, please open an issue first to discuss it and
|
||||
claim it. This prevents overlapping efforts and keeps the project in-line with
|
||||
its goals. If you prefer to discuss the feature privately, you can reach other
|
||||
developers on Gitter or you may email me directly. (My email address is below.)
|
||||
|
||||
And don't forget to write tests for new features!
|
||||
|
||||
|
||||
#### Vulnerabilities
|
||||
|
||||
If you've found a vulnerability that is serious, please email me: Matthew dot
|
||||
Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
|
||||
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! Caddy would not be what it is today without your contributions.
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -178,7 +179,7 @@
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
@@ -186,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -1,172 +1,152 @@
|
||||
[](https://caddyserver.com)
|
||||
|
||||
[](https://gitter.im/mholt/caddy)
|
||||
[](https://godoc.org/github.com/mholt/caddy)
|
||||
[](https://travis-ci.org/mholt/caddy)
|
||||
[](https://ci.appveyor.com/project/mholt/caddy)
|
||||
|
||||
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD
|
||||
and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android).
|
||||
It is a capable alternative to other popular and easy to use web servers.
|
||||
([@caddyserver](https://twitter.com/caddyserver) on Twitter)
|
||||
|
||||
The most notable features are HTTP/2, [Let's Encrypt](https://letsencrypt.org)
|
||||
support, Virtual Hosts, TLS + SNI, and easy configuration with a
|
||||
[Caddyfile](https://caddyserver.com/docs/caddyfile). In development, you usually
|
||||
put one Caddyfile with each site. In production, Caddy serves HTTPS by default
|
||||
and manages all cryptographic assets for you.
|
||||
|
||||
[Download](https://github.com/mholt/caddy/releases) ·
|
||||
[User Guide](https://caddyserver.com/docs)
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
|
||||
</p>
|
||||
<h3 align="center">Every site on HTTPS</h3>
|
||||
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
|
||||
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
||||
<a href="https://app.fuzzit.dev/orgs/caddyserver-gh/dashboard"><img src="https://app.fuzzit.dev/badge?org_id=caddyserver-gh"></a>
|
||||
<br>
|
||||
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
|
||||
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/caddyserver/caddy/releases">Download</a> ·
|
||||
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
||||
<a href="https://caddy.community">Community</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
### Menu
|
||||
|
||||
- [Getting Caddy](#getting-caddy)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Running from Source](#running-from-source)
|
||||
- [Contributing](#contributing)
|
||||
- [About the Project](#about-the-project)
|
||||
- [Features](#features)
|
||||
- [Build from source](#build-from-source)
|
||||
- [For development](#for-development)
|
||||
- [With version information and/or plugins](#with-version-information-andor-plugins)
|
||||
- [Getting started](#getting-started)
|
||||
- [Overview](#overview)
|
||||
- [Full documentation](#full-documentation)
|
||||
- [Getting help](#getting-help)
|
||||
- [About](#about)
|
||||
|
||||
<p align="center">
|
||||
<b>Powered by</b>
|
||||
<br>
|
||||
<a href="https://github.com/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
|
||||
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
|
||||
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/api)
|
||||
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
|
||||
- **Automatic HTTPS** by default
|
||||
- [Let's Encrypt](https://letsencrypt.org) for public sites
|
||||
- Fully-managed local CA for internal names & IPs
|
||||
- Can coordinate with other Caddy instances in a cluster
|
||||
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
||||
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
|
||||
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/extending-caddy) lets Caddy do anything without bloat
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
- Written in Go, a language with higher **memory safety guarantees** than other servers
|
||||
- Actually **fun to use**
|
||||
- So, so much more to discover
|
||||
|
||||
|
||||
|
||||
## Build from source
|
||||
|
||||
## Getting Caddy
|
||||
Requirements:
|
||||
|
||||
Caddy binaries have no dependencies and are available for nearly every platform.
|
||||
- [Go 1.14 or newer](https://golang.org/dl/)
|
||||
- Do NOT disable [Go modules](https://github.com/golang/go/wiki/Modules) (`export GO111MODULE=on`)
|
||||
|
||||
[Latest release](https://github.com/mholt/caddy/releases/latest)
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
The website has [full documentation](https://caddyserver.com/docs) but this will
|
||||
get you started in about 30 seconds:
|
||||
|
||||
Place a file named "Caddyfile" with your site. Paste this into it and save:
|
||||
|
||||
```
|
||||
localhost
|
||||
|
||||
gzip
|
||||
browse
|
||||
ext .html
|
||||
websocket /echo cat
|
||||
log ../access.log
|
||||
header /api Access-Control-Allow-Origin *
|
||||
### For development
|
||||
|
||||
```bash
|
||||
$ git clone "https://github.com/caddyserver/caddy.git"
|
||||
$ cd caddy/cmd/caddy/
|
||||
$ go build
|
||||
```
|
||||
|
||||
Run `caddy` from that directory, and it will automatically use that Caddyfile to
|
||||
configure itself.
|
||||
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
|
||||
|
||||
That simple file enables compression, allows directory browsing (for folders
|
||||
without an index file), serves clean URLs, hosts a WebSocket echo server at
|
||||
/echo, logs requests to access.log, and adds the coveted
|
||||
`Access-Control-Allow-Origin: *` header for all responses from some API.
|
||||
### With version information and/or plugins
|
||||
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
|
||||
#### Defining multiple sites
|
||||
|
||||
You can run multiple sites from the same Caddyfile, too:
|
||||
Using [our builder tool](https://github.com/caddyserver/xcaddy)...
|
||||
|
||||
```
|
||||
site1.com {
|
||||
# ...
|
||||
}
|
||||
|
||||
site2.com, sub.site2.com {
|
||||
# ...
|
||||
}
|
||||
$ xcaddy --version CADDY_VERSION
|
||||
```
|
||||
|
||||
Note that all these sites will automatically be served over HTTPS using Let's
|
||||
Encrypt as the CA. Caddy will manage the certificates (including renewals) for
|
||||
you. You don't even have to think about it.
|
||||
...the following steps are automated:
|
||||
|
||||
For more documentation, please view [the website](https://caddyserver.com/docs).
|
||||
You may also be interested in the [developer guide]
|
||||
(https://github.com/mholt/caddy/wiki) on this project's GitHub wiki.
|
||||
1. Create a new folder: `mkdir caddy`
|
||||
2. Change into it: `cd caddy`
|
||||
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
|
||||
4. Initialize a Go module: `go mod init caddy`
|
||||
5. Pin Caddy version: `go get github.com/caddyserver/caddy/v2@TAG` replacing `TAG` with a git tag or commit. You can also pin any plugin versions similarly.
|
||||
6. Compile: `go build`
|
||||
|
||||
|
||||
|
||||
|
||||
## Running from Source
|
||||
## Quick start
|
||||
|
||||
Note: You will need **[Go 1.6](https://golang.org/dl/)** or newer.
|
||||
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
|
||||
|
||||
1. `$ go get github.com/mholt/caddy`
|
||||
2. `cd` into your website's directory
|
||||
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
|
||||
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
|
||||
|
||||
If you're tinkering, you can also use `go run main.go`.
|
||||
|
||||
By default, Caddy serves the current directory at
|
||||
[localhost:2015](http://localhost:2015). You can place a Caddyfile to configure
|
||||
Caddy for serving your site.
|
||||
|
||||
Caddy accepts some flags from the command line. Run `caddy -h` to view the help
|
||||
for flags. You can also pipe a Caddyfile into the caddy command.
|
||||
|
||||
**Running as root:** We advise against this; use setcap instead, like so:
|
||||
`setcap cap_net_bind_service=+ep ./caddy` This will allow you to listen on
|
||||
ports < 1024 like 80 and 443.
|
||||
|
||||
|
||||
|
||||
#### Docker Container
|
||||
|
||||
Caddy is available as a Docker container from any of these sources:
|
||||
|
||||
- [abiosoft/caddy](https://hub.docker.com/r/abiosoft/caddy/)
|
||||
- [darron/caddy](https://hub.docker.com/r/darron/caddy/)
|
||||
- [joshix/caddy](https://hub.docker.com/r/joshix/caddy/)
|
||||
- [jumanjiman/caddy](https://hub.docker.com/r/jumanjiman/caddy/)
|
||||
- [zenithar/nano-caddy](https://hub.docker.com/r/zenithar/nano-caddy/)
|
||||
|
||||
|
||||
|
||||
#### 3rd-party dependencies
|
||||
|
||||
Although Caddy's binaries are completely static, Caddy relies on some excellent
|
||||
libraries. [Godoc.org](https://godoc.org/github.com/mholt/caddy) shows the
|
||||
packages that each Caddy package imports.
|
||||
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
|
||||
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
## Overview
|
||||
|
||||
**[Join our dev chat on Gitter](https://gitter.im/mholt/caddy)** to chat with
|
||||
other Caddy developers! (Dev chat only; try our
|
||||
[support room](https://gitter.im/caddyserver/support) for help or
|
||||
[general](https://gitter.im/caddyserver/general) for anything else.)
|
||||
Caddy is most often used as an HTTPS server, but it is suitable for any long-running Go program. First and foremost, it is a platform to run Go applications. Caddy "apps" are just Go programs that are implemented as Caddy modules. Two apps -- `tls` and `http` -- ship standard with Caddy.
|
||||
|
||||
This project would not be what it is without your help. Please see the
|
||||
[contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md)
|
||||
if you haven't already.
|
||||
Caddy apps instantly benefit from [automated documentation](https://caddyserver.com/docs/json/), graceful on-line [config changes via API](https://caddyserver.com/docs/api), and unification with other Caddy apps.
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
Although [JSON](https://caddyserver.com/docs/json/) is Caddy's native config language, Caddy can accept input from [config adapters](https://caddyserver.com/docs/config-adapters) which can essentially convert any config format of your choice into JSON: Caddyfile, JSON 5, YAML, TOML, NGINX config, and more.
|
||||
|
||||
Special thanks to
|
||||
[](https://www.digitalocean.com)
|
||||
for hosting the Caddy project.
|
||||
The primary way to configure Caddy is through [its API](https://caddyserver.com/docs/api), but if you prefer config files, the [command-line interface](https://caddyserver.com/docs/command-line) supports those too.
|
||||
|
||||
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
|
||||
|
||||
To wield the power of this design, you need to know how the config document is structured. Please see the [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
|
||||
|
||||
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
|
||||
|
||||
|
||||
## Full documentation
|
||||
|
||||
Our website has complete documentation:
|
||||
|
||||
**https://caddyserver.com/docs/**
|
||||
|
||||
The docs are also open source. You can contribute to them here: https://github.com/caddyserver/website
|
||||
|
||||
|
||||
|
||||
## Getting help
|
||||
|
||||
## About the project
|
||||
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
|
||||
|
||||
Caddy was born out of the need for a "batteries-included" web server that runs
|
||||
anywhere and doesn't have to take its configuration with it. Caddy took
|
||||
inspiration from [spark](https://github.com/rif/spark),
|
||||
[nginx](https://github.com/nginx/nginx), lighttpd,
|
||||
[Websocketd](https://github.com/joewalnes/websocketd)
|
||||
and [Vagrant](https://www.vagrantup.com/),
|
||||
which provides a pleasant mixture of features from each of them.
|
||||
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
|
||||
|
||||
Please use our [issue tracker](/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
|
||||
|
||||
|
||||
*Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
|
||||
## About
|
||||
|
||||
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Light Code Labs, LLC.
|
||||
|
||||
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
||||
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
|
||||
|
||||
@@ -0,0 +1,792 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
|
||||
|
||||
// AdminConfig configures Caddy's API endpoint, which is used
|
||||
// to manage Caddy while it is running.
|
||||
type AdminConfig struct {
|
||||
// If true, the admin endpoint will be completely disabled.
|
||||
// Note that this makes any runtime changes to the config
|
||||
// impossible, since the interface to do so is through the
|
||||
// admin endpoint.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
|
||||
// The address to which the admin endpoint's listener should
|
||||
// bind itself. Can be any single network address that can be
|
||||
// parsed by Caddy. Default: localhost:2019
|
||||
Listen string `json:"listen,omitempty"`
|
||||
|
||||
// If true, CORS headers will be emitted, and requests to the
|
||||
// API will be rejected if their `Host` and `Origin` headers
|
||||
// do not match the expected value(s). Use `origins` to
|
||||
// customize which origins/hosts are allowed.If `origins` is
|
||||
// not set, the listen address is the only value allowed by
|
||||
// default.
|
||||
EnforceOrigin bool `json:"enforce_origin,omitempty"`
|
||||
|
||||
// The list of allowed origins for API requests. Only used if
|
||||
// `enforce_origin` is true. If not set, the listener address
|
||||
// will be the default value. If set but empty, no origins will
|
||||
// be allowed.
|
||||
Origins []string `json:"origins,omitempty"`
|
||||
|
||||
// Options related to configuration management.
|
||||
Config *ConfigSettings `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigSettings configures the, uh, configuration... and
|
||||
// management thereof.
|
||||
type ConfigSettings struct {
|
||||
// Whether to keep a copy of the active config on disk. Default is true.
|
||||
Persist *bool `json:"persist,omitempty"`
|
||||
}
|
||||
|
||||
// listenAddr extracts a singular listen address from ac.Listen,
|
||||
// returning the network and the address of the listener.
|
||||
func (admin AdminConfig) listenAddr() (string, string, error) {
|
||||
input := admin.Listen
|
||||
if input == "" {
|
||||
input = DefaultAdminListen
|
||||
}
|
||||
listenAddr, err := ParseNetworkAddress(input)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("parsing admin listener address: %v", err)
|
||||
}
|
||||
if listenAddr.PortRangeSize() != 1 {
|
||||
return "", "", fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
|
||||
}
|
||||
return listenAddr.Network, listenAddr.JoinHostPort(0), nil
|
||||
}
|
||||
|
||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||
func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
|
||||
muxWrap := adminHandler{
|
||||
enforceOrigin: admin.EnforceOrigin,
|
||||
allowedOrigins: admin.allowedOrigins(listenAddr),
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
// addRoute just calls muxWrap.mux.Handle after
|
||||
// wrapping the handler with error handling
|
||||
addRoute := func(pattern string, h AdminHandler) {
|
||||
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.ServeHTTP(w, r)
|
||||
muxWrap.handleError(w, r, err)
|
||||
})
|
||||
muxWrap.mux.Handle(pattern, wrapper)
|
||||
}
|
||||
|
||||
// register standard config control endpoints
|
||||
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
|
||||
addRoute("/id/", AdminHandlerFunc(handleConfigID))
|
||||
addRoute("/stop", AdminHandlerFunc(handleStop))
|
||||
|
||||
// register debugging endpoints
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
muxWrap.mux.Handle("/debug/vars", expvar.Handler())
|
||||
|
||||
// register third-party module endpoints
|
||||
for _, m := range GetModules("admin.api") {
|
||||
router := m.New().(AdminRouter)
|
||||
for _, route := range router.Routes() {
|
||||
addRoute(route.Pattern, route.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
return muxWrap
|
||||
}
|
||||
|
||||
// allowedOrigins returns a list of origins that are allowed.
|
||||
// If admin.Origins is nil (null), the provided listen address
|
||||
// will be used as the default origin. If admin.Origins is
|
||||
// empty, no origins will be allowed, effectively bricking the
|
||||
// endpoint, but whatever.
|
||||
func (admin AdminConfig) allowedOrigins(listen string) []string {
|
||||
uniqueOrigins := make(map[string]struct{})
|
||||
for _, o := range admin.Origins {
|
||||
uniqueOrigins[o] = struct{}{}
|
||||
}
|
||||
if admin.Origins == nil {
|
||||
uniqueOrigins[listen] = struct{}{}
|
||||
}
|
||||
var allowed []string
|
||||
for origin := range uniqueOrigins {
|
||||
allowed = append(allowed, origin)
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
// replaceAdmin replaces the running admin server according
|
||||
// to the relevant configuration in cfg. If no configuration
|
||||
// for the admin endpoint exists in cfg, a default one is
|
||||
// used, so that there is always an admin server (unless it
|
||||
// is explicitly configured to be disabled).
|
||||
func replaceAdmin(cfg *Config) error {
|
||||
// always be sure to close down the old admin endpoint
|
||||
// as gracefully as possible, even if the new one is
|
||||
// disabled -- careful to use reference to the current
|
||||
// (old) admin endpoint since it will be different
|
||||
// when the function returns
|
||||
oldAdminServer := adminServer
|
||||
defer func() {
|
||||
// do the shutdown asynchronously so that any
|
||||
// current API request gets a response; this
|
||||
// goroutine may last a few seconds
|
||||
if oldAdminServer != nil {
|
||||
go func(oldAdminServer *http.Server) {
|
||||
err := stopAdminServer(oldAdminServer)
|
||||
if err != nil {
|
||||
Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
|
||||
}
|
||||
}(oldAdminServer)
|
||||
}
|
||||
}()
|
||||
|
||||
// always get a valid admin config
|
||||
adminConfig := DefaultAdminConfig
|
||||
if cfg != nil && cfg.Admin != nil {
|
||||
adminConfig = cfg.Admin
|
||||
}
|
||||
|
||||
// if new admin endpoint is to be disabled, we're done
|
||||
if adminConfig.Disabled {
|
||||
Log().Named("admin").Warn("admin endpoint disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract a singular listener address
|
||||
netw, addr, err := adminConfig.listenAddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := adminConfig.newAdminHandler(addr)
|
||||
|
||||
ln, err := Listen(netw, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminServer = &http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 1024 * 64,
|
||||
}
|
||||
|
||||
go adminServer.Serve(ln)
|
||||
|
||||
Log().Named("admin").Info(
|
||||
"admin endpoint started",
|
||||
zap.String("address", addr),
|
||||
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
||||
zap.Strings("origins", handler.allowedOrigins),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopAdminServer(srv *http.Server) error {
|
||||
if srv == nil {
|
||||
return fmt.Errorf("no admin server")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := srv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down admin server: %v", err)
|
||||
}
|
||||
Log().Named("admin").Info("stopped previous server")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminRouter is a type which can return routes for the admin API.
|
||||
type AdminRouter interface {
|
||||
Routes() []AdminRoute
|
||||
}
|
||||
|
||||
// AdminRoute represents a route for the admin endpoint.
|
||||
type AdminRoute struct {
|
||||
Pattern string
|
||||
Handler AdminHandler
|
||||
}
|
||||
|
||||
type adminHandler struct {
|
||||
enforceOrigin bool
|
||||
allowedOrigins []string
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// ServeHTTP is the external entry point for API requests.
|
||||
// It will only be called once per request.
|
||||
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Log().Named("admin.api").Info("received request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("uri", r.RequestURI),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Reflect("headers", r.Header),
|
||||
)
|
||||
h.serveHTTP(w, r)
|
||||
}
|
||||
|
||||
// serveHTTP is the internal entry point for API requests. It may
|
||||
// be called more than once per request, for example if a request
|
||||
// is rewritten (i.e. internal redirect).
|
||||
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.enforceOrigin {
|
||||
// DNS rebinding mitigation
|
||||
err := h.checkHost(r)
|
||||
if err != nil {
|
||||
h.handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// cross-site mitigation
|
||||
origin, err := h.checkOrigin(r)
|
||||
if err != nil {
|
||||
h.handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// TODO: authentication & authorization, if configured
|
||||
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == ErrInternalRedir {
|
||||
h.serveHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
apiErr, ok := err.(APIError)
|
||||
if !ok {
|
||||
apiErr = APIError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if apiErr.Code == 0 {
|
||||
apiErr.Code = http.StatusInternalServerError
|
||||
}
|
||||
if apiErr.Message == "" && apiErr.Err != nil {
|
||||
apiErr.Message = apiErr.Err.Error()
|
||||
}
|
||||
|
||||
Log().Named("admin.api").Error("request error",
|
||||
zap.Error(err),
|
||||
zap.Int("status_code", apiErr.Code),
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(apiErr.Code)
|
||||
json.NewEncoder(w).Encode(apiErr)
|
||||
}
|
||||
|
||||
// checkHost returns a handler that wraps next such that
|
||||
// it will only be called if the request's Host header matches
|
||||
// a trustworthy/expected value. This helps to mitigate DNS
|
||||
// rebinding attacks.
|
||||
func (h adminHandler) checkHost(r *http.Request) error {
|
||||
var allowed bool
|
||||
for _, allowedHost := range h.allowedOrigins {
|
||||
if r.Host == allowedHost {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("host not allowed: %s", r.Host),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkOrigin ensures that the Origin header, if
|
||||
// set, matches the intended target; prevents arbitrary
|
||||
// sites from issuing requests to our listener. It
|
||||
// returns the origin that was obtained from r.
|
||||
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
||||
origin := h.getOriginHost(r)
|
||||
if origin == "" {
|
||||
return origin, APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("missing required Origin header"),
|
||||
}
|
||||
}
|
||||
if !h.originAllowed(origin) {
|
||||
return origin, APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
||||
}
|
||||
}
|
||||
return origin, nil
|
||||
}
|
||||
|
||||
func (h adminHandler) getOriginHost(r *http.Request) string {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = r.Header.Get("Referer")
|
||||
}
|
||||
originURL, err := url.Parse(origin)
|
||||
if err == nil && originURL.Host != "" {
|
||||
origin = originURL.Host
|
||||
}
|
||||
return origin
|
||||
}
|
||||
|
||||
func (h adminHandler) originAllowed(origin string) bool {
|
||||
for _, allowedOrigin := range h.allowedOrigins {
|
||||
originCopy := origin
|
||||
if !strings.Contains(allowedOrigin, "://") {
|
||||
// no scheme specified, so allow both
|
||||
originCopy = strings.TrimPrefix(originCopy, "http://")
|
||||
originCopy = strings.TrimPrefix(originCopy, "https://")
|
||||
}
|
||||
if originCopy == allowedOrigin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := readConfig(r.URL.Path, w)
|
||||
if err != nil {
|
||||
return APIError{Code: http.StatusBadRequest, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete:
|
||||
|
||||
// DELETE does not use a body, but the others do
|
||||
var body []byte
|
||||
if r.Method != http.MethodDelete {
|
||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
|
||||
}
|
||||
}
|
||||
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer bufPool.Put(buf)
|
||||
|
||||
_, err := io.Copy(buf, r.Body)
|
||||
if err != nil {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("reading request body: %v", err),
|
||||
}
|
||||
}
|
||||
body = buf.Bytes()
|
||||
}
|
||||
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method %s not allowed", r.Method),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||
idPath := r.URL.Path
|
||||
|
||||
parts := strings.Split(idPath, "/")
|
||||
if len(parts) < 3 || parts[2] == "" {
|
||||
return fmt.Errorf("request path is missing object ID")
|
||||
}
|
||||
if parts[0] != "" || parts[1] != "id" {
|
||||
return fmt.Errorf("malformed object path")
|
||||
}
|
||||
id := parts[2]
|
||||
|
||||
// map the ID to the expanded path
|
||||
currentCfgMu.RLock()
|
||||
expanded, ok := rawCfgIndex[id]
|
||||
defer currentCfgMu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown object ID '%s'", id)
|
||||
}
|
||||
|
||||
// piece the full URL path back together
|
||||
parts = append([]string{expanded}, parts[3:]...)
|
||||
r.URL.Path = path.Join(parts...)
|
||||
|
||||
return ErrInternalRedir
|
||||
}
|
||||
|
||||
func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||
err := handleUnload(w, r)
|
||||
if err != nil {
|
||||
Log().Named("admin.api").Error("unload error", zap.Error(err))
|
||||
}
|
||||
go func() {
|
||||
err := stopAdminServer(adminServer)
|
||||
var exitCode int
|
||||
if err != nil {
|
||||
exitCode = ExitCodeFailedQuit
|
||||
Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
|
||||
}
|
||||
Log().Named("admin.api").Info("stopping now, bye!! 👋")
|
||||
os.Exit(exitCode)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUnload stops the current configuration that is running.
|
||||
// Note that doing this can also be accomplished with DELETE /config/
|
||||
// but we leave this function because handleStop uses it.
|
||||
func handleUnload(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodPost {
|
||||
return APIError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method not allowed"),
|
||||
}
|
||||
}
|
||||
currentCfgMu.RLock()
|
||||
hasCfg := currentCfg != nil
|
||||
currentCfgMu.RUnlock()
|
||||
if !hasCfg {
|
||||
Log().Named("admin.api").Info("nothing to unload")
|
||||
return nil
|
||||
}
|
||||
Log().Named("admin.api").Info("unloading")
|
||||
if err := stopAndCleanup(); err != nil {
|
||||
Log().Named("admin.api").Error("error unloading", zap.Error(err))
|
||||
} else {
|
||||
Log().Named("admin.api").Info("unloading completed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsyncedConfigAccess traverses into the current config and performs
|
||||
// the operation at path according to method, using body and out as
|
||||
// needed. This is a low-level, unsynchronized function; most callers
|
||||
// will want to use changeConfig or readConfig instead. This requires a
|
||||
// read or write lock on currentCfgMu, depending on method (GET needs
|
||||
// only a read lock; all others need a write lock).
|
||||
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
||||
var err error
|
||||
var val interface{}
|
||||
|
||||
// if there is a request body, decode it into the
|
||||
// variable that will be set in the config according
|
||||
// to method and path
|
||||
if len(body) > 0 {
|
||||
err = json.Unmarshal(body, &val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding request body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(out)
|
||||
|
||||
cleanPath := strings.Trim(path, "/")
|
||||
if cleanPath == "" {
|
||||
return fmt.Errorf("no traversable path")
|
||||
}
|
||||
|
||||
parts := strings.Split(cleanPath, "/")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("path missing")
|
||||
}
|
||||
|
||||
// A path that ends with "..." implies:
|
||||
// 1) the part before it is an array
|
||||
// 2) the payload is an array
|
||||
// and means that the user wants to expand the elements
|
||||
// in the payload array and append each one into the
|
||||
// destination array, like so:
|
||||
// array = append(array, elems...)
|
||||
// This special case is handled below.
|
||||
ellipses := parts[len(parts)-1] == "..."
|
||||
if ellipses {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
|
||||
var ptr interface{} = rawCfg
|
||||
|
||||
traverseLoop:
|
||||
for i, part := range parts {
|
||||
switch v := ptr.(type) {
|
||||
case map[string]interface{}:
|
||||
// if the next part enters a slice, and the slice is our destination,
|
||||
// handle it specially (because appending to the slice copies the slice
|
||||
// header, which does not replace the original one like we want)
|
||||
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
|
||||
var idx int
|
||||
if method != http.MethodPost {
|
||||
idxStr := parts[len(parts)-1]
|
||||
idx, err = strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
||||
path, idxStr, err)
|
||||
}
|
||||
if idx < 0 || idx >= len(arr) {
|
||||
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
||||
}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
err = enc.Encode(arr[idx])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
if ellipses {
|
||||
valArray, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("final element is not an array")
|
||||
}
|
||||
v[part] = append(arr, valArray...)
|
||||
} else {
|
||||
v[part] = append(arr, val)
|
||||
}
|
||||
case http.MethodPut:
|
||||
// avoid creation of new slice and a second copy (see
|
||||
// https://github.com/golang/go/wiki/SliceTricks#insert)
|
||||
arr = append(arr, nil)
|
||||
copy(arr[idx+1:], arr[idx:])
|
||||
arr[idx] = val
|
||||
v[part] = arr
|
||||
case http.MethodPatch:
|
||||
arr[idx] = val
|
||||
case http.MethodDelete:
|
||||
v[part] = append(arr[:idx], arr[idx+1:]...)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
}
|
||||
break traverseLoop
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
err = enc.Encode(v[part])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
// if the part is an existing list, POST appends to
|
||||
// it, otherwise it just sets or creates the value
|
||||
if arr, ok := v[part].([]interface{}); ok {
|
||||
if ellipses {
|
||||
valArray, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("final element is not an array")
|
||||
}
|
||||
v[part] = append(arr, valArray...)
|
||||
} else {
|
||||
v[part] = append(arr, val)
|
||||
}
|
||||
} else {
|
||||
v[part] = val
|
||||
}
|
||||
case http.MethodPut:
|
||||
if _, ok := v[part]; ok {
|
||||
return fmt.Errorf("[%s] key already exists: %s", path, part)
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodPatch:
|
||||
if _, ok := v[part]; !ok {
|
||||
return fmt.Errorf("[%s] key does not exist: %s", path, part)
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodDelete:
|
||||
delete(v, part)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
}
|
||||
} else {
|
||||
// if we are "PUTting" a new resource, the key(s) in its path
|
||||
// might not exist yet; that's OK but we need to make them as
|
||||
// we go, while we still have a pointer from the level above
|
||||
if v[part] == nil && method == http.MethodPut {
|
||||
v[part] = make(map[string]interface{})
|
||||
}
|
||||
ptr = v[part]
|
||||
}
|
||||
|
||||
case []interface{}:
|
||||
partInt, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||
strings.Join(parts[:i+1], "/"), part, err)
|
||||
}
|
||||
if partInt < 0 || partInt >= len(v) {
|
||||
return fmt.Errorf("[/%s] array index out of bounds: %s",
|
||||
strings.Join(parts[:i+1], "/"), part)
|
||||
}
|
||||
ptr = v[partInt]
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveMetaFields removes meta fields like "@id" from a JSON message
|
||||
// by using a simple regular expression. (An alternate way to do this
|
||||
// would be to delete them from the raw, map[string]interface{}
|
||||
// representation as they are indexed, then iterate the index we made
|
||||
// and add them back after encoding as JSON, but this is simpler.)
|
||||
func RemoveMetaFields(rawJSON []byte) []byte {
|
||||
return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte {
|
||||
// matches with a comma on both sides (when "@id" property is
|
||||
// not the first or last in the object) need to keep exactly
|
||||
// one comma for correct JSON syntax
|
||||
comma := []byte{','}
|
||||
if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
|
||||
return comma
|
||||
}
|
||||
return []byte{}
|
||||
})
|
||||
}
|
||||
|
||||
// AdminHandler is like http.Handler except ServeHTTP may return an error.
|
||||
//
|
||||
// If any handler encounters an error, it should be returned for proper
|
||||
// handling.
|
||||
type AdminHandler interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request) error
|
||||
}
|
||||
|
||||
// AdminHandlerFunc is a convenience type like http.HandlerFunc.
|
||||
type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
// ServeHTTP implements the Handler interface.
|
||||
func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
return f(w, r)
|
||||
}
|
||||
|
||||
// APIError is a structured error that every API
|
||||
// handler should return for consistency in logging
|
||||
// and client responses. If Message is unset, then
|
||||
// Err.Error() will be serialized in its place.
|
||||
type APIError struct {
|
||||
Code int `json:"-"`
|
||||
Err error `json:"-"`
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultAdminListen is the address for the admin
|
||||
// listener, if none is specified at startup.
|
||||
DefaultAdminListen = "localhost:2019"
|
||||
|
||||
// ErrInternalRedir indicates an internal redirect
|
||||
// and is useful when admin API handlers rewrite
|
||||
// the request; in that case, authentication and
|
||||
// authorization needs to happen again for the
|
||||
// rewritten request.
|
||||
ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
|
||||
|
||||
// DefaultAdminConfig is the default configuration
|
||||
// for the administration endpoint.
|
||||
DefaultAdminConfig = &AdminConfig{
|
||||
Listen: DefaultAdminListen,
|
||||
}
|
||||
)
|
||||
|
||||
// idRegexp is used to match ID fields and their associated values
|
||||
// in the config. It also matches adjacent commas so that syntax
|
||||
// can be preserved no matter where in the object the field appears.
|
||||
// It supports string and most numeric values.
|
||||
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
|
||||
|
||||
const (
|
||||
rawConfigKey = "config"
|
||||
idKey = "@id"
|
||||
)
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
var adminServer *http.Server
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||
// each test is performed in sequence, so
|
||||
// each change builds on the previous ones;
|
||||
// the config is not reset between tests
|
||||
for i, tc := range []struct {
|
||||
method string
|
||||
path string // rawConfigKey will be prepended
|
||||
payload string
|
||||
expect string // JSON representation of what the whole config is expected to be after the request
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
method: "POST",
|
||||
path: "",
|
||||
payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
|
||||
expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/foo",
|
||||
payload: `"jet"`,
|
||||
expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/bar",
|
||||
payload: `{"aa": "bb", "qq": "zz"}`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/bar/qq",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/list",
|
||||
payload: `"e"`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
|
||||
},
|
||||
{
|
||||
method: "PUT",
|
||||
path: "/list/3",
|
||||
payload: `"d"`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/list/3",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
|
||||
},
|
||||
{
|
||||
method: "PATCH",
|
||||
path: "/list/3",
|
||||
payload: `"d"`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/list/...",
|
||||
payload: `["e", "f", "g"]`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
|
||||
},
|
||||
} {
|
||||
err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
|
||||
|
||||
if tc.shouldErr && err == nil {
|
||||
t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
|
||||
}
|
||||
if !tc.shouldErr && err != nil {
|
||||
t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
|
||||
}
|
||||
|
||||
// decode the expected config so we can do a convenient DeepEqual
|
||||
var expectedDecoded interface{}
|
||||
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
|
||||
}
|
||||
|
||||
// make sure the resulting config is as we expect it
|
||||
if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
|
||||
t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
|
||||
i, expectedDecoded, rawCfg[rawConfigKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cfg := []byte(`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"myserver": {
|
||||
"listen": ["tcp/localhost:8080-8084"],
|
||||
"read_timeout": "30s"
|
||||
},
|
||||
"yourserver": {
|
||||
"listen": ["127.0.0.1:5000"],
|
||||
"read_header_timeout": "15s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
Load(cfg, true)
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
CGO_ENABLED: 0
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.windows-amd64.zip
|
||||
- 7z x go1.6.windows-amd64.zip -y -oC:\ > NUL
|
||||
- go version
|
||||
- go env
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
- go get -t ./...
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
|
||||
deploy: off
|
||||
@@ -0,0 +1,570 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Config is the top (or beginning) of the Caddy configuration structure.
|
||||
// Caddy config is expressed natively as a JSON document. If you prefer
|
||||
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
|
||||
// available that can convert various inputs into Caddy JSON.
|
||||
//
|
||||
// Many parts of this config are extensible through the use of Caddy modules.
|
||||
// Fields which have a json.RawMessage type and which appear as dots (•••) in
|
||||
// the online docs can be fulfilled by modules in a certain module
|
||||
// namespace. The docs show which modules can be used in a given place.
|
||||
//
|
||||
// Whenever a module is used, its name must be given either inline as part of
|
||||
// the module, or as the key to the module's value. The docs will make it clear
|
||||
// which to use.
|
||||
//
|
||||
// Generally, all config settings are optional, as it is Caddy convention to
|
||||
// have good, documented default values. If a parameter is required, the docs
|
||||
// should say so.
|
||||
//
|
||||
// Go programs which are directly building a Config struct value should take
|
||||
// care to populate the JSON-encodable fields of the struct (i.e. the fields
|
||||
// with `json` struct tags) if employing the module lifecycle (e.g. Provision
|
||||
// method calls).
|
||||
type Config struct {
|
||||
Admin *AdminConfig `json:"admin,omitempty"`
|
||||
Logging *Logging `json:"logging,omitempty"`
|
||||
|
||||
// StorageRaw is a storage module that defines how/where Caddy
|
||||
// stores assets (such as TLS certificates). The default storage
|
||||
// module is `caddy.storage.file_system` (the local file system),
|
||||
// and the default path
|
||||
// [depends on the OS and environment](/docs/conventions#data-directory).
|
||||
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
||||
|
||||
// AppsRaw are the apps that Caddy will load and run. The
|
||||
// app module name is the key, and the app's config is the
|
||||
// associated value.
|
||||
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
||||
|
||||
apps map[string]App
|
||||
storage certmagic.Storage
|
||||
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
// App is a thing that Caddy runs.
|
||||
type App interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// Run runs the given config, replacing any existing config.
|
||||
func Run(cfg *Config) error {
|
||||
cfgJSON, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Load(cfgJSON, true)
|
||||
}
|
||||
|
||||
// Load loads the given config JSON and runs it only
|
||||
// if it is different from the current config or
|
||||
// forceReload is true.
|
||||
func Load(cfgJSON []byte, forceReload bool) error {
|
||||
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
||||
}
|
||||
|
||||
// changeConfig changes the current config (rawCfg) according to the
|
||||
// method, traversed via the given path, and uses the given input as
|
||||
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
||||
// If the resulting config is the same as the previous, no reload will
|
||||
// occur unless forceReload is true. This function is safe for
|
||||
// concurrent use.
|
||||
func changeConfig(method, path string, input []byte, forceReload bool) error {
|
||||
switch method {
|
||||
case http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodOptions,
|
||||
http.MethodConnect,
|
||||
http.MethodTrace:
|
||||
return fmt.Errorf("method not allowed")
|
||||
}
|
||||
|
||||
currentCfgMu.Lock()
|
||||
defer currentCfgMu.Unlock()
|
||||
|
||||
err := unsyncedConfigAccess(method, path, input, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// the mutation is complete, so encode the entire config as JSON
|
||||
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
|
||||
if err != nil {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("encoding new config: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// if nothing changed, no need to do a whole reload unless the client forces it
|
||||
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
|
||||
Log().Named("admin.api").Info("config is unchanged")
|
||||
return nil
|
||||
}
|
||||
|
||||
// find any IDs in this config and index them
|
||||
idx := make(map[string]string)
|
||||
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
||||
if err != nil {
|
||||
return APIError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Err: fmt.Errorf("indexing config: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// load this new config; if it fails, we need to revert to
|
||||
// our old representation of caddy's actual config
|
||||
err = unsyncedDecodeAndRun(newCfg)
|
||||
if err != nil {
|
||||
if len(rawCfgJSON) > 0 {
|
||||
// restore old config state to keep it consistent
|
||||
// with what caddy is still running; we need to
|
||||
// unmarshal it again because it's likely that
|
||||
// pointers deep in our rawCfg map were modified
|
||||
var oldCfg interface{}
|
||||
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
|
||||
if err2 != nil {
|
||||
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
||||
}
|
||||
rawCfg[rawConfigKey] = oldCfg
|
||||
}
|
||||
|
||||
return fmt.Errorf("loading new config: %v", err)
|
||||
}
|
||||
|
||||
// success, so update our stored copy of the encoded
|
||||
// config to keep it consistent with what caddy is now
|
||||
// running (storing an encoded copy is not strictly
|
||||
// necessary, but avoids an extra json.Marshal for
|
||||
// each config change)
|
||||
rawCfgJSON = newCfg
|
||||
rawCfgIndex = idx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readConfig traverses the current config to path
|
||||
// and writes its JSON encoding to out.
|
||||
func readConfig(path string, out io.Writer) error {
|
||||
currentCfgMu.RLock()
|
||||
defer currentCfgMu.RUnlock()
|
||||
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
// indexConfigObjects recursively searches ptr for object fields named
|
||||
// "@id" and maps that ID value to the full configPath in the index.
|
||||
// This function is NOT safe for concurrent access; obtain a write lock
|
||||
// on currentCfgMu.
|
||||
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
|
||||
switch val := ptr.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range val {
|
||||
if k == idKey {
|
||||
switch idVal := v.(type) {
|
||||
case string:
|
||||
index[idVal] = configPath
|
||||
case float64: // all JSON numbers decode as float64
|
||||
index[fmt.Sprintf("%v", idVal)] = configPath
|
||||
default:
|
||||
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// traverse this object property recursively
|
||||
err := indexConfigObjects(val[k], path.Join(configPath, k), index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
// traverse each element of the array recursively
|
||||
for i := range val {
|
||||
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsyncedDecodeAndRun removes any meta fields (like @id tags)
|
||||
// from cfgJSON, decodes the result into a *Config, and runs
|
||||
// it as the new config, replacing any other current config.
|
||||
// It does NOT update the raw config state, as this is a
|
||||
// lower-level function; most callers will want to use Load
|
||||
// instead. A write lock on currentCfgMu is required!
|
||||
func unsyncedDecodeAndRun(cfgJSON []byte) error {
|
||||
// remove any @id fields from the JSON, which would cause
|
||||
// loading to break since the field wouldn't be recognized
|
||||
strippedCfgJSON := RemoveMetaFields(cfgJSON)
|
||||
|
||||
var newCfg *Config
|
||||
err := strictUnmarshalJSON(strippedCfgJSON, &newCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the new config and start all its apps
|
||||
err = run(newCfg, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// swap old config with the new one
|
||||
oldCfg := currentCfg
|
||||
currentCfg = newCfg
|
||||
|
||||
// Stop, Cleanup each old app
|
||||
unsyncedStop(oldCfg)
|
||||
|
||||
// autosave a non-nil config, if not disabled
|
||||
if newCfg != nil &&
|
||||
(newCfg.Admin == nil ||
|
||||
newCfg.Admin.Config == nil ||
|
||||
newCfg.Admin.Config.Persist == nil ||
|
||||
*newCfg.Admin.Config.Persist) {
|
||||
dir := filepath.Dir(ConfigAutosavePath)
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
Log().Error("unable to create folder for config autosave",
|
||||
zap.String("dir", dir),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
||||
if err == nil {
|
||||
Log().Info("autosaved config", zap.String("file", ConfigAutosavePath))
|
||||
} else {
|
||||
Log().Error("unable to autosave config",
|
||||
zap.String("file", ConfigAutosavePath),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// run runs newCfg and starts all its apps if
|
||||
// start is true. If any errors happen, cleanup
|
||||
// is performed if any modules were provisioned;
|
||||
// apps that were started already will be stopped,
|
||||
// so this function should not leak resources if
|
||||
// an error is returned. However, if no error is
|
||||
// returned and start == false, you should cancel
|
||||
// the config if you are not going to start it,
|
||||
// so that each provisioned module will be
|
||||
// cleaned up.
|
||||
//
|
||||
// This is a low-level function; most callers
|
||||
// will want to use Run instead, which also
|
||||
// updates the config's raw state.
|
||||
func run(newCfg *Config, start bool) error {
|
||||
// because we will need to roll back any state
|
||||
// modifications if this function errors, we
|
||||
// keep a single error value and scope all
|
||||
// sub-operations to their own functions to
|
||||
// ensure this error value does not get
|
||||
// overridden or missed when it should have
|
||||
// been set by a short assignment
|
||||
var err error
|
||||
|
||||
// start the admin endpoint (and stop any prior one)
|
||||
if start {
|
||||
err = replaceAdmin(newCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if newCfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare the new config for use
|
||||
newCfg.apps = make(map[string]App)
|
||||
|
||||
// create a context within which to load
|
||||
// modules - essentially our new config's
|
||||
// execution environment; be sure that
|
||||
// cleanup occurs when we return if there
|
||||
// was an error; if no error, it will get
|
||||
// cleaned up on next config cycle
|
||||
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// if there were any errors during startup,
|
||||
// we should cancel the new context we created
|
||||
// since the associated config won't be used;
|
||||
// this will cause all modules that were newly
|
||||
// provisioned to clean themselves up
|
||||
cancel()
|
||||
|
||||
// also undo any other state changes we made
|
||||
if currentCfg != nil {
|
||||
certmagic.Default.Storage = currentCfg.storage
|
||||
}
|
||||
}
|
||||
}()
|
||||
newCfg.cancelFunc = cancel // clean up later
|
||||
|
||||
// set up logging before anything bad happens
|
||||
if newCfg.Logging == nil {
|
||||
newCfg.Logging = new(Logging)
|
||||
}
|
||||
err = newCfg.Logging.openLogs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set up global storage and make it CertMagic's default storage, too
|
||||
err = func() error {
|
||||
if newCfg.StorageRaw != nil {
|
||||
val, err := ctx.LoadModule(newCfg, "StorageRaw")
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading storage module: %v", err)
|
||||
}
|
||||
stor, err := val.(StorageConverter).CertMagicStorage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating storage value: %v", err)
|
||||
}
|
||||
newCfg.storage = stor
|
||||
}
|
||||
|
||||
if newCfg.storage == nil {
|
||||
newCfg.storage = DefaultStorage
|
||||
}
|
||||
certmagic.Default.Storage = newCfg.storage
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load and Provision each app and their submodules
|
||||
err = func() error {
|
||||
for appName := range newCfg.AppsRaw {
|
||||
if _, err := ctx.App(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !start {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start
|
||||
return func() error {
|
||||
var started []string
|
||||
for name, a := range newCfg.apps {
|
||||
err := a.Start()
|
||||
if err != nil {
|
||||
// an app failed to start, so we need to stop
|
||||
// all other apps that were already started
|
||||
for _, otherAppName := range started {
|
||||
err2 := newCfg.apps[otherAppName].Stop()
|
||||
if err2 != nil {
|
||||
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
|
||||
err, otherAppName, err2)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s app module: start: %v", name, err)
|
||||
}
|
||||
started = append(started, name)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops running the current configuration.
|
||||
// It is the antithesis of Run(). This function
|
||||
// will log any errors that occur during the
|
||||
// stopping of individual apps and continue to
|
||||
// stop the others. Stop should only be called
|
||||
// if not replacing with a new config.
|
||||
func Stop() error {
|
||||
currentCfgMu.Lock()
|
||||
defer currentCfgMu.Unlock()
|
||||
unsyncedStop(currentCfg)
|
||||
currentCfg = nil
|
||||
rawCfgJSON = nil
|
||||
rawCfgIndex = nil
|
||||
rawCfg[rawConfigKey] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsyncedStop stops cfg from running, but has
|
||||
// no locking around cfg. It is a no-op if cfg is
|
||||
// nil. If any app returns an error when stopping,
|
||||
// it is logged and the function continues stopping
|
||||
// the next app. This function assumes all apps in
|
||||
// cfg were successfully started first.
|
||||
func unsyncedStop(cfg *Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// stop each app
|
||||
for name, a := range cfg.apps {
|
||||
err := a.Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] stop %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// clean up all modules
|
||||
cfg.cancelFunc()
|
||||
}
|
||||
|
||||
// stopAndCleanup calls stop and cleans up anything
|
||||
// else that is expedient. This should only be used
|
||||
// when stopping and not replacing with a new config.
|
||||
func stopAndCleanup() error {
|
||||
if err := Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
certmagic.CleanUpOwnLocks()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate loads, provisions, and validates
|
||||
// cfg, but does not start running it.
|
||||
func Validate(cfg *Config) error {
|
||||
err := run(cfg, false)
|
||||
if err == nil {
|
||||
cfg.cancelFunc() // call Cleanup on all modules
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Duration can be an integer or a string. An integer is
|
||||
// interpreted as nanoseconds. If a string, it is a Go
|
||||
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
|
||||
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, and `h`.
|
||||
type Duration time.Duration
|
||||
|
||||
// UnmarshalJSON satisfies json.Unmarshaler.
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
var dur time.Duration
|
||||
var err error
|
||||
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
|
||||
dur, err = time.ParseDuration(strings.Trim(string(b), `"`))
|
||||
} else {
|
||||
err = json.Unmarshal(b, &dur)
|
||||
}
|
||||
*d = Duration(dur)
|
||||
return err
|
||||
}
|
||||
|
||||
// GoModule returns the build info of this Caddy
|
||||
// build from debug.BuildInfo (requires Go modules).
|
||||
// If no version information is available, a non-nil
|
||||
// value will still be returned, but with an
|
||||
// unknown version.
|
||||
func GoModule() *debug.Module {
|
||||
var mod debug.Module
|
||||
return goModule(&mod)
|
||||
}
|
||||
|
||||
// goModule holds the actual implementation of GoModule.
|
||||
// Allocating debug.Module in GoModule() and passing a
|
||||
// reference to goModule enables mid-stack inlining.
|
||||
func goModule(mod *debug.Module) *debug.Module {
|
||||
mod.Version = "unknown"
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
mod.Path = bi.Main.Path
|
||||
// The recommended way to build Caddy involves
|
||||
// creating a separate main module, which
|
||||
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
||||
// once that issue is fixed, we should just be able to use bi.Main... hopefully.
|
||||
for _, dep := range bi.Deps {
|
||||
if dep.Path == ImportPath {
|
||||
return dep
|
||||
}
|
||||
}
|
||||
return &bi.Main
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
// CtxKey is a value type for use with context.WithValue.
|
||||
type CtxKey string
|
||||
|
||||
// This group of variables pertains to the current configuration.
|
||||
var (
|
||||
// currentCfgMu protects everything in this var block.
|
||||
currentCfgMu sync.RWMutex
|
||||
|
||||
// currentCfg is the currently-running configuration.
|
||||
currentCfg *Config
|
||||
|
||||
// rawCfg is the current, generic-decoded configuration;
|
||||
// we initialize it as a map with one field ("config")
|
||||
// to maintain parity with the API endpoint and to avoid
|
||||
// the special case of having to access/mutate the variable
|
||||
// directly without traversing into it.
|
||||
rawCfg = map[string]interface{}{
|
||||
rawConfigKey: nil,
|
||||
}
|
||||
|
||||
// rawCfgJSON is the JSON-encoded form of rawCfg. Keeping
|
||||
// this around avoids an extra Marshal call during changes.
|
||||
rawCfgJSON []byte
|
||||
|
||||
// rawCfgIndex is the map of user-assigned ID to expanded
|
||||
// path, for converting /id/ paths to /config/ paths.
|
||||
rawCfgIndex map[string]string
|
||||
)
|
||||
|
||||
// ImportPath is the package import path for Caddy core.
|
||||
const ImportPath = "github.com/caddyserver/caddy/v2"
|
||||
@@ -1,29 +0,0 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Path returns the path to the folder
|
||||
// where the application may store data. This
|
||||
// currently resolves to ~/.caddy
|
||||
func Path() string {
|
||||
return filepath.Join(userHomeDir(), ".caddy")
|
||||
}
|
||||
|
||||
// userHomeDir returns the user's home directory according to
|
||||
// environment variables.
|
||||
//
|
||||
// Credit: http://stackoverflow.com/a/7922977/1048862
|
||||
func userHomeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
return home
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
if actual := Path(); !strings.HasSuffix(actual, ".caddy") {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
}
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
// Package caddy implements the Caddy web server as a service
|
||||
// in your own Go programs.
|
||||
//
|
||||
// To use this package, follow a few simple steps:
|
||||
//
|
||||
// 1. Set the AppName and AppVersion variables.
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile (it
|
||||
// might have been piped in as part of a restart).
|
||||
// You should pass in your own Caddyfile loader.
|
||||
// 3. Call caddy.Start() to start Caddy, caddy.Stop()
|
||||
// to stop it, or caddy.Restart() to restart it.
|
||||
//
|
||||
// You should use caddy.Wait() to wait for all Caddy servers
|
||||
// to quit before your process exits.
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// AppName is the name of the application.
|
||||
AppName string
|
||||
|
||||
// AppVersion is the version of the application.
|
||||
AppVersion string
|
||||
|
||||
// Quiet when set to true, will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
|
||||
// HTTP2 indicates whether HTTP2 is enabled or not.
|
||||
HTTP2 bool
|
||||
|
||||
// PidFile is the path to the pidfile to create.
|
||||
PidFile string
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
)
|
||||
|
||||
var (
|
||||
// caddyfile is the input configuration text used for this process
|
||||
caddyfile Input
|
||||
|
||||
// caddyfileMu protects caddyfile during changes
|
||||
caddyfileMu sync.Mutex
|
||||
|
||||
// errIncompleteRestart occurs if this process is a fork
|
||||
// of the parent but no Caddyfile was piped in
|
||||
errIncompleteRestart = errors.New("incomplete restart")
|
||||
|
||||
// servers is a list of all the currently-listening servers
|
||||
servers []*server.Server
|
||||
|
||||
// serversMu protects the servers slice during changes
|
||||
serversMu sync.Mutex
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg sync.WaitGroup
|
||||
|
||||
// loadedGob is used if this is a child process as part of
|
||||
// a graceful restart; it is used to map listeners to their
|
||||
// index in the list of inherited file descriptors. This
|
||||
// variable is not safe for concurrent access.
|
||||
loadedGob caddyfileGob
|
||||
|
||||
// startedBefore should be set to true if caddy has been started
|
||||
// at least once (does not indicate whether currently running).
|
||||
startedBefore bool
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHost is the default host.
|
||||
DefaultHost = ""
|
||||
// DefaultPort is the default port.
|
||||
DefaultPort = "2015"
|
||||
// DefaultRoot is the default root folder.
|
||||
DefaultRoot = "."
|
||||
)
|
||||
|
||||
// Start starts Caddy with the given Caddyfile. If cdyfile
|
||||
// is nil, the LoadCaddyfile function will be called to get
|
||||
// one.
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
//
|
||||
// Note (POSIX): If Start is called in the child process of a
|
||||
// restart more than once within the duration of the graceful
|
||||
// cutoff (i.e. the child process called Start a first time,
|
||||
// then called Stop, then Start again within the first 5 seconds
|
||||
// or however long GracefulTimeout is) and the Caddyfiles have
|
||||
// at least one listener address in common, the second Start
|
||||
// may fail with "address already in use" as there's no
|
||||
// guarantee that the parent process has relinquished the
|
||||
// address before the grace period ends.
|
||||
func Start(cdyfile Input) (err error) {
|
||||
// If we return with no errors, we must do two things: tell the
|
||||
// parent that we succeeded and write to the pidfile.
|
||||
defer func() {
|
||||
if err == nil {
|
||||
signalSuccessToParent() // TODO: Is doing this more than once per process a bad idea? Start could get called more than once in other apps.
|
||||
if PidFile != "" {
|
||||
err := writePidFile()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Could not write pidfile: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Input must never be nil; try to load something
|
||||
if cdyfile == nil {
|
||||
cdyfile, err = LoadCaddyfile(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
caddyfileMu.Lock()
|
||||
caddyfile = cdyfile
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
// load the server configs (activates Let's Encrypt)
|
||||
configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// group virtualhosts by address
|
||||
groupings, err := arrangeBindings(configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start each server with its one or more configurations
|
||||
err = startServers(groupings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startedBefore = true
|
||||
|
||||
// Show initialization output
|
||||
if !Quiet && !IsRestart() {
|
||||
var checkedFdLimit bool
|
||||
for _, group := range groupings {
|
||||
for _, conf := range group.Configs {
|
||||
// Print address of site
|
||||
fmt.Println(conf.Address())
|
||||
|
||||
// Note if non-localhost site resolves to loopback interface
|
||||
if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
|
||||
conf.Host, group.BindAddr.IP.String())
|
||||
}
|
||||
if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
checkFdlimit()
|
||||
checkedFdLimit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startServers starts all the servers in groupings,
|
||||
// taking into account whether or not this process is
|
||||
// a child from a graceful restart or not. It blocks
|
||||
// until the servers are listening.
|
||||
func startServers(groupings bindingGroup) error {
|
||||
var startupWg sync.WaitGroup
|
||||
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
|
||||
|
||||
for _, group := range groupings {
|
||||
s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.HTTP2 = HTTP2
|
||||
s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running
|
||||
if s.OnDemandTLS {
|
||||
s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome!
|
||||
} else {
|
||||
s.TLSConfig.GetCertificate = https.GetCertificate
|
||||
}
|
||||
|
||||
var ln server.ListenerFile
|
||||
if IsRestart() {
|
||||
// Look up this server's listener in the map of inherited file descriptors;
|
||||
// if we don't have one, we must make a new one (later).
|
||||
if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
|
||||
fln, err := net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ln, ok = fln.(server.ListenerFile)
|
||||
if !ok {
|
||||
return errors.New("listener for " + s.Addr + " was not a ListenerFile")
|
||||
}
|
||||
|
||||
file.Close()
|
||||
delete(loadedGob.ListenerFds, s.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(s *server.Server, ln server.ListenerFile) {
|
||||
defer wg.Done()
|
||||
|
||||
// run startup functions that should only execute when
|
||||
// the original parent process is starting.
|
||||
if !IsRestart() && !startedBefore {
|
||||
err := s.RunFirstStartupFuncs()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// start the server
|
||||
if ln != nil {
|
||||
errChan <- s.Serve(ln)
|
||||
} else {
|
||||
errChan <- s.ListenAndServe()
|
||||
}
|
||||
}(s, ln)
|
||||
|
||||
startupWg.Add(1)
|
||||
go func(s *server.Server) {
|
||||
defer startupWg.Done()
|
||||
s.WaitUntilStarted()
|
||||
}(s)
|
||||
|
||||
serversMu.Lock()
|
||||
servers = append(servers, s)
|
||||
serversMu.Unlock()
|
||||
}
|
||||
|
||||
// Close the remaining (unused) file descriptors to free up resources
|
||||
if IsRestart() {
|
||||
for key, fdIndex := range loadedGob.ListenerFds {
|
||||
os.NewFile(fdIndex, "").Close()
|
||||
delete(loadedGob.ListenerFds, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all servers to finish starting
|
||||
startupWg.Wait()
|
||||
|
||||
// Return the first error, if any
|
||||
select {
|
||||
case err := <-errChan:
|
||||
// "use of closed network connection" is normal if it was a graceful shutdown
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops all servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks that may have been
|
||||
// configured by middleware (they must be executed separately).
|
||||
func Stop() error {
|
||||
https.Deactivate()
|
||||
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
if err := s.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", s.Addr, err)
|
||||
}
|
||||
}
|
||||
servers = []*server.Server{} // don't reuse servers
|
||||
serversMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks until all servers are stopped.
|
||||
func Wait() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile, prioritizing a Caddyfile
|
||||
// piped from stdin as part of a restart (only happens on first call
|
||||
// to LoadCaddyfile). If it is not a restart, this function tries
|
||||
// calling the user's loader function, and if that returns nil, then
|
||||
// this function resorts to the default configuration. Thus, if there
|
||||
// are no other errors, this function always returns at least the
|
||||
// default Caddyfile.
|
||||
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
|
||||
// If we are a fork, finishing the restart is highest priority;
|
||||
// piped input is required in this case.
|
||||
if IsRestart() {
|
||||
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cdyfile = loadedGob.Caddyfile
|
||||
atomic.StoreInt32(https.OnDemandIssuedCount, loadedGob.OnDemandTLSCertsIssued)
|
||||
}
|
||||
|
||||
// Try user's loader
|
||||
if cdyfile == nil && loader != nil {
|
||||
cdyfile, err = loader()
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the current Caddyfile
|
||||
func Caddyfile() Input {
|
||||
caddyfileMu.Lock()
|
||||
defer caddyfileMu.Unlock()
|
||||
return caddyfile
|
||||
}
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// IsFile returns true if the original input was a file on the file system
|
||||
// that could be loaded again later if requested.
|
||||
IsFile() bool
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCaddyStartStop(t *testing.T) {
|
||||
caddyfile := "localhost:1984"
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting, iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(2 * time.Second),
|
||||
}
|
||||
resp, err := client.Get("http://localhost:1984")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Error stopping, iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
)
|
||||
|
||||
const filename = "Caddyfile"
|
||||
|
||||
// ToJSON converts caddyfile to its JSON representation.
|
||||
func ToJSON(caddyfile []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
|
||||
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
block := ServerBlock{Body: [][]interface{}{}}
|
||||
|
||||
// Fill up host list
|
||||
for _, host := range sb.HostList() {
|
||||
block.Hosts = append(block.Hosts, standardizeScheme(host))
|
||||
}
|
||||
|
||||
// Extract directives deterministically by sorting them
|
||||
var directives = make([]string, len(sb.Tokens))
|
||||
for dir := range sb.Tokens {
|
||||
directives = append(directives, dir)
|
||||
}
|
||||
sort.Strings(directives)
|
||||
|
||||
// Convert each directive's tokens into our JSON structure
|
||||
for _, dir := range directives {
|
||||
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
for disp.Next() {
|
||||
block.Body = append(block.Body, constructLine(&disp))
|
||||
}
|
||||
}
|
||||
|
||||
// tack this block onto the end of the list
|
||||
j = append(j, block)
|
||||
}
|
||||
|
||||
result, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// constructLine transforms tokens into a JSON-encodable structure;
|
||||
// but only one line at a time, to be used at the top-level of
|
||||
// a server block only (where the first token on each line is a
|
||||
// directive) - not to be used at any other nesting level.
|
||||
func constructLine(d *parse.Dispenser) []interface{} {
|
||||
var args []interface{}
|
||||
|
||||
args = append(args, d.Val())
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
args = append(args, constructBlock(d))
|
||||
continue
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// constructBlock recursively processes tokens into a
|
||||
// JSON-encodable structure. To be used in a directive's
|
||||
// block. Goes to end of block.
|
||||
func constructBlock(d *parse.Dispenser) [][]interface{} {
|
||||
block := [][]interface{}{}
|
||||
|
||||
for d.Next() {
|
||||
if d.Val() == "}" {
|
||||
break
|
||||
}
|
||||
block = append(block, constructLine(d))
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
|
||||
func FromJSON(jsonBytes []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
var result string
|
||||
|
||||
err := json.Unmarshal(jsonBytes, &j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for sbPos, sb := range j {
|
||||
if sbPos > 0 {
|
||||
result += "\n\n"
|
||||
}
|
||||
for i, host := range sb.Hosts {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += standardizeScheme(host)
|
||||
}
|
||||
result += jsonToText(sb.Body, 1)
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
// jsonToText recursively transforms a scope of JSON into plain
|
||||
// Caddyfile text.
|
||||
func jsonToText(scope interface{}, depth int) string {
|
||||
var result string
|
||||
|
||||
switch val := scope.(type) {
|
||||
case string:
|
||||
if strings.ContainsAny(val, "\" \n\t\r") {
|
||||
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
|
||||
} else {
|
||||
result += val
|
||||
}
|
||||
case int:
|
||||
result += strconv.Itoa(val)
|
||||
case float64:
|
||||
result += fmt.Sprintf("%v", val)
|
||||
case bool:
|
||||
result += fmt.Sprintf("%t", val)
|
||||
case [][]interface{}:
|
||||
result += " {\n"
|
||||
for _, arg := range val {
|
||||
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
case []interface{}:
|
||||
for i, v := range val {
|
||||
if block, ok := v.([]interface{}); ok {
|
||||
result += "{\n"
|
||||
for _, arg := range block {
|
||||
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
continue
|
||||
}
|
||||
result += jsonToText(v, depth)
|
||||
if i < len(val)-1 {
|
||||
result += " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// standardizeScheme turns an address like host:https into https://host,
|
||||
// or "host:" into "host".
|
||||
func standardizeScheme(addr string) string {
|
||||
if hostname, port, err := net.SplitHostPort(addr); err == nil {
|
||||
if port == "http" || port == "https" {
|
||||
addr = port + "://" + hostname
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(addr, ":")
|
||||
}
|
||||
|
||||
// Caddyfile encapsulates a slice of ServerBlocks.
|
||||
type Caddyfile []ServerBlock
|
||||
|
||||
// ServerBlock represents a server block.
|
||||
type ServerBlock struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package caddyfile
|
||||
|
||||
import "testing"
|
||||
|
||||
var tests = []struct {
|
||||
caddyfile, json string
|
||||
}{
|
||||
{ // 0
|
||||
caddyfile: `foo {
|
||||
root /bar
|
||||
}`,
|
||||
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
|
||||
},
|
||||
{ // 1
|
||||
caddyfile: `host1, host2 {
|
||||
dir {
|
||||
def
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
|
||||
},
|
||||
{ // 2
|
||||
caddyfile: `host1, host2 {
|
||||
dir abc {
|
||||
def ghi
|
||||
jkl
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
|
||||
},
|
||||
{ // 3
|
||||
caddyfile: `host1:1234, host2:5678 {
|
||||
dir abc {
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
|
||||
},
|
||||
{ // 4
|
||||
caddyfile: `host {
|
||||
foo "bar baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`,
|
||||
},
|
||||
{ // 5
|
||||
caddyfile: `host, host:80 {
|
||||
foo "bar \"baz\""
|
||||
}`,
|
||||
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
|
||||
},
|
||||
{ // 6
|
||||
caddyfile: `host {
|
||||
foo "bar
|
||||
baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`,
|
||||
},
|
||||
{ // 7
|
||||
caddyfile: `host {
|
||||
dir 123 4.56 true
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
},
|
||||
{ // 8
|
||||
caddyfile: `http://host, https://host {
|
||||
}`,
|
||||
json: `[{"hosts":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
},
|
||||
{ // 9
|
||||
caddyfile: `host {
|
||||
dir1 a b
|
||||
dir2 c d
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
|
||||
},
|
||||
{ // 10
|
||||
caddyfile: `host {
|
||||
dir a b
|
||||
dir c d
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
|
||||
},
|
||||
{ // 11
|
||||
caddyfile: `host {
|
||||
dir1 a b
|
||||
dir2 {
|
||||
c
|
||||
d
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
|
||||
},
|
||||
{ // 12
|
||||
caddyfile: `host1 {
|
||||
dir1
|
||||
}
|
||||
|
||||
host2 {
|
||||
dir2
|
||||
}`,
|
||||
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
output, err := ToJSON([]byte(test.caddyfile))
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
if string(output) != test.json {
|
||||
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromJSON(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
output, err := FromJSON([]byte(test.json))
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
if string(output) != test.caddyfile {
|
||||
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
// host:https should be converted to https://host
|
||||
output, err := ToJSON([]byte(`host:https`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"hosts":["https://host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
|
||||
output, err = FromJSON([]byte(`[{"hosts":["https://host"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := "https://host {\n}", string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
|
||||
// host: should be converted to just host
|
||||
output, err = ToJSON([]byte(`host:`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"hosts":["host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
output, err = FromJSON([]byte(`[{"hosts":["host:"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := "host {\n}", string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
-348
@@ -1,348 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
||||
|
||||
// loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them,
|
||||
// the parsed server blocks, the index of the last directive it processed, and an error (if any).
|
||||
func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) {
|
||||
var configs []server.Config
|
||||
|
||||
// Each server block represents similar hosts/addresses, since they
|
||||
// were grouped together in the Caddyfile.
|
||||
serverBlocks, err := parse.ServerBlocks(filename, input, true)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
if len(serverBlocks) == 0 {
|
||||
newInput := DefaultInput()
|
||||
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off
|
||||
|
||||
// Iterate each server block and make a config for each one,
|
||||
// executing the directives that were parsed in order up to the tls
|
||||
// directive; this is because we must activate Let's Encrypt.
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j, addr := range sb.Addresses {
|
||||
config := server.Config{
|
||||
Host: addr.Host,
|
||||
Port: addr.Port,
|
||||
Scheme: addr.Scheme,
|
||||
Root: Root,
|
||||
ConfigFile: filename,
|
||||
AppName: AppName,
|
||||
AppVersion: AppVersion,
|
||||
}
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
for k, dir := range directiveOrder {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
// Each setup function gets a controller, from which setup functions
|
||||
// get access to the config, tokens, and other state information useful
|
||||
// to set up its own host only.
|
||||
controller := &setup.Controller{
|
||||
Config: &config,
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
// execute setup function and append middleware handler, if any
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, nil, lastDirectiveIndex, err
|
||||
}
|
||||
if midware != nil {
|
||||
config.Middleware = append(config.Middleware, midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
|
||||
// Stop after TLS setup, since we need to activate Let's Encrypt before continuing;
|
||||
// it makes some changes to the configs that middlewares might want to know about.
|
||||
if dir.name == "tls" {
|
||||
lastDirectiveIndex = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
}
|
||||
|
||||
return configs, serverBlocks, lastDirectiveIndex, nil
|
||||
}
|
||||
|
||||
// loadConfigs reads input (named filename) and parses it, returning the
|
||||
// server configurations in the order they appeared in the input. As part
|
||||
// of this, it activates Let's Encrypt for the configs that are produced.
|
||||
// Thus, the returned configs are already optimally configured for HTTPS.
|
||||
func loadConfigs(filename string, input io.Reader) ([]server.Config, error) {
|
||||
configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we have all the configs, but they have only been set up to the
|
||||
// point of tls. We need to activate Let's Encrypt before setting up
|
||||
// the rest of the middlewares so they have correct information regarding
|
||||
// TLS configuration, if necessary. (this only appends, so our iterations
|
||||
// over server blocks below shouldn't be affected)
|
||||
if !IsRestart() && !Quiet {
|
||||
fmt.Print("Activating privacy features...")
|
||||
}
|
||||
configs, err = https.Activate(configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !IsRestart() && !Quiet {
|
||||
fmt.Println(" done.")
|
||||
}
|
||||
|
||||
// Finish setting up the rest of the directives, now that TLS is
|
||||
// optimally configured. These loops are similar to above except
|
||||
// we don't iterate all the directives from the beginning and we
|
||||
// don't create new configs.
|
||||
configIndex := -1
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j := range sb.Addresses {
|
||||
configIndex++
|
||||
|
||||
for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ {
|
||||
dir := directiveOrder[k]
|
||||
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
controller := &setup.Controller{
|
||||
Config: &configs[configIndex],
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if midware != nil {
|
||||
configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// makeOnces makes a map of directive name to sync.Once
|
||||
// instance. This is intended to be called once per server
|
||||
// block when setting up configs so that Setup functions
|
||||
// for each directive can perform a task just once per
|
||||
// server block, even if there are multiple hosts on the block.
|
||||
//
|
||||
// We need one Once per directive, otherwise the first
|
||||
// directive to use it would exclude other directives from
|
||||
// using it at all, which would be a bug.
|
||||
func makeOnces() map[string]*sync.Once {
|
||||
onces := make(map[string]*sync.Once)
|
||||
for _, dir := range directiveOrder {
|
||||
onces[dir.name] = new(sync.Once)
|
||||
}
|
||||
return onces
|
||||
}
|
||||
|
||||
// makeStorages makes a map of directive name to interface{}
|
||||
// so that directives' setup functions can persist state
|
||||
// between different hosts on the same server block during the
|
||||
// setup phase.
|
||||
func makeStorages() map[string]interface{} {
|
||||
storages := make(map[string]interface{})
|
||||
for _, dir := range directiveOrder {
|
||||
storages[dir.name] = nil
|
||||
}
|
||||
return storages
|
||||
}
|
||||
|
||||
// arrangeBindings groups configurations by their bind address. For example,
|
||||
// a server that should listen on localhost and another on 127.0.0.1 will
|
||||
// be grouped into the same address: 127.0.0.1. It will return an error
|
||||
// if an address is malformed or a TLS listener is configured on the
|
||||
// same address as a plaintext HTTP listener. The return value is a map of
|
||||
// bind address to list of configs that would become VirtualHosts on that
|
||||
// server. Use the keys of the returned map to create listeners, and use
|
||||
// the associated values to set up the virtualhosts.
|
||||
func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) {
|
||||
var groupings bindingGroup
|
||||
|
||||
// Group configs by bind address
|
||||
for _, conf := range allConfigs {
|
||||
// use default port if none is specified
|
||||
if conf.Port == "" {
|
||||
conf.Port = Port
|
||||
}
|
||||
|
||||
bindAddr, warnErr, fatalErr := resolveAddr(conf)
|
||||
if fatalErr != nil {
|
||||
return groupings, fatalErr
|
||||
}
|
||||
if warnErr != nil {
|
||||
log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr)
|
||||
}
|
||||
|
||||
// Make sure to compare the string representation of the address,
|
||||
// not the pointer, since a new *TCPAddr is created each time.
|
||||
var existing bool
|
||||
for i := 0; i < len(groupings); i++ {
|
||||
if groupings[i].BindAddr.String() == bindAddr.String() {
|
||||
groupings[i].Configs = append(groupings[i].Configs, conf)
|
||||
existing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existing {
|
||||
groupings = append(groupings, bindingMapping{
|
||||
BindAddr: bindAddr,
|
||||
Configs: []server.Config{conf},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow HTTP and HTTPS to be served on the same address
|
||||
for _, group := range groupings {
|
||||
isTLS := group.Configs[0].TLS.Enabled
|
||||
for _, config := range group.Configs {
|
||||
if config.TLS.Enabled != isTLS {
|
||||
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
|
||||
if config.TLS.Enabled {
|
||||
thisConfigProto = "HTTPS"
|
||||
}
|
||||
if group.Configs[0].TLS.Enabled {
|
||||
otherConfigProto = "HTTPS"
|
||||
}
|
||||
return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
|
||||
group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupings, nil
|
||||
}
|
||||
|
||||
// resolveAddr determines the address (host and port) that a config will
|
||||
// bind to. The returned address, resolvAddr, should be used to bind the
|
||||
// listener or group the config with other configs using the same address.
|
||||
// The first error, if not nil, is just a warning and should be reported
|
||||
// but execution may continue. The second error, if not nil, is a real
|
||||
// problem and the server should not be started.
|
||||
//
|
||||
// This function does not handle edge cases like port "http" or "https" if
|
||||
// they are not known to the system. It does, however, serve on the wildcard
|
||||
// host if resolving the address of the specific hostname fails.
|
||||
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) {
|
||||
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port))
|
||||
if warnErr != nil {
|
||||
// the hostname probably couldn't be resolved, just bind to wildcard then
|
||||
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port))
|
||||
if fatalErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// validDirective returns true if d is a valid
|
||||
// directive; false otherwise.
|
||||
func validDirective(d string) bool {
|
||||
for _, dir := range directiveOrder {
|
||||
if dir.name == d {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
// It uses the default host and port (depends on
|
||||
// host, e.g. localhost is 2015, otherwise 443) and
|
||||
// root.
|
||||
func DefaultInput() CaddyfileInput {
|
||||
port := Port
|
||||
if https.HostQualifies(Host) && port == DefaultPort {
|
||||
port = "443"
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)),
|
||||
}
|
||||
}
|
||||
|
||||
// These defaults are configurable through the command line
|
||||
var (
|
||||
// Root is the site root
|
||||
Root = DefaultRoot
|
||||
|
||||
// Host is the site host
|
||||
Host = DefaultHost
|
||||
|
||||
// Port is the site port
|
||||
Port = DefaultPort
|
||||
)
|
||||
|
||||
// bindingMapping maps a network address to configurations
|
||||
// that will bind to it. The order of the configs is important.
|
||||
type bindingMapping struct {
|
||||
BindAddr *net.TCPAddr
|
||||
Configs []server.Config
|
||||
}
|
||||
|
||||
// bindingGroup maps network addresses to their configurations.
|
||||
// Preserving the order of the groupings is important
|
||||
// (related to graceful shutdown and restart)
|
||||
// so this is a slice, not a literal map.
|
||||
type bindingGroup []bindingMapping
|
||||
@@ -1,159 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func TestDefaultInput(t *testing.T) {
|
||||
if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
// next few tests simulate user providing -host and/or -port flags
|
||||
|
||||
Host = "not-localhost.com"
|
||||
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "[::1]"
|
||||
if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "127.0.1.1"
|
||||
if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "not-localhost.com"
|
||||
Port = "1234"
|
||||
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = DefaultHost
|
||||
Port = "1234"
|
||||
if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAddr(t *testing.T) {
|
||||
// NOTE: If tests fail due to comparing to string "127.0.0.1",
|
||||
// it's possible that system env resolves with IPv6, or ::1.
|
||||
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
|
||||
// for the assertion, rather than a direct string comparison.
|
||||
|
||||
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
|
||||
// will not behave the same cross-platform, so they have been omitted.
|
||||
|
||||
for i, test := range []struct {
|
||||
config server.Config
|
||||
shouldWarnErr bool
|
||||
shouldFatalErr bool
|
||||
expectedIP string
|
||||
expectedPort int
|
||||
}{
|
||||
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
|
||||
{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0},
|
||||
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234},
|
||||
} {
|
||||
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
|
||||
|
||||
if test.shouldFatalErr && fatalErr == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldFatalErr && fatalErr != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
|
||||
}
|
||||
if fatalErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if test.shouldWarnErr && warnErr == nil {
|
||||
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldWarnErr && warnErr != nil {
|
||||
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
|
||||
}
|
||||
|
||||
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
|
||||
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
|
||||
}
|
||||
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
|
||||
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeOnces(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
onces := makeOnces()
|
||||
if len(onces) != len(directives) {
|
||||
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
|
||||
}
|
||||
expected := map[string]*sync.Once{
|
||||
"dummy": new(sync.Once),
|
||||
"dummy2": new(sync.Once),
|
||||
}
|
||||
if !reflect.DeepEqual(onces, expected) {
|
||||
t.Errorf("onces was %v, expected %v", onces, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeStorages(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
storages := makeStorages()
|
||||
if len(storages) != len(directives) {
|
||||
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
|
||||
}
|
||||
expected := map[string]interface{}{
|
||||
"dummy": nil,
|
||||
"dummy2": nil,
|
||||
}
|
||||
if !reflect.DeepEqual(storages, expected) {
|
||||
t.Errorf("storages was %v, expected %v", storages, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidDirective(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
for i, test := range []struct {
|
||||
directive string
|
||||
valid bool
|
||||
}{
|
||||
{"dummy", true},
|
||||
{"dummy2", true},
|
||||
{"dummy3", false},
|
||||
} {
|
||||
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
|
||||
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The parse package must know which directives
|
||||
// are valid, but it must not import the setup
|
||||
// or config package. To solve this problem, we
|
||||
// fill up this map in our init function here.
|
||||
// The parse package does not need to know the
|
||||
// ordering of the directives.
|
||||
for _, dir := range directiveOrder {
|
||||
parse.ValidDirectives[dir.name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed. Middleware (directives that inject a handler)
|
||||
// are executed in the order A-B-C-*-C-B-A, assuming
|
||||
// they all call the Next handler in the chain.
|
||||
//
|
||||
// Ordering is VERY important. Every middleware will
|
||||
// feel the effects of all other middleware below
|
||||
// (after) them during a request, but they must not
|
||||
// care what middleware above them are doing.
|
||||
//
|
||||
// For example, log needs to know the status code and
|
||||
// exactly how many bytes were written to the client,
|
||||
// which every other middleware can affect, so it gets
|
||||
// registered first. The errors middleware does not
|
||||
// care if gzip or log modifies its response, so it
|
||||
// gets registered below them. Gzip, on the other hand,
|
||||
// DOES care what errors does to the response since it
|
||||
// must compress every output to the client, even error
|
||||
// pages, so it must be registered before the errors
|
||||
// middleware and any others that would write to the
|
||||
// response.
|
||||
var directiveOrder = []directive{
|
||||
// Essential directives that initialize vital configuration settings
|
||||
{"root", setup.Root},
|
||||
{"bind", setup.BindHost},
|
||||
{"tls", https.Setup},
|
||||
|
||||
// Other directives that don't create HTTP handlers
|
||||
{"startup", setup.Startup},
|
||||
{"shutdown", setup.Shutdown},
|
||||
|
||||
// Directives that inject handlers (middleware)
|
||||
{"log", setup.Log},
|
||||
{"gzip", setup.Gzip},
|
||||
{"errors", setup.Errors},
|
||||
{"header", setup.Headers},
|
||||
{"rewrite", setup.Rewrite},
|
||||
{"redir", setup.Redir},
|
||||
{"ext", setup.Ext},
|
||||
{"mime", setup.Mime},
|
||||
{"basicauth", setup.BasicAuth},
|
||||
{"internal", setup.Internal},
|
||||
{"proxy", setup.Proxy},
|
||||
{"fastcgi", setup.FastCGI},
|
||||
{"websocket", setup.WebSocket},
|
||||
{"markdown", setup.Markdown},
|
||||
{"templates", setup.Templates},
|
||||
{"browse", setup.Browse},
|
||||
}
|
||||
|
||||
// RegisterDirective adds the given directive to caddy's list of directives.
|
||||
// Pass the name of a directive you want it to be placed after,
|
||||
// otherwise it will be placed at the bottom of the stack.
|
||||
func RegisterDirective(name string, setup SetupFunc, after string) {
|
||||
dir := directive{name: name, setup: setup}
|
||||
idx := len(directiveOrder)
|
||||
for i := range directiveOrder {
|
||||
if directiveOrder[i].name == after {
|
||||
idx = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...)
|
||||
directiveOrder = newDirectives
|
||||
parse.ValidDirectives[name] = struct{}{}
|
||||
}
|
||||
|
||||
// directive ties together a directive name with its setup function.
|
||||
type directive struct {
|
||||
name string
|
||||
setup SetupFunc
|
||||
}
|
||||
|
||||
// SetupFunc takes a controller and may optionally return a middleware.
|
||||
// If the resulting middleware is not nil, it will be chained into
|
||||
// the HTTP handlers in the order specified in this package.
|
||||
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
|
||||
@@ -1,31 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
RegisterDirective("foo", nil, "dummy")
|
||||
if len(directiveOrder) != 3 {
|
||||
t.Fatal("Should have 3 directives now")
|
||||
}
|
||||
getNames := func() (s []string) {
|
||||
for _, d := range directiveOrder {
|
||||
s = append(s, d.name)
|
||||
}
|
||||
return s
|
||||
}
|
||||
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) {
|
||||
t.Fatalf("directive order doesn't match: %s", getNames())
|
||||
}
|
||||
RegisterDirective("bar", nil, "ASDASD")
|
||||
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) {
|
||||
t.Fatalf("directive order doesn't match: %s", getNames())
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// isLocalhost returns true if host looks explicitly like a localhost address.
|
||||
func isLocalhost(host string) bool {
|
||||
return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 4096
|
||||
|
||||
// Warn if ulimit is too low for production sites
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
|
||||
if err == nil {
|
||||
// Note that an error here need not be reported
|
||||
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
|
||||
if err == nil && lim < min {
|
||||
fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// signalSuccessToParent tells the parent our status using pipe at index 3.
|
||||
// If this process is not a restart, this function does nothing.
|
||||
// Calling this function once this process has successfully initialized
|
||||
// is vital so that the parent process can unblock and kill itself.
|
||||
// This function is idempotent; it executes at most once per process.
|
||||
func signalSuccessToParent() {
|
||||
signalParentOnce.Do(func() {
|
||||
if IsRestart() {
|
||||
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
|
||||
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
|
||||
}
|
||||
ppipe.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// signalParentOnce is used to make sure that the parent is only
|
||||
// signaled once; doing so more than once breaks whatever socket is
|
||||
// at fd 4 (the reason for this is still unclear - to reproduce,
|
||||
// call Stop() and Start() in succession at least once after a
|
||||
// restart, then try loading first host of Caddyfile in the browser).
|
||||
// Do not use this directly - call signalSuccessToParent instead.
|
||||
var signalParentOnce sync.Once
|
||||
|
||||
// caddyfileGob maps bind address to index of the file descriptor
|
||||
// in the Files array passed to the child process. It also contains
|
||||
// the caddyfile contents and other state needed by the new process.
|
||||
// Used only during graceful restarts where a new process is spawned.
|
||||
type caddyfileGob struct {
|
||||
ListenerFds map[string]uintptr
|
||||
Caddyfile Input
|
||||
OnDemandTLSCertsIssued int32
|
||||
}
|
||||
|
||||
// IsRestart returns whether this process is, according
|
||||
// to env variables, a fork as part of a graceful restart.
|
||||
func IsRestart() bool {
|
||||
return os.Getenv("CADDY_RESTART") == "true"
|
||||
}
|
||||
|
||||
// writePidFile writes the process ID to the file at PidFile, if specified.
|
||||
func writePidFile() error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
return ioutil.WriteFile(PidFile, pid, 0644)
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
RealFile bool
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// IsFile returns true if the original input was a real file on the file system.
|
||||
func (c CaddyfileInput) IsFile() bool { return c.RealFile }
|
||||
@@ -1,234 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// certCache stores certificates in memory,
|
||||
// keying certificates by name.
|
||||
var certCache = make(map[string]Certificate)
|
||||
var certCacheMu sync.RWMutex
|
||||
|
||||
// Certificate is a tls.Certificate with associated metadata tacked on.
|
||||
// Even if the metadata can be obtained by parsing the certificate,
|
||||
// we can be more efficient by extracting the metadata once so it's
|
||||
// just there, ready to use.
|
||||
type Certificate struct {
|
||||
tls.Certificate
|
||||
|
||||
// Names is the list of names this certificate is written for.
|
||||
// The first is the CommonName (if any), the rest are SAN.
|
||||
Names []string
|
||||
|
||||
// NotAfter is when the certificate expires.
|
||||
NotAfter time.Time
|
||||
|
||||
// Managed certificates are certificates that Caddy is managing,
|
||||
// as opposed to the user specifying a certificate and key file
|
||||
// or directory and managing the certificate resources themselves.
|
||||
Managed bool
|
||||
|
||||
// OnDemand certificates are obtained or loaded on-demand during TLS
|
||||
// handshakes (as opposed to preloaded certificates, which are loaded
|
||||
// at startup). If OnDemand is true, Managed must necessarily be true.
|
||||
// OnDemand certificates are maintained in the background just like
|
||||
// preloaded ones, however, if an OnDemand certificate fails to renew,
|
||||
// it is removed from the in-memory cache.
|
||||
OnDemand bool
|
||||
|
||||
// OCSP contains the certificate's parsed OCSP response.
|
||||
OCSP *ocsp.Response
|
||||
}
|
||||
|
||||
// getCertificate gets a certificate that matches name (a server name)
|
||||
// from the in-memory cache. If there is no exact match for name, it
|
||||
// will be checked against names of the form '*.example.com' (wildcard
|
||||
// certificates) according to RFC 6125. If a match is found, matched will
|
||||
// be true. If no matches are found, matched will be false and a default
|
||||
// certificate will be returned with defaulted set to true. If no default
|
||||
// certificate is set, defaulted will be set to false.
|
||||
//
|
||||
// The logic in this function is adapted from the Go standard library,
|
||||
// which is by the Go Authors.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func getCertificate(name string) (cert Certificate, matched, defaulted bool) {
|
||||
var ok bool
|
||||
|
||||
// Not going to trim trailing dots here since RFC 3546 says,
|
||||
// "The hostname is represented ... without a trailing dot."
|
||||
// Just normalize to lowercase.
|
||||
name = strings.ToLower(name)
|
||||
|
||||
certCacheMu.RLock()
|
||||
defer certCacheMu.RUnlock()
|
||||
|
||||
// exact match? great, let's use it
|
||||
if cert, ok = certCache[name]; ok {
|
||||
matched = true
|
||||
return
|
||||
}
|
||||
|
||||
// try replacing labels in the name with wildcards until we get a match
|
||||
labels := strings.Split(name, ".")
|
||||
for i := range labels {
|
||||
labels[i] = "*"
|
||||
candidate := strings.Join(labels, ".")
|
||||
if cert, ok = certCache[candidate]; ok {
|
||||
matched = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if nothing matches, use the default certificate or bust
|
||||
cert, defaulted = certCache[""]
|
||||
return
|
||||
}
|
||||
|
||||
// cacheManagedCertificate loads the certificate for domain into the
|
||||
// cache, flagging it as Managed and, if onDemand is true, as OnDemand
|
||||
// (meaning that it was obtained or loaded during a TLS handshake).
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func cacheManagedCertificate(domain string, onDemand bool) (Certificate, error) {
|
||||
cert, err := makeCertificateFromDisk(storage.SiteCertFile(domain), storage.SiteKeyFile(domain))
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
cert.Managed = true
|
||||
cert.OnDemand = onDemand
|
||||
cacheCertificate(cert)
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
|
||||
// and keyFile, which must be in PEM format. It stores the certificate in
|
||||
// memory. The Managed and OnDemand flags of the certificate will be set to
|
||||
// false.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
|
||||
cert, err := makeCertificateFromDisk(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheCertificate(cert)
|
||||
return nil
|
||||
}
|
||||
|
||||
// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
|
||||
// of the certificate and key, then caches it in memory.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
|
||||
cert, err := makeCertificate(certBytes, keyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheCertificate(cert)
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeCertificateFromDisk makes a Certificate by loading the
|
||||
// certificate and key files. It fills out all the fields in
|
||||
// the certificate except for the Managed and OnDemand flags.
|
||||
// (It is up to the caller to set those.)
|
||||
func makeCertificateFromDisk(certFile, keyFile string) (Certificate, error) {
|
||||
certPEMBlock, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
keyPEMBlock, err := ioutil.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
return makeCertificate(certPEMBlock, keyPEMBlock)
|
||||
}
|
||||
|
||||
// makeCertificate turns a certificate PEM bundle and a key PEM block into
|
||||
// a Certificate, with OCSP and other relevant metadata tagged with it,
|
||||
// except for the OnDemand and Managed flags. It is up to the caller to
|
||||
// set those properties.
|
||||
func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
|
||||
var cert Certificate
|
||||
|
||||
// Convert to a tls.Certificate
|
||||
tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
if len(tlsCert.Certificate) == 0 {
|
||||
return cert, errors.New("certificate is empty")
|
||||
}
|
||||
|
||||
// Parse leaf certificate and extract relevant metadata
|
||||
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
if leaf.Subject.CommonName != "" {
|
||||
cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
|
||||
}
|
||||
for _, name := range leaf.DNSNames {
|
||||
if name != leaf.Subject.CommonName {
|
||||
cert.Names = append(cert.Names, strings.ToLower(name))
|
||||
}
|
||||
}
|
||||
cert.NotAfter = leaf.NotAfter
|
||||
|
||||
// Staple OCSP
|
||||
ocspBytes, ocspResp, err := acme.GetOCSPForCert(certPEMBlock)
|
||||
if err != nil {
|
||||
// An error here is not a problem because a certificate may simply
|
||||
// not contain a link to an OCSP server. But we should log it anyway.
|
||||
log.Printf("[WARNING] No OCSP stapling for %v: %v", cert.Names, err)
|
||||
} else if ocspResp.Status == ocsp.Good {
|
||||
tlsCert.OCSPStaple = ocspBytes
|
||||
cert.OCSP = ocspResp
|
||||
}
|
||||
|
||||
cert.Certificate = tlsCert
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// cacheCertificate adds cert to the in-memory cache. If the cache is
|
||||
// empty, cert will be used as the default certificate. If the cache is
|
||||
// full, random entries are deleted until there is room to map all the
|
||||
// names on the certificate.
|
||||
//
|
||||
// This certificate will be keyed to the names in cert.Names. Any name
|
||||
// that is already a key in the cache will be replaced with this cert.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func cacheCertificate(cert Certificate) {
|
||||
certCacheMu.Lock()
|
||||
if _, ok := certCache[""]; !ok {
|
||||
// use as default
|
||||
cert.Names = append(cert.Names, "")
|
||||
certCache[""] = cert
|
||||
}
|
||||
for len(certCache)+len(cert.Names) > 10000 {
|
||||
// for simplicity, just remove random elements
|
||||
for key := range certCache {
|
||||
if key == "" { // ... but not the default cert
|
||||
continue
|
||||
}
|
||||
delete(certCache, key)
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, name := range cert.Names {
|
||||
certCache[name] = cert
|
||||
}
|
||||
certCacheMu.Unlock()
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package https
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestUnexportedGetCertificate(t *testing.T) {
|
||||
defer func() { certCache = make(map[string]Certificate) }()
|
||||
|
||||
// When cache is empty
|
||||
if _, matched, defaulted := getCertificate("example.com"); matched || defaulted {
|
||||
t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted)
|
||||
}
|
||||
|
||||
// When cache has one certificate in it (also is default)
|
||||
defaultCert := Certificate{Names: []string{"example.com", ""}}
|
||||
certCache[""] = defaultCert
|
||||
certCache["example.com"] = defaultCert
|
||||
if cert, matched, defaulted := getCertificate("Example.com"); !matched || defaulted || cert.Names[0] != "example.com" {
|
||||
t.Errorf("Didn't get a cert for 'Example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
|
||||
}
|
||||
if cert, matched, defaulted := getCertificate(""); !matched || defaulted || cert.Names[0] != "example.com" {
|
||||
t.Errorf("Didn't get a cert for '' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
|
||||
}
|
||||
|
||||
// When retrieving wildcard certificate
|
||||
certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}}
|
||||
if cert, matched, defaulted := getCertificate("sub.example.com"); !matched || defaulted || cert.Names[0] != "*.example.com" {
|
||||
t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
|
||||
}
|
||||
|
||||
// When no certificate matches, the default is returned
|
||||
if cert, matched, defaulted := getCertificate("nomatch"); matched || !defaulted {
|
||||
t.Errorf("Expected matched=false, defaulted=true; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
|
||||
} else if cert.Names[0] != "example.com" {
|
||||
t.Errorf("Expected default cert, got: %v", cert)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheCertificate(t *testing.T) {
|
||||
defer func() { certCache = make(map[string]Certificate) }()
|
||||
|
||||
cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}})
|
||||
if _, ok := certCache["example.com"]; !ok {
|
||||
t.Error("Expected first cert to be cached by key 'example.com', but it wasn't")
|
||||
}
|
||||
if _, ok := certCache["sub.example.com"]; !ok {
|
||||
t.Error("Expected first cert to be cached by key 'sub.exmaple.com', but it wasn't")
|
||||
}
|
||||
if cert, ok := certCache[""]; !ok || cert.Names[2] != "" {
|
||||
t.Error("Expected first cert to be cached additionally as the default certificate with empty name added, but it wasn't")
|
||||
}
|
||||
|
||||
cacheCertificate(Certificate{Names: []string{"example2.com"}})
|
||||
if _, ok := certCache["example2.com"]; !ok {
|
||||
t.Error("Expected second cert to be cached by key 'exmaple2.com', but it wasn't")
|
||||
}
|
||||
if cert, ok := certCache[""]; ok && cert.Names[0] == "example2.com" {
|
||||
t.Error("Expected second cert to NOT be cached as default, but it was")
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// acmeMu ensures that only one ACME challenge occurs at a time.
|
||||
var acmeMu sync.Mutex
|
||||
|
||||
// ACMEClient is an acme.Client with custom state attached.
|
||||
type ACMEClient struct {
|
||||
*acme.Client
|
||||
AllowPrompts bool // if false, we assume AlternatePort must be used
|
||||
}
|
||||
|
||||
// NewACMEClient creates a new ACMEClient given an email and whether
|
||||
// prompting the user is allowed. Clients should not be kept and
|
||||
// re-used over long periods of time, but immediate re-use is more
|
||||
// efficient than re-creating on every iteration.
|
||||
var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) {
|
||||
// Look up or create the LE user account
|
||||
leUser, err := getUser(email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The client facilitates our communication with the CA server.
|
||||
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If not registered, the user must register an account with the CA
|
||||
// and agree to terms
|
||||
if leUser.Registration == nil {
|
||||
reg, err := client.Register()
|
||||
if err != nil {
|
||||
return nil, errors.New("registration error: " + err.Error())
|
||||
}
|
||||
leUser.Registration = reg
|
||||
|
||||
if allowPrompts { // can't prompt a user who isn't there
|
||||
if !Agreed && reg.TosURL == "" {
|
||||
Agreed = promptUserAgreement(saURL, false) // TODO - latest URL
|
||||
}
|
||||
if !Agreed && reg.TosURL == "" {
|
||||
return nil, errors.New("user must agree to terms")
|
||||
}
|
||||
}
|
||||
|
||||
err = client.AgreeToTOS()
|
||||
if err != nil {
|
||||
saveUser(leUser) // Might as well try, right?
|
||||
return nil, errors.New("error agreeing to terms: " + err.Error())
|
||||
}
|
||||
|
||||
// save user to the file system
|
||||
err = saveUser(leUser)
|
||||
if err != nil {
|
||||
return nil, errors.New("could not save user: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &ACMEClient{
|
||||
Client: client,
|
||||
AllowPrompts: allowPrompts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewACMEClientGetEmail creates a new ACMEClient and gets an email
|
||||
// address at the same time (a server config is required, since it
|
||||
// may contain an email address in it).
|
||||
func NewACMEClientGetEmail(config server.Config, allowPrompts bool) (*ACMEClient, error) {
|
||||
return NewACMEClient(getEmail(config, allowPrompts), allowPrompts)
|
||||
}
|
||||
|
||||
// Configure configures c according to bindHost, which is the host (not
|
||||
// whole address) to bind the listener to in solving the http and tls-sni
|
||||
// challenges.
|
||||
func (c *ACMEClient) Configure(bindHost string) {
|
||||
// If we allow prompts, operator must be present. In our case,
|
||||
// that is synonymous with saying the server is not already
|
||||
// started. So if the user is still there, we don't use
|
||||
// AlternatePort because we don't need to proxy the challenges.
|
||||
// Conversely, if the operator is not there, the server has
|
||||
// already started and we need to proxy the challenge.
|
||||
if c.AllowPrompts {
|
||||
// Operator is present; server is not already listening
|
||||
c.SetHTTPAddress(net.JoinHostPort(bindHost, ""))
|
||||
c.SetTLSAddress(net.JoinHostPort(bindHost, ""))
|
||||
//c.ExcludeChallenges([]acme.Challenge{acme.DNS01})
|
||||
} else {
|
||||
// Operator is not present; server is started, so proxy challenges
|
||||
c.SetHTTPAddress(net.JoinHostPort(bindHost, AlternatePort))
|
||||
c.SetTLSAddress(net.JoinHostPort(bindHost, AlternatePort))
|
||||
//c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
|
||||
}
|
||||
c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) // TODO: can we proxy TLS challenges? and we should support DNS...
|
||||
}
|
||||
|
||||
// Obtain obtains a single certificate for names. It stores the certificate
|
||||
// on the disk if successful.
|
||||
func (c *ACMEClient) Obtain(names []string) error {
|
||||
Attempts:
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
acmeMu.Lock()
|
||||
certificate, failures := c.ObtainCertificate(names, true, nil)
|
||||
acmeMu.Unlock()
|
||||
if len(failures) > 0 {
|
||||
// Error - try to fix it or report it to the user and abort
|
||||
var errMsg string // we'll combine all the failures into a single error message
|
||||
var promptedForAgreement bool // only prompt user for agreement at most once
|
||||
|
||||
for errDomain, obtainErr := range failures {
|
||||
// TODO: Double-check, will obtainErr ever be nil?
|
||||
if tosErr, ok := obtainErr.(acme.TOSError); ok {
|
||||
// Terms of Service agreement error; we can probably deal with this
|
||||
if !Agreed && !promptedForAgreement && c.AllowPrompts {
|
||||
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
|
||||
promptedForAgreement = true
|
||||
}
|
||||
if Agreed || !c.AllowPrompts {
|
||||
err := c.AgreeToTOS()
|
||||
if err != nil {
|
||||
return errors.New("error agreeing to updated terms: " + err.Error())
|
||||
}
|
||||
continue Attempts
|
||||
}
|
||||
}
|
||||
|
||||
// If user did not agree or it was any other kind of error, just append to the list of errors
|
||||
errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
|
||||
}
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
|
||||
// Success - immediately save the certificate resource
|
||||
err := saveCertResource(certificate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving assets for %v: %v", names, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renew renews the managed certificate for name. Right now our storage
|
||||
// mechanism only supports one name per certificate, so this function only
|
||||
// accepts one domain as input. It can be easily modified to support SAN
|
||||
// certificates if, one day, they become desperately needed enough that our
|
||||
// storage mechanism is upgraded to be more complex to support SAN certs.
|
||||
//
|
||||
// Anyway, this function is safe for concurrent use.
|
||||
func (c *ACMEClient) Renew(name string) error {
|
||||
// Prepare for renewal (load PEM cert, key, and meta)
|
||||
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var certMeta acme.CertificateResource
|
||||
err = json.Unmarshal(metaBytes, &certMeta)
|
||||
certMeta.Certificate = certBytes
|
||||
certMeta.PrivateKey = keyBytes
|
||||
|
||||
// Perform renewal and retry if necessary, but not too many times.
|
||||
var newCertMeta acme.CertificateResource
|
||||
var success bool
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
acmeMu.Lock()
|
||||
newCertMeta, err = c.RenewCertificate(certMeta, true)
|
||||
acmeMu.Unlock()
|
||||
if err == nil {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
|
||||
// If the legal terms changed and need to be agreed to again,
|
||||
// we can handle that.
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := c.AgreeToTOS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// For any other kind of error, wait 10s and try again.
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return errors.New("too many renewal attempts; last error: " + err.Error())
|
||||
}
|
||||
|
||||
return saveCertResource(newCertMeta)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// loadRSAPrivateKey loads a PEM-encoded RSA private key from file.
|
||||
func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) {
|
||||
keyBytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
|
||||
// saveRSAPrivateKey saves a PEM-encoded RSA private key to file.
|
||||
func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error {
|
||||
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||
keyOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyOut.Chmod(0600)
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pemKey)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rsaKeySizeToUse = 128 // make tests faster; small key size OK for testing
|
||||
}
|
||||
|
||||
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
|
||||
keyFile := "test.key"
|
||||
defer os.Remove(keyFile)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test save
|
||||
err = saveRSAPrivateKey(privateKey, keyFile)
|
||||
if err != nil {
|
||||
t.Fatal("error saving private key:", err)
|
||||
}
|
||||
|
||||
// it doesn't make sense to test file permission on windows
|
||||
if runtime.GOOS != "windows" {
|
||||
// get info of the key file
|
||||
info, err := os.Stat(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal("error stating private key:", err)
|
||||
}
|
||||
// verify permission of key file is correct
|
||||
if info.Mode().Perm() != 0600 {
|
||||
t.Error("Expected key file to have permission 0600, but it wasn't")
|
||||
}
|
||||
}
|
||||
|
||||
// test load
|
||||
loadedKey, err := loadRSAPrivateKey(keyFile)
|
||||
if err != nil {
|
||||
t.Error("error loading private key:", err)
|
||||
}
|
||||
|
||||
// verify loaded key is correct
|
||||
if !rsaPrivateKeysSame(privateKey, loadedKey) {
|
||||
t.Error("Expected key bytes to be the same, but they weren't")
|
||||
}
|
||||
}
|
||||
|
||||
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
|
||||
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
|
||||
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
|
||||
}
|
||||
|
||||
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
|
||||
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
|
||||
return x509.MarshalPKCS1PrivateKey(key)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const challengeBasePath = "/.well-known/acme-challenge"
|
||||
|
||||
// RequestCallback proxies challenge requests to ACME client if the
|
||||
// request path starts with challengeBasePath. It returns true if it
|
||||
// handled the request and no more needs to be done; it returns false
|
||||
// if this call was a no-op and the request still needs handling.
|
||||
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
|
||||
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("[ERROR] ACME proxy handler: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestCallbackNoOp(t *testing.T) {
|
||||
// try base paths that aren't handled by this handler
|
||||
for _, url := range []string{
|
||||
"http://localhost/",
|
||||
"http://localhost/foo.html",
|
||||
"http://localhost/.git",
|
||||
"http://localhost/.well-known/",
|
||||
"http://localhost/.well-known/acme-challenging",
|
||||
} {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not craft request, got error: %v", err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
if RequestCallback(rw, req) {
|
||||
t.Errorf("Got true with this URL, but shouldn't have: %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestCallbackSuccess(t *testing.T) {
|
||||
expectedPath := challengeBasePath + "/asdf"
|
||||
|
||||
// Set up fake acme handler backend to make sure proxying succeeds
|
||||
var proxySuccess bool
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxySuccess = true
|
||||
if r.URL.Path != expectedPath {
|
||||
t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
|
||||
// Custom listener that uses the port we expect
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:"+AlternatePort)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to start test server listener: %v", err)
|
||||
}
|
||||
ts.Listener = ln
|
||||
|
||||
// Start our engines and run the test
|
||||
ts.Start()
|
||||
defer ts.Close()
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:"+AlternatePort+expectedPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not craft request, got error: %v", err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
RequestCallback(rw, req)
|
||||
|
||||
if !proxySuccess {
|
||||
t.Fatal("Expected request to be proxied, but it wasn't")
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// GetCertificate gets a certificate to satisfy clientHello as long as
|
||||
// the certificate is already cached in memory. It will not be loaded
|
||||
// from disk or obtained from the CA during the handshake.
|
||||
//
|
||||
// This function is safe for use as a tls.Config.GetCertificate callback.
|
||||
func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := getCertDuringHandshake(clientHello.ServerName, false, false)
|
||||
return &cert.Certificate, err
|
||||
}
|
||||
|
||||
// GetOrObtainCertificate will get a certificate to satisfy clientHello, even
|
||||
// if that means obtaining a new certificate from a CA during the handshake.
|
||||
// It first checks the in-memory cache, then accesses disk, then accesses the
|
||||
// network if it must. An obtained certificate will be stored on disk and
|
||||
// cached in memory.
|
||||
//
|
||||
// This function is safe for use as a tls.Config.GetCertificate callback.
|
||||
func GetOrObtainCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := getCertDuringHandshake(clientHello.ServerName, true, true)
|
||||
return &cert.Certificate, err
|
||||
}
|
||||
|
||||
// getCertDuringHandshake will get a certificate for name. It first tries
|
||||
// the in-memory cache. If no certificate for name is in the cache and if
|
||||
// loadIfNecessary == true, it goes to disk to load it into the cache and
|
||||
// serve it. If it's not on disk and if obtainIfNecessary == true, the
|
||||
// certificate will be obtained from the CA, cached, and served. If
|
||||
// obtainIfNecessary is true, then loadIfNecessary must also be set to true.
|
||||
// An error will be returned if and only if no certificate is available.
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
|
||||
// First check our in-memory cache to see if we've already loaded it
|
||||
cert, matched, defaulted := getCertificate(name)
|
||||
if matched {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
if loadIfNecessary {
|
||||
// Then check to see if we have one on disk
|
||||
loadedCert, err := cacheManagedCertificate(name, true)
|
||||
if err == nil {
|
||||
loadedCert, err = handshakeMaintenance(name, loadedCert)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
|
||||
}
|
||||
return loadedCert, nil
|
||||
}
|
||||
|
||||
if obtainIfNecessary {
|
||||
// By this point, we need to ask the CA for a certificate
|
||||
|
||||
name = strings.ToLower(name)
|
||||
|
||||
// Make sure aren't over any applicable limits
|
||||
err := checkLimitsForObtainingNewCerts(name)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
// Name has to qualify for a certificate
|
||||
if !HostQualifies(name) {
|
||||
return cert, errors.New("hostname '" + name + "' does not qualify for certificate")
|
||||
}
|
||||
|
||||
// Obtain certificate from the CA
|
||||
return obtainOnDemandCertificate(name)
|
||||
}
|
||||
}
|
||||
|
||||
if defaulted {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
return Certificate{}, errors.New("no certificate for " + name)
|
||||
}
|
||||
|
||||
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
|
||||
// now according to mitigating factors we keep track of and preferences the
|
||||
// user has set. If a non-nil error is returned, do not issue a new certificate
|
||||
// for name.
|
||||
func checkLimitsForObtainingNewCerts(name string) error {
|
||||
// User can set hard limit for number of certs for the process to issue
|
||||
if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue {
|
||||
return fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue)
|
||||
}
|
||||
|
||||
// Make sure name hasn't failed a challenge recently
|
||||
failedIssuanceMu.RLock()
|
||||
when, ok := failedIssuance[name]
|
||||
failedIssuanceMu.RUnlock()
|
||||
if ok {
|
||||
return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
|
||||
}
|
||||
|
||||
// Make sure, if we've issued a few certificates already, that we haven't
|
||||
// issued any recently
|
||||
lastIssueTimeMu.Lock()
|
||||
since := time.Since(lastIssueTime)
|
||||
lastIssueTimeMu.Unlock()
|
||||
if atomic.LoadInt32(OnDemandIssuedCount) >= 10 && since < 10*time.Minute {
|
||||
return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
|
||||
}
|
||||
|
||||
// 👍Good to go
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtainOnDemandCertificate obtains a certificate for name for the given
|
||||
// name. If another goroutine has already started obtaining a cert for
|
||||
// name, it will wait and use what the other goroutine obtained.
|
||||
//
|
||||
// This function is safe for use by multiple concurrent goroutines.
|
||||
func obtainOnDemandCertificate(name string) (Certificate, error) {
|
||||
// We must protect this process from happening concurrently, so synchronize.
|
||||
obtainCertWaitChansMu.Lock()
|
||||
wait, ok := obtainCertWaitChans[name]
|
||||
if ok {
|
||||
// lucky us -- another goroutine is already obtaining the certificate.
|
||||
// wait for it to finish obtaining the cert and then we'll use it.
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
<-wait
|
||||
return getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// looks like it's up to us to do all the work and obtain the cert
|
||||
wait = make(chan struct{})
|
||||
obtainCertWaitChans[name] = wait
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
// Unblock waiters and delete waitgroup when we return
|
||||
defer func() {
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
}()
|
||||
|
||||
log.Printf("[INFO] Obtaining new certificate for %s", name)
|
||||
|
||||
// obtain cert
|
||||
client, err := NewACMEClientGetEmail(server.Config{}, false)
|
||||
if err != nil {
|
||||
return Certificate{}, errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
client.Configure("") // TODO: which BindHost?
|
||||
err = client.Obtain([]string{name})
|
||||
if err != nil {
|
||||
// Failed to solve challenge, so don't allow another on-demand
|
||||
// issue for this name to be attempted for a little while.
|
||||
failedIssuanceMu.Lock()
|
||||
failedIssuance[name] = time.Now()
|
||||
go func(name string) {
|
||||
time.Sleep(5 * time.Minute)
|
||||
failedIssuanceMu.Lock()
|
||||
delete(failedIssuance, name)
|
||||
failedIssuanceMu.Unlock()
|
||||
}(name)
|
||||
failedIssuanceMu.Unlock()
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
// Success - update counters and stuff
|
||||
atomic.AddInt32(OnDemandIssuedCount, 1)
|
||||
lastIssueTimeMu.Lock()
|
||||
lastIssueTime = time.Now()
|
||||
lastIssueTimeMu.Unlock()
|
||||
|
||||
// The certificate is already on disk; now just start over to load it and serve it
|
||||
return getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// handshakeMaintenance performs a check on cert for expiration and OCSP
|
||||
// validity.
|
||||
//
|
||||
// This function is safe for use by multiple concurrent goroutines.
|
||||
func handshakeMaintenance(name string, cert Certificate) (Certificate, error) {
|
||||
// Check cert expiration
|
||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||
if timeLeft < renewDurationBefore {
|
||||
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
|
||||
return renewDynamicCertificate(name)
|
||||
}
|
||||
|
||||
// Check OCSP staple validity
|
||||
if cert.OCSP != nil {
|
||||
refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
|
||||
if time.Now().After(refreshTime) {
|
||||
err := stapleOCSP(&cert, nil)
|
||||
if err != nil {
|
||||
// An error with OCSP stapling is not the end of the world, and in fact, is
|
||||
// quite common considering not all certs have issuer URLs that support it.
|
||||
log.Printf("[ERROR] Getting OCSP for %s: %v", name, err)
|
||||
}
|
||||
certCacheMu.Lock()
|
||||
certCache[name] = cert
|
||||
certCacheMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// renewDynamicCertificate renews currentCert using the clientHello. It returns the
|
||||
// certificate to use and an error, if any. currentCert may be returned even if an
|
||||
// error occurs, since we perform renewals before they expire and it may still be
|
||||
// usable. name should already be lower-cased before calling this function.
|
||||
//
|
||||
// This function is safe for use by multiple concurrent goroutines.
|
||||
func renewDynamicCertificate(name string) (Certificate, error) {
|
||||
obtainCertWaitChansMu.Lock()
|
||||
wait, ok := obtainCertWaitChans[name]
|
||||
if ok {
|
||||
// lucky us -- another goroutine is already renewing the certificate.
|
||||
// wait for it to finish, then we'll use the new one.
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
<-wait
|
||||
return getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// looks like it's up to us to do all the work and renew the cert
|
||||
wait = make(chan struct{})
|
||||
obtainCertWaitChans[name] = wait
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
// unblock waiters and delete waitgroup when we return
|
||||
defer func() {
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
}()
|
||||
|
||||
log.Printf("[INFO] Renewing certificate for %s", name)
|
||||
|
||||
client, err := NewACMEClientGetEmail(server.Config{}, false)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
client.Configure("") // TODO: Bind address of relevant listener, yuck
|
||||
err = client.Renew(name)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
||||
return getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// stapleOCSP staples OCSP information to cert for hostname name.
|
||||
// If you have it handy, you should pass in the PEM-encoded certificate
|
||||
// bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
|
||||
// If you don't have the PEM blocks handy, just pass in nil.
|
||||
//
|
||||
// Errors here are not necessarily fatal, it could just be that the
|
||||
// certificate doesn't have an issuer URL.
|
||||
func stapleOCSP(cert *Certificate, pemBundle []byte) error {
|
||||
if pemBundle == nil {
|
||||
// The function in the acme package that gets OCSP requires a PEM-encoded cert
|
||||
bundle := new(bytes.Buffer)
|
||||
for _, derBytes := range cert.Certificate.Certificate {
|
||||
pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
}
|
||||
pemBundle = bundle.Bytes()
|
||||
}
|
||||
|
||||
ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert.Certificate.OCSPStaple = ocspBytes
|
||||
cert.OCSP = ocspResp
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
|
||||
var obtainCertWaitChans = make(map[string]chan struct{})
|
||||
var obtainCertWaitChansMu sync.Mutex
|
||||
|
||||
// OnDemandIssuedCount is the number of certificates that have been issued
|
||||
// on-demand by this process. It is only safe to modify this count atomically.
|
||||
// If it reaches onDemandMaxIssue, on-demand issuances will fail.
|
||||
var OnDemandIssuedCount = new(int32)
|
||||
|
||||
// onDemandMaxIssue is set based on max_certs in tls config. It specifies the
|
||||
// maximum number of certificates that can be issued.
|
||||
// TODO: This applies globally, but we should probably make a server-specific
|
||||
// way to keep track of these limits and counts, since it's specified in the
|
||||
// Caddyfile...
|
||||
var onDemandMaxIssue int32
|
||||
|
||||
// failedIssuance is a set of names that we recently failed to get a
|
||||
// certificate for from the ACME CA. They are removed after some time.
|
||||
// When a name is in this map, do not issue a certificate for it on-demand.
|
||||
var failedIssuance = make(map[string]time.Time)
|
||||
var failedIssuanceMu sync.RWMutex
|
||||
|
||||
// lastIssueTime records when we last obtained a certificate successfully.
|
||||
// If this value is recent, do not make any on-demand certificate requests.
|
||||
var lastIssueTime time.Time
|
||||
var lastIssueTimeMu sync.Mutex
|
||||
@@ -1,54 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCertificate(t *testing.T) {
|
||||
defer func() { certCache = make(map[string]Certificate) }()
|
||||
|
||||
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
|
||||
helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"}
|
||||
helloNoSNI := &tls.ClientHelloInfo{}
|
||||
helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"}
|
||||
|
||||
// When cache is empty
|
||||
if cert, err := GetCertificate(hello); err == nil {
|
||||
t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert)
|
||||
}
|
||||
if cert, err := GetCertificate(helloNoSNI); err == nil {
|
||||
t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert)
|
||||
}
|
||||
|
||||
// When cache has one certificate in it (also is default)
|
||||
defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}}
|
||||
certCache[""] = defaultCert
|
||||
certCache["example.com"] = defaultCert
|
||||
if cert, err := GetCertificate(hello); err != nil {
|
||||
t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err)
|
||||
} else if cert.Leaf.DNSNames[0] != "example.com" {
|
||||
t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert)
|
||||
}
|
||||
if cert, err := GetCertificate(helloNoSNI); err != nil {
|
||||
t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err)
|
||||
} else if cert.Leaf.DNSNames[0] != "example.com" {
|
||||
t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert)
|
||||
}
|
||||
|
||||
// When retrieving wildcard certificate
|
||||
certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}}
|
||||
if cert, err := GetCertificate(helloSub); err != nil {
|
||||
t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err)
|
||||
} else if cert.Leaf.DNSNames[0] != "*.example.com" {
|
||||
t.Errorf("Got wrong certificate, expected wildcard: %v", cert)
|
||||
}
|
||||
|
||||
// When no certificate matches, the default is returned
|
||||
if cert, err := GetCertificate(helloNoMatch); err != nil {
|
||||
t.Errorf("Expected default certificate with no error when no matches, got err: %v", err)
|
||||
} else if cert.Leaf.DNSNames[0] != "example.com" {
|
||||
t.Errorf("Expected default cert with no matches, got: %v", cert)
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
// Package https facilitates the management of TLS assets and integrates
|
||||
// Let's Encrypt functionality into Caddy with first-class support for
|
||||
// creating and renewing certificates automatically. It is designed to
|
||||
// configure sites for HTTPS by default.
|
||||
package https
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Activate sets up TLS for each server config in configs
|
||||
// as needed; this consists of acquiring and maintaining
|
||||
// certificates and keys for qualifying configs and enabling
|
||||
// OCSP stapling for all TLS-enabled configs.
|
||||
//
|
||||
// This function may prompt the user to provide an email
|
||||
// address if none is available through other means. It
|
||||
// prefers the email address specified in the config, but
|
||||
// if that is not available it will check the command line
|
||||
// argument. If absent, it will use the most recent email
|
||||
// address from last time. If there isn't one, the user
|
||||
// will be prompted and shown SA link.
|
||||
//
|
||||
// Also note that calling this function activates asset
|
||||
// management automatically, which keeps certificates
|
||||
// renewed and OCSP stapling updated.
|
||||
//
|
||||
// Activate returns the updated list of configs, since
|
||||
// some may have been appended, for example, to redirect
|
||||
// plaintext HTTP requests to their HTTPS counterpart.
|
||||
// This function only appends; it does not splice.
|
||||
func Activate(configs []server.Config) ([]server.Config, error) {
|
||||
// just in case previous caller forgot...
|
||||
Deactivate()
|
||||
|
||||
// pre-screen each config and earmark the ones that qualify for managed TLS
|
||||
MarkQualified(configs)
|
||||
|
||||
// place certificates and keys on disk
|
||||
err := ObtainCerts(configs, true, false)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// update TLS configurations
|
||||
err = EnableTLS(configs, true)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// set up redirects
|
||||
configs = MakePlaintextRedirects(configs)
|
||||
|
||||
// renew all relevant certificates that need renewal. this is important
|
||||
// to do right away for a couple reasons, mainly because each restart,
|
||||
// the renewal ticker is reset, so if restarts happen more often than
|
||||
// the ticker interval, renewals would never happen. but doing
|
||||
// it right away at start guarantees that renewals aren't missed.
|
||||
err = renewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// keep certificates renewed and OCSP stapling updated
|
||||
go maintainAssets(stopChan)
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// Deactivate cleans up long-term, in-memory resources
|
||||
// allocated by calling Activate(). Essentially, it stops
|
||||
// the asset maintainer from running, meaning that certificates
|
||||
// will not be renewed, OCSP staples will not be updated, etc.
|
||||
func Deactivate() (err error) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
err = errors.New("already deactivated")
|
||||
}
|
||||
}()
|
||||
close(stopChan)
|
||||
stopChan = make(chan struct{})
|
||||
return
|
||||
}
|
||||
|
||||
// MarkQualified scans each config and, if it qualifies for managed
|
||||
// TLS, it sets the Managed field of the TLSConfig to true.
|
||||
func MarkQualified(configs []server.Config) {
|
||||
for i := 0; i < len(configs); i++ {
|
||||
if ConfigQualifies(configs[i]) {
|
||||
configs[i].TLS.Managed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCerts obtains certificates for all these configs as long as a
|
||||
// certificate does not already exist on disk. It does not modify the
|
||||
// configs at all; it only obtains and stores certificates and keys to
|
||||
// the disk. If allowPrompts is true, the user may be shown a prompt.
|
||||
// If proxyACME is true, the ACME challenges will be proxied to our alt port.
|
||||
func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
|
||||
// We group configs by email so we don't make the same clients over and
|
||||
// over. This has the potential to prompt the user for an email, but we
|
||||
// prevent that by assuming that if we already have a listener that can
|
||||
// proxy ACME challenge requests, then the server is already running and
|
||||
// the operator is no longer present.
|
||||
groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
|
||||
|
||||
for email, group := range groupedConfigs {
|
||||
// Wait as long as we can before creating the client, because it
|
||||
// may not be needed, for example, if we already have what we
|
||||
// need on disk. Creating a client involves the network and
|
||||
// potentially prompting the user, etc., so only do if necessary.
|
||||
var client *ACMEClient
|
||||
|
||||
for _, cfg := range group {
|
||||
if cfg.Host == "" || existingCertAndKey(cfg.Host) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Now we definitely do need a client
|
||||
if client == nil {
|
||||
var err error
|
||||
client, err = NewACMEClient(email, allowPrompts)
|
||||
if err != nil {
|
||||
return errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// c.Configure assumes that allowPrompts == !proxyACME,
|
||||
// but that's not always true. For example, a restart where
|
||||
// the user isn't present and we're not listening on port 80.
|
||||
// TODO: This could probably be refactored better.
|
||||
if proxyACME {
|
||||
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
|
||||
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
|
||||
} else {
|
||||
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
|
||||
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
|
||||
}
|
||||
|
||||
err := client.Obtain([]string{cfg.Host})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// groupConfigsByEmail groups configs by the email address to be used by an
|
||||
// ACME client. It only groups configs that have TLS enabled and that are
|
||||
// marked as Managed. If userPresent is true, the operator MAY be prompted
|
||||
// for an email address.
|
||||
func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
|
||||
initMap := make(map[string][]server.Config)
|
||||
for _, cfg := range configs {
|
||||
if !cfg.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
leEmail := getEmail(cfg, userPresent)
|
||||
initMap[leEmail] = append(initMap[leEmail], cfg)
|
||||
}
|
||||
return initMap
|
||||
}
|
||||
|
||||
// EnableTLS configures each config to use TLS according to default settings.
|
||||
// It will only change configs that are marked as managed, and assumes that
|
||||
// certificates and keys are already on disk. If loadCertificates is true,
|
||||
// the certificates will be loaded from disk into the cache for this process
|
||||
// to use. If false, TLS will still be enabled and configured with default
|
||||
// settings, but no certificates will be parsed loaded into the cache, and
|
||||
// the returned error value will always be nil.
|
||||
func EnableTLS(configs []server.Config, loadCertificates bool) error {
|
||||
for i := 0; i < len(configs); i++ {
|
||||
if !configs[i].TLS.Managed {
|
||||
continue
|
||||
}
|
||||
configs[i].TLS.Enabled = true
|
||||
if loadCertificates && configs[i].Host != "" {
|
||||
_, err := cacheManagedCertificate(configs[i].Host, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setDefaultTLSParams(&configs[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hostHasOtherPort returns true if there is another config in the list with the same
|
||||
// hostname that has port otherPort, or false otherwise. All the configs are checked
|
||||
// against the hostname of allConfigs[thisConfigIdx].
|
||||
func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == thisConfigIdx {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
|
||||
// hosts. You must pass in all configs, not just configs that qualify, since
|
||||
// we must know whether the same host already exists on port 80, and those would
|
||||
// not be in a list of configs that qualify for automatic HTTPS. This function will
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func MakePlaintextRedirects(allConfigs []server.Config) []server.Config {
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// ConfigQualifies returns true if cfg qualifies for
|
||||
// fully managed TLS (but not on-demand TLS, which is
|
||||
// not considered here). It does NOT check to see if a
|
||||
// cert and key already exist for the config. If the
|
||||
// config does qualify, you should set cfg.TLS.Managed
|
||||
// to true and check that instead, because the process of
|
||||
// setting up the config may make it look like it
|
||||
// doesn't qualify even though it originally did.
|
||||
func ConfigQualifies(cfg server.Config) bool {
|
||||
return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
|
||||
|
||||
// user can force-disable automatic HTTPS for this host
|
||||
cfg.Scheme != "http" &&
|
||||
cfg.Port != "80" &&
|
||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
||||
|
||||
// we get can't certs for some kinds of hostnames, but
|
||||
// on-demand TLS allows empty hostnames at startup
|
||||
(HostQualifies(cfg.Host) || cfg.TLS.OnDemand)
|
||||
}
|
||||
|
||||
// HostQualifies returns true if the hostname alone
|
||||
// appears eligible for automatic HTTPS. For example,
|
||||
// localhost, empty hostname, and IP addresses are
|
||||
// not eligible because we cannot obtain certificates
|
||||
// for those names.
|
||||
func HostQualifies(hostname string) bool {
|
||||
return hostname != "localhost" && // localhost is ineligible
|
||||
|
||||
// hostname must not be empty
|
||||
strings.TrimSpace(hostname) != "" &&
|
||||
|
||||
// cannot be an IP address, see
|
||||
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
||||
// (also trim [] from either end, since that special case can sneak through
|
||||
// for IPv6 addresses using the -host flag and with empty/no Caddyfile)
|
||||
net.ParseIP(strings.Trim(hostname, "[]")) == nil
|
||||
}
|
||||
|
||||
// existingCertAndKey returns true if the host has a certificate
|
||||
// and private key in storage already, false otherwise.
|
||||
func existingCertAndKey(host string) bool {
|
||||
_, err := os.Stat(storage.SiteCertFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(storage.SiteKeyFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// saveCertResource saves the certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func saveCertResource(cert acme.CertificateResource) error {
|
||||
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert
|
||||
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key
|
||||
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert metadata
|
||||
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on port 80.
|
||||
func redirPlaintextHost(cfg server.Config) server.Config {
|
||||
toURL := "https://{host}" // serve any host, since cfg.Host could be empty
|
||||
if cfg.Port != "443" && cfg.Port != "80" {
|
||||
toURL += ":" + cfg.Port
|
||||
}
|
||||
|
||||
redirMidware := func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: []redirect.Rule{
|
||||
{
|
||||
FromScheme: "http",
|
||||
FromPath: "/",
|
||||
To: toURL + "{uri}",
|
||||
Code: http.StatusMovedPermanently,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
return server.Config{
|
||||
Host: cfg.Host,
|
||||
BindHost: cfg.BindHost,
|
||||
Port: "80",
|
||||
Middleware: []middleware.Middleware{redirMidware},
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for host via ACME protocol.
|
||||
func Revoke(host string) error {
|
||||
if !existingCertAndKey(host) {
|
||||
return errors.New("no certificate and key for " + host)
|
||||
}
|
||||
|
||||
email := getEmail(server.Config{Host: host}, true)
|
||||
if email == "" {
|
||||
return errors.New("email is required to revoke")
|
||||
}
|
||||
|
||||
client, err := NewACMEClient(email, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile := storage.SiteCertFile(host)
|
||||
certBytes, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.RevokeCertificate(certBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Remove(certFile)
|
||||
if err != nil {
|
||||
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultEmail represents the Let's Encrypt account email to use if none provided
|
||||
DefaultEmail string
|
||||
|
||||
// Agreed indicates whether user has agreed to the Let's Encrypt SA
|
||||
Agreed bool
|
||||
|
||||
// CAUrl represents the base URL to the CA's ACME endpoint
|
||||
CAUrl string
|
||||
)
|
||||
|
||||
// AlternatePort is the port on which the acme client will open a
|
||||
// listener and solve the CA's challenges. If this alternate port
|
||||
// is used instead of the default port (80 or 443), then the
|
||||
// default port for the challenge must be forwarded to this one.
|
||||
const AlternatePort = "5033"
|
||||
|
||||
// KeySize represents the length of a key in bits.
|
||||
type KeySize int
|
||||
|
||||
// Key sizes are used to determine the strength of a key.
|
||||
const (
|
||||
Ecc224 KeySize = 224
|
||||
Ecc256 = 256
|
||||
Rsa2048 = 2048
|
||||
Rsa4096 = 4096
|
||||
)
|
||||
|
||||
// rsaKeySizeToUse is the size to use for new RSA keys.
|
||||
// This shouldn't need to change except for in tests;
|
||||
// the size can be drastically reduced for speed.
|
||||
var rsaKeySizeToUse = Rsa2048
|
||||
|
||||
// stopChan is used to signal the maintenance goroutine
|
||||
// to terminate.
|
||||
var stopChan chan struct{}
|
||||
@@ -1,332 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func TestHostQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
host string
|
||||
expect bool
|
||||
}{
|
||||
{"localhost", false},
|
||||
{"127.0.0.1", false},
|
||||
{"127.0.1.5", false},
|
||||
{"::1", false},
|
||||
{"[::1]", false},
|
||||
{"[::]", false},
|
||||
{"::", false},
|
||||
{"", false},
|
||||
{" ", false},
|
||||
{"0.0.0.0", false},
|
||||
{"192.168.1.3", false},
|
||||
{"10.0.2.1", false},
|
||||
{"169.112.53.4", false},
|
||||
{"foobar.com", true},
|
||||
{"sub.foobar.com", true},
|
||||
} {
|
||||
if HostQualifies(test.host) && !test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
|
||||
}
|
||||
if !HostQualifies(test.host) && test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
cfg server.Config
|
||||
expect bool
|
||||
}{
|
||||
{server.Config{Host: ""}, false},
|
||||
{server.Config{Host: "localhost"}, false},
|
||||
{server.Config{Host: "123.44.3.21"}, false},
|
||||
{server.Config{Host: "example.com"}, true},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true},
|
||||
{server.Config{Host: "example.com", Scheme: "http"}, false},
|
||||
{server.Config{Host: "example.com", Port: "80"}, false},
|
||||
{server.Config{Host: "example.com", Port: "1234"}, true},
|
||||
{server.Config{Host: "example.com", Scheme: "https"}, true},
|
||||
{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false},
|
||||
} {
|
||||
if test.expect && !ConfigQualifies(test.cfg) {
|
||||
t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg)
|
||||
}
|
||||
if !test.expect && ConfigQualifies(test.cfg) {
|
||||
t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(server.Config{
|
||||
Host: "example.com",
|
||||
BindHost: "93.184.216.34",
|
||||
Port: "1234",
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected {
|
||||
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Port, "80"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.Middleware == nil || len(cfg.Middleware) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
|
||||
}
|
||||
|
||||
handler, ok := cfg.Middleware[0](nil).(redirect.Redirect)
|
||||
if !ok {
|
||||
t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
|
||||
}
|
||||
if len(handler.Rules) != 1 {
|
||||
t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
|
||||
}
|
||||
|
||||
// Check redirect rule for correctness
|
||||
if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
|
||||
t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
|
||||
}
|
||||
|
||||
// browsers can infer a default port from scheme, so make sure the port
|
||||
// doesn't get added in explicitly for default ports like 443 for https.
|
||||
cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"})
|
||||
handler, ok = cfg.Middleware[0](nil).(redirect.Redirect)
|
||||
if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected {
|
||||
t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCertResource(t *testing.T) {
|
||||
storage = Storage("./le_test_save")
|
||||
defer func() {
|
||||
err := os.RemoveAll(string(storage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
certContents := "certificate"
|
||||
keyContents := "private key"
|
||||
metaContents := `{
|
||||
"domain": "example.com",
|
||||
"certUrl": "https://example.com/cert",
|
||||
"certStableUrl": "https://example.com/cert/stable"
|
||||
}`
|
||||
|
||||
cert := acme.CertificateResource{
|
||||
Domain: domain,
|
||||
CertURL: "https://example.com/cert",
|
||||
CertStableURL: "https://example.com/cert/stable",
|
||||
PrivateKey: []byte(keyContents),
|
||||
Certificate: []byte(certContents),
|
||||
}
|
||||
|
||||
err := saveCertResource(cert)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading certificate file, got: %v", err)
|
||||
}
|
||||
if string(certFile) != certContents {
|
||||
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
|
||||
}
|
||||
|
||||
keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading private key file, got: %v", err)
|
||||
}
|
||||
if string(keyFile) != keyContents {
|
||||
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
|
||||
}
|
||||
|
||||
metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading meta file, got: %v", err)
|
||||
}
|
||||
if string(metaFile) != metaContents {
|
||||
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingCertAndKey(t *testing.T) {
|
||||
storage = Storage("./le_test_existing")
|
||||
defer func() {
|
||||
err := os.RemoveAll(string(storage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
|
||||
if existingCertAndKey(domain) {
|
||||
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
|
||||
}
|
||||
|
||||
err := saveCertResource(acme.CertificateResource{
|
||||
Domain: domain,
|
||||
PrivateKey: []byte("key"),
|
||||
Certificate: []byte("cert"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if !existingCertAndKey(domain) {
|
||||
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHasOtherPort(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", Port: "80"},
|
||||
{Host: "sub1.example.com", Port: "80"},
|
||||
{Host: "sub1.example.com", Port: "443"},
|
||||
}
|
||||
|
||||
if hostHasOtherPort(configs, 0, "80") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
|
||||
}
|
||||
if hostHasOtherPort(configs, 0, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
|
||||
}
|
||||
if !hostHasOtherPort(configs, 1, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePlaintextRedirects(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
// Happy path = standard redirect from 80 to 443
|
||||
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Host on port 80 already defined; don't change it (no redirect)
|
||||
{Host: "sub1.example.com", Port: "80", Scheme: "http"},
|
||||
{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Redirect from port 80 to port 5000 in this case
|
||||
{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Can redirect from 80 to either 443 or 5001, but choose 443
|
||||
{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}},
|
||||
{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}},
|
||||
}
|
||||
|
||||
result := MakePlaintextRedirects(configs)
|
||||
expectedRedirCount := 3
|
||||
|
||||
if len(result) != len(configs)+expectedRedirCount {
|
||||
t.Errorf("Expected %d redirect(s) to be added, but got %d",
|
||||
expectedRedirCount, len(result)-len(configs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableTLS(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
EnableTLS(configs, false)
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
}
|
||||
if configs[1].TLS.Enabled {
|
||||
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupConfigsByEmail(t *testing.T) {
|
||||
if groupConfigsByEmail([]server.Config{}, false) == nil {
|
||||
t.Errorf("With empty input, returned map was nil, but expected non-nil map")
|
||||
}
|
||||
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
|
||||
{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
|
||||
{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed
|
||||
}
|
||||
DefaultEmail = "test@example.com"
|
||||
|
||||
groups := groupConfigsByEmail(configs, true)
|
||||
|
||||
if groups == nil {
|
||||
t.Fatalf("Returned map was nil, but expected values")
|
||||
}
|
||||
|
||||
if len(groups) != 2 {
|
||||
t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups)
|
||||
}
|
||||
if len(groups["foo@bar"]) != 2 {
|
||||
t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"])
|
||||
}
|
||||
if len(groups[DefaultEmail]) != 3 {
|
||||
t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkQualified(t *testing.T) {
|
||||
// TODO: TestConfigQualifies and this test share the same config list...
|
||||
configs := []server.Config{
|
||||
{Host: ""},
|
||||
{Host: "localhost"},
|
||||
{Host: "123.44.3.21"},
|
||||
{Host: "example.com"},
|
||||
{Host: "example.com", TLS: server.TLSConfig{Manual: true}},
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}},
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}},
|
||||
{Host: "example.com", Scheme: "http"},
|
||||
{Host: "example.com", Port: "80"},
|
||||
{Host: "example.com", Port: "1234"},
|
||||
{Host: "example.com", Scheme: "https"},
|
||||
{Host: "example.com", Port: "80", Scheme: "https"},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
MarkQualified(configs)
|
||||
|
||||
count := 0
|
||||
for _, cfg := range configs {
|
||||
if cfg.TLS.Managed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != expectedManagedCount {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
const (
|
||||
// RenewInterval is how often to check certificates for renewal.
|
||||
RenewInterval = 12 * time.Hour
|
||||
|
||||
// OCSPInterval is how often to check if OCSP stapling needs updating.
|
||||
OCSPInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
// maintainAssets is a permanently-blocking function
|
||||
// that loops indefinitely and, on a regular schedule, checks
|
||||
// certificates for expiration and initiates a renewal of certs
|
||||
// that are expiring soon. It also updates OCSP stapling and
|
||||
// performs other maintenance of assets.
|
||||
//
|
||||
// You must pass in the channel which you'll close when
|
||||
// maintenance should stop, to allow this goroutine to clean up
|
||||
// after itself and unblock.
|
||||
func maintainAssets(stopChan chan struct{}) {
|
||||
renewalTicker := time.NewTicker(RenewInterval)
|
||||
ocspTicker := time.NewTicker(OCSPInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-renewalTicker.C:
|
||||
log.Println("[INFO] Scanning for expiring certificates")
|
||||
renewManagedCertificates(false)
|
||||
log.Println("[INFO] Done checking certificates")
|
||||
case <-ocspTicker.C:
|
||||
log.Println("[INFO] Scanning for stale OCSP staples")
|
||||
updateOCSPStaples()
|
||||
log.Println("[INFO] Done checking OCSP staples")
|
||||
case <-stopChan:
|
||||
renewalTicker.Stop()
|
||||
ocspTicker.Stop()
|
||||
log.Println("[INFO] Stopped background maintenance routine")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renewManagedCertificates(allowPrompts bool) (err error) {
|
||||
var renewed, deleted []Certificate
|
||||
var client *ACMEClient
|
||||
visitedNames := make(map[string]struct{})
|
||||
|
||||
certCacheMu.RLock()
|
||||
for name, cert := range certCache {
|
||||
if !cert.Managed {
|
||||
continue
|
||||
}
|
||||
|
||||
// the list of names on this cert should never be empty...
|
||||
if cert.Names == nil || len(cert.Names) == 0 {
|
||||
log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v", name, cert.Names)
|
||||
deleted = append(deleted, cert)
|
||||
continue
|
||||
}
|
||||
|
||||
// skip names whose certificate we've already renewed
|
||||
if _, ok := visitedNames[name]; ok {
|
||||
continue
|
||||
}
|
||||
for _, name := range cert.Names {
|
||||
visitedNames[name] = struct{}{}
|
||||
}
|
||||
|
||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||
if timeLeft < renewDurationBefore {
|
||||
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
|
||||
|
||||
if client == nil {
|
||||
client, err = NewACMEClientGetEmail(server.Config{}, allowPrompts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client.Configure("") // TODO: Bind address of relevant listener, yuck
|
||||
}
|
||||
|
||||
err := client.Renew(cert.Names[0]) // managed certs better have only one name
|
||||
if err != nil {
|
||||
if client.AllowPrompts {
|
||||
// User is present, so stop immediately and report the error
|
||||
certCacheMu.RUnlock()
|
||||
return err
|
||||
}
|
||||
log.Printf("[ERROR] %v", err)
|
||||
if cert.OnDemand {
|
||||
deleted = append(deleted, cert)
|
||||
}
|
||||
} else {
|
||||
renewed = append(renewed, cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
certCacheMu.RUnlock()
|
||||
|
||||
// Apply changes to the cache
|
||||
for _, cert := range renewed {
|
||||
_, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand)
|
||||
if err != nil {
|
||||
if client.AllowPrompts {
|
||||
return err // operator is present, so report error immediately
|
||||
}
|
||||
log.Printf("[ERROR] %v", err)
|
||||
}
|
||||
}
|
||||
for _, cert := range deleted {
|
||||
certCacheMu.Lock()
|
||||
for _, name := range cert.Names {
|
||||
delete(certCache, name)
|
||||
}
|
||||
certCacheMu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateOCSPStaples() {
|
||||
// Create a temporary place to store updates
|
||||
// until we release the potentially long-lived
|
||||
// read lock and use a short-lived write lock.
|
||||
type ocspUpdate struct {
|
||||
rawBytes []byte
|
||||
parsed *ocsp.Response
|
||||
}
|
||||
updated := make(map[string]ocspUpdate)
|
||||
|
||||
// A single SAN certificate maps to multiple names, so we use this
|
||||
// set to make sure we don't waste cycles checking OCSP for the same
|
||||
// certificate multiple times.
|
||||
visited := make(map[string]struct{})
|
||||
|
||||
certCacheMu.RLock()
|
||||
for name, cert := range certCache {
|
||||
// skip this certificate if we've already visited it,
|
||||
// and if not, mark all the names as visited
|
||||
if _, ok := visited[name]; ok {
|
||||
continue
|
||||
}
|
||||
for _, n := range cert.Names {
|
||||
visited[n] = struct{}{}
|
||||
}
|
||||
|
||||
// no point in updating OCSP for expired certificates
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
continue
|
||||
}
|
||||
|
||||
var lastNextUpdate time.Time
|
||||
if cert.OCSP != nil {
|
||||
// start checking OCSP staple about halfway through validity period for good measure
|
||||
lastNextUpdate = cert.OCSP.NextUpdate
|
||||
refreshTime := cert.OCSP.ThisUpdate.Add(lastNextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
|
||||
|
||||
// since OCSP is already stapled, we need only check if we're in that "refresh window"
|
||||
if time.Now().Before(refreshTime) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err := stapleOCSP(&cert, nil)
|
||||
if err != nil {
|
||||
if cert.OCSP != nil {
|
||||
// if it was no staple before, that's fine, otherwise we should log the error
|
||||
log.Printf("[ERROR] Checking OCSP for %s: %v", name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// By this point, we've obtained the latest OCSP response.
|
||||
// If there was no staple before, or if the response is updated, make
|
||||
// sure we apply the update to all names on the certificate.
|
||||
if lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate {
|
||||
log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s",
|
||||
cert.Names, lastNextUpdate, cert.OCSP.NextUpdate)
|
||||
for _, n := range cert.Names {
|
||||
updated[n] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP}
|
||||
}
|
||||
}
|
||||
}
|
||||
certCacheMu.RUnlock()
|
||||
|
||||
// This write lock should be brief since we have all the info we need now.
|
||||
certCacheMu.Lock()
|
||||
for name, update := range updated {
|
||||
cert := certCache[name]
|
||||
cert.OCSP = update.parsed
|
||||
cert.Certificate.OCSPStaple = update.rawBytes
|
||||
certCache[name] = cert
|
||||
}
|
||||
certCacheMu.Unlock()
|
||||
}
|
||||
|
||||
// renewDurationBefore is how long before expiration to renew certificates.
|
||||
const renewDurationBefore = (24 * time.Hour) * 30
|
||||
@@ -1,318 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Setup sets up the TLS configuration and installs certificates that
|
||||
// are specified by the user in the config file. All the automatic HTTPS
|
||||
// stuff comes later outside of this function.
|
||||
func Setup(c *setup.Controller) (middleware.Middleware, error) {
|
||||
if c.Port == "80" || c.Scheme == "http" {
|
||||
c.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address())
|
||||
return nil, nil
|
||||
}
|
||||
c.TLS.Enabled = true
|
||||
|
||||
for c.Next() {
|
||||
var certificateFile, keyFile, loadDir, maxCerts string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 1:
|
||||
c.TLS.LetsEncryptEmail = args[0]
|
||||
|
||||
// user can force-disable managed TLS this way
|
||||
if c.TLS.LetsEncryptEmail == "off" {
|
||||
c.TLS.Enabled = false
|
||||
return nil, nil
|
||||
}
|
||||
case 2:
|
||||
certificateFile = args[0]
|
||||
keyFile = args[1]
|
||||
c.TLS.Manual = true
|
||||
}
|
||||
|
||||
// Optional block with extra parameters
|
||||
var hadBlock bool
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
switch c.Val() {
|
||||
case "protocols":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value, ok := supportedProtocols[strings.ToLower(args[0])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMinVersion = value
|
||||
value, ok = supportedProtocols[strings.ToLower(args[1])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMaxVersion = value
|
||||
case "ciphers":
|
||||
for c.NextArg() {
|
||||
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
|
||||
}
|
||||
case "clients":
|
||||
c.TLS.ClientCerts = c.RemainingArgs()
|
||||
if len(c.TLS.ClientCerts) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
case "load":
|
||||
c.Args(&loadDir)
|
||||
c.TLS.Manual = true
|
||||
case "max_certs":
|
||||
c.Args(&maxCerts)
|
||||
c.TLS.OnDemand = true
|
||||
default:
|
||||
return nil, c.Errf("Unknown keyword '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
// tls requires at least one argument if a block is not opened
|
||||
if len(args) == 0 && !hadBlock {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
// set certificate limit if on-demand TLS is enabled
|
||||
if maxCerts != "" {
|
||||
maxCertsNum, err := strconv.Atoi(maxCerts)
|
||||
if err != nil || maxCertsNum < 1 {
|
||||
return nil, c.Err("max_certs must be a positive integer")
|
||||
}
|
||||
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
|
||||
onDemandMaxIssue = int32(maxCertsNum)
|
||||
}
|
||||
}
|
||||
|
||||
// don't try to load certificates unless we're supposed to
|
||||
if !c.TLS.Enabled || !c.TLS.Manual {
|
||||
continue
|
||||
}
|
||||
|
||||
// load a single certificate and key, if specified
|
||||
if certificateFile != "" && keyFile != "" {
|
||||
err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
|
||||
}
|
||||
|
||||
// load a directory of certificates, if specified
|
||||
if loadDir != "" {
|
||||
err := loadCertsInDir(c, loadDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultTLSParams(c.Config)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// loadCertsInDir loads all the certificates/keys in dir, as long as
|
||||
// the file ends with .pem. This method of loading certificates is
|
||||
// modeled after haproxy, which expects the certificate and key to
|
||||
// be bundled into the same file:
|
||||
// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
|
||||
//
|
||||
// This function may write to the log as it walks the directory tree.
|
||||
func loadCertsInDir(c *setup.Controller, dir string) error {
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
|
||||
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
|
||||
var foundKey bool // use only the first key in the file
|
||||
|
||||
bundle, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
// Decode next block so we can see what type it is
|
||||
var derBlock *pem.Block
|
||||
derBlock, bundle = pem.Decode(bundle)
|
||||
if derBlock == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if derBlock.Type == "CERTIFICATE" {
|
||||
// Re-encode certificate as PEM, appending to certificate chain
|
||||
pem.Encode(certBuilder, derBlock)
|
||||
} else if derBlock.Type == "EC PARAMETERS" {
|
||||
// EC keys generated from openssl can be composed of two blocks:
|
||||
// parameters and key (parameter block should come first)
|
||||
if !foundKey {
|
||||
// Encode parameters
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
|
||||
// Key must immediately follow
|
||||
derBlock, bundle = pem.Decode(bundle)
|
||||
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
|
||||
return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
|
||||
}
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
foundKey = true
|
||||
}
|
||||
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
|
||||
// RSA key
|
||||
if !foundKey {
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
foundKey = true
|
||||
}
|
||||
} else {
|
||||
return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
|
||||
}
|
||||
}
|
||||
|
||||
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
|
||||
if len(certPEMBytes) == 0 {
|
||||
return c.Errf("%s: failed to parse PEM data", path)
|
||||
}
|
||||
if len(keyPEMBytes) == 0 {
|
||||
return c.Errf("%s: no private key block found", path)
|
||||
}
|
||||
|
||||
err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
|
||||
if err != nil {
|
||||
return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
|
||||
// and server preferences of a server.Config if they were not previously set
|
||||
// (it does not overwrite; only fills in missing values). It will also set the
|
||||
// port to 443 if not already set, TLS is enabled, TLS is manual, and the host
|
||||
// does not equal localhost.
|
||||
func setDefaultTLSParams(c *server.Config) {
|
||||
// If no ciphers provided, use default list
|
||||
if len(c.TLS.Ciphers) == 0 {
|
||||
c.TLS.Ciphers = defaultCiphers
|
||||
}
|
||||
|
||||
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
|
||||
// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
|
||||
c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...)
|
||||
|
||||
// Set default protocol min and max versions - must balance compatibility and security
|
||||
if c.TLS.ProtocolMinVersion == 0 {
|
||||
c.TLS.ProtocolMinVersion = tls.VersionTLS10
|
||||
}
|
||||
if c.TLS.ProtocolMaxVersion == 0 {
|
||||
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
// Prefer server cipher suites
|
||||
c.TLS.PreferServerCipherSuites = true
|
||||
|
||||
// Default TLS port is 443; only use if port is not manually specified,
|
||||
// TLS is enabled, and the host is not localhost
|
||||
if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" {
|
||||
c.Port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// Map of supported protocols.
|
||||
// SSLv3 will be not supported in future release.
|
||||
// HTTP/2 only supports TLS 1.2 and higher.
|
||||
var supportedProtocols = map[string]uint16{
|
||||
"ssl3.0": tls.VersionSSL30,
|
||||
"tls1.0": tls.VersionTLS10,
|
||||
"tls1.1": tls.VersionTLS11,
|
||||
"tls1.2": tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Map of supported ciphers, used only for parsing config.
|
||||
//
|
||||
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
|
||||
// including all but two of the suites below (the two GCM suites).
|
||||
// See https://http2.github.io/http2-spec/#BadCipherSuites
|
||||
//
|
||||
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
|
||||
// it is always added (even though it is not technically a cipher suite).
|
||||
//
|
||||
// This map, like any map, is NOT ORDERED. Do not range over this map.
|
||||
var supportedCiphersMap = map[string]uint16{
|
||||
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of supported cipher suites in descending order of preference.
|
||||
// Ordering is very important! Getting the wrong order will break
|
||||
// mainstream clients, especially with HTTP/2.
|
||||
//
|
||||
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
|
||||
// added manually.
|
||||
var supportedCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of all the ciphers we want to use by default
|
||||
var defaultCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Write test certificates to disk before tests, and clean up
|
||||
// when we're done.
|
||||
err := ioutil.WriteFile(certFile, testCert, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(keyFile, testKey, 0644)
|
||||
if err != nil {
|
||||
os.Remove(certFile)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
result := m.Run()
|
||||
|
||||
os.Remove(certFile)
|
||||
os.Remove(keyFile)
|
||||
os.Exit(result)
|
||||
}
|
||||
|
||||
func TestSetupParseBasic(t *testing.T) {
|
||||
c := setup.NewTestController(`tls ` + certFile + ` ` + keyFile + ``)
|
||||
|
||||
_, err := Setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
// Basic checks
|
||||
if !c.TLS.Manual {
|
||||
t.Error("Expected TLS Manual=true, but was false")
|
||||
}
|
||||
if !c.TLS.Enabled {
|
||||
t.Error("Expected TLS Enabled=true, but was false")
|
||||
}
|
||||
|
||||
// Security defaults
|
||||
if c.TLS.ProtocolMinVersion != tls.VersionTLS10 {
|
||||
t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
|
||||
}
|
||||
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion)
|
||||
}
|
||||
|
||||
// Cipher checks
|
||||
expectedCiphers := []uint16{
|
||||
tls.TLS_FALLBACK_SCSV,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
}
|
||||
|
||||
// Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
|
||||
if len(c.TLS.Ciphers) != len(expectedCiphers) {
|
||||
t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
|
||||
len(expectedCiphers), len(c.TLS.Ciphers))
|
||||
}
|
||||
|
||||
// Ensure ordering is correct
|
||||
for i, actual := range c.TLS.Ciphers {
|
||||
if actual != expectedCiphers[i] {
|
||||
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
|
||||
}
|
||||
}
|
||||
|
||||
if !c.TLS.PreferServerCipherSuites {
|
||||
t.Error("Expected PreferServerCipherSuites = true, but was false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseIncompleteParams(t *testing.T) {
|
||||
// Using tls without args is an error because it's unnecessary.
|
||||
c := setup.NewTestController(`tls`)
|
||||
_, err := Setup(c)
|
||||
if err == nil {
|
||||
t.Error("Expected an error, but didn't get one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithOptionalParams(t *testing.T) {
|
||||
params := `tls ` + certFile + ` ` + keyFile + ` {
|
||||
protocols ssl3.0 tls1.2
|
||||
ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384
|
||||
}`
|
||||
c := setup.NewTestController(params)
|
||||
|
||||
_, err := Setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if c.TLS.ProtocolMinVersion != tls.VersionSSL30 {
|
||||
t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
|
||||
}
|
||||
|
||||
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion)
|
||||
}
|
||||
|
||||
if len(c.TLS.Ciphers)-1 != 3 {
|
||||
t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupDefaultWithOptionalParams(t *testing.T) {
|
||||
params := `tls {
|
||||
ciphers RSA-3DES-EDE-CBC-SHA
|
||||
}`
|
||||
c := setup.NewTestController(params)
|
||||
|
||||
_, err := Setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
if len(c.TLS.Ciphers)-1 != 1 {
|
||||
t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: If we allow this... but probably not a good idea.
|
||||
// func TestSetupDisableHTTPRedirect(t *testing.T) {
|
||||
// c := NewTestController(`tls {
|
||||
// allow_http
|
||||
// }`)
|
||||
// _, err := TLS(c)
|
||||
// if err != nil {
|
||||
// t.Errorf("Expected no error, but got %v", err)
|
||||
// }
|
||||
// if !c.TLS.DisableHTTPRedir {
|
||||
// t.Error("Expected HTTP redirect to be disabled, but it wasn't")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestSetupParseWithWrongOptionalParams(t *testing.T) {
|
||||
// Test protocols wrong params
|
||||
params := `tls ` + certFile + ` ` + keyFile + ` {
|
||||
protocols ssl tls
|
||||
}`
|
||||
c := setup.NewTestController(params)
|
||||
_, err := Setup(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected errors, but no error returned")
|
||||
}
|
||||
|
||||
// Test ciphers wrong params
|
||||
params = `tls ` + certFile + ` ` + keyFile + ` {
|
||||
ciphers not-valid-cipher
|
||||
}`
|
||||
c = setup.NewTestController(params)
|
||||
_, err = Setup(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected errors, but no error returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParseWithClientAuth(t *testing.T) {
|
||||
params := `tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients client_ca.crt client2_ca.crt
|
||||
}`
|
||||
c := setup.NewTestController(params)
|
||||
_, err := Setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if count := len(c.TLS.ClientCerts); count != 2 {
|
||||
t.Fatalf("Expected two client certs, had %d", count)
|
||||
}
|
||||
if actual := c.TLS.ClientCerts[0]; actual != "client_ca.crt" {
|
||||
t.Errorf("Expected first client cert file to be '%s', but was '%s'", "client_ca.crt", actual)
|
||||
}
|
||||
if actual := c.TLS.ClientCerts[1]; actual != "client2_ca.crt" {
|
||||
t.Errorf("Expected second client cert file to be '%s', but was '%s'", "client2_ca.crt", actual)
|
||||
}
|
||||
|
||||
// Test missing client cert file
|
||||
params = `tls ` + certFile + ` ` + keyFile + ` {
|
||||
clients
|
||||
}`
|
||||
c = setup.NewTestController(params)
|
||||
_, err = Setup(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error, but no error returned")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
certFile = "test_cert.pem"
|
||||
keyFile = "test_key.pem"
|
||||
)
|
||||
|
||||
var testCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIBkjCCATmgAwIBAgIJANfFCBcABL6LMAkGByqGSM49BAEwFDESMBAGA1UEAxMJ
|
||||
bG9jYWxob3N0MB4XDTE2MDIxMDIyMjAyNFoXDTE4MDIwOTIyMjAyNFowFDESMBAG
|
||||
A1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs22MtnG7
|
||||
9K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDLSiVQvFZ6lUszTlczNxVk
|
||||
pEfqrM6xAupB7qN1MHMwHQYDVR0OBBYEFHxYDvAxUwL4XrjPev6qZ/BiLDs5MEQG
|
||||
A1UdIwQ9MDuAFHxYDvAxUwL4XrjPev6qZ/BiLDs5oRikFjAUMRIwEAYDVQQDEwls
|
||||
b2NhbGhvc3SCCQDXxQgXAAS+izAMBgNVHRMEBTADAQH/MAkGByqGSM49BAEDSAAw
|
||||
RQIgRvBqbyJM2JCJqhA1FmcoZjeMocmhxQHTt1c+1N2wFUgCIQDtvrivbBPA688N
|
||||
Qh3sMeAKNKPsx5NxYdoWuu9KWcKz9A==
|
||||
-----END CERTIFICATE-----
|
||||
`)
|
||||
|
||||
var testKey = []byte(`-----BEGIN EC PARAMETERS-----
|
||||
BggqhkjOPQMBBw==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIGLtRmwzYVcrH3J0BnzYbGPdWVF10i9p6mxkA4+b2fURoAoGCCqGSM49
|
||||
AwEHoUQDQgAEs22MtnG79K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDL
|
||||
SiVQvFZ6lUszTlczNxVkpEfqrM6xAupB7g==
|
||||
-----END EC PRIVATE KEY-----
|
||||
`)
|
||||
@@ -1,94 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/assets"
|
||||
)
|
||||
|
||||
// storage is used to get file paths in a consistent,
|
||||
// cross-platform way for persisting Let's Encrypt assets
|
||||
// on the file system.
|
||||
var storage = Storage(filepath.Join(assets.Path(), "letsencrypt"))
|
||||
|
||||
// Storage is a root directory and facilitates
|
||||
// forming file paths derived from it.
|
||||
type Storage string
|
||||
|
||||
// Sites gets the directory that stores site certificate and keys.
|
||||
func (s Storage) Sites() string {
|
||||
return filepath.Join(string(s), "sites")
|
||||
}
|
||||
|
||||
// Site returns the path to the folder containing assets for domain.
|
||||
func (s Storage) Site(domain string) string {
|
||||
return filepath.Join(s.Sites(), domain)
|
||||
}
|
||||
|
||||
// SiteCertFile returns the path to the certificate file for domain.
|
||||
func (s Storage) SiteCertFile(domain string) string {
|
||||
return filepath.Join(s.Site(domain), domain+".crt")
|
||||
}
|
||||
|
||||
// SiteKeyFile returns the path to domain's private key file.
|
||||
func (s Storage) SiteKeyFile(domain string) string {
|
||||
return filepath.Join(s.Site(domain), domain+".key")
|
||||
}
|
||||
|
||||
// SiteMetaFile returns the path to the domain's asset metadata file.
|
||||
func (s Storage) SiteMetaFile(domain string) string {
|
||||
return filepath.Join(s.Site(domain), domain+".json")
|
||||
}
|
||||
|
||||
// Users gets the directory that stores account folders.
|
||||
func (s Storage) Users() string {
|
||||
return filepath.Join(string(s), "users")
|
||||
}
|
||||
|
||||
// User gets the account folder for the user with email.
|
||||
func (s Storage) User(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
return filepath.Join(s.Users(), email)
|
||||
}
|
||||
|
||||
// UserRegFile gets the path to the registration file for
|
||||
// the user with the given email address.
|
||||
func (s Storage) UserRegFile(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
fileName := emailUsername(email)
|
||||
if fileName == "" {
|
||||
fileName = "registration"
|
||||
}
|
||||
return filepath.Join(s.User(email), fileName+".json")
|
||||
}
|
||||
|
||||
// UserKeyFile gets the path to the private key file for
|
||||
// the user with the given email address.
|
||||
func (s Storage) UserKeyFile(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
fileName := emailUsername(email)
|
||||
if fileName == "" {
|
||||
fileName = "private"
|
||||
}
|
||||
return filepath.Join(s.User(email), fileName+".key")
|
||||
}
|
||||
|
||||
// emailUsername returns the username portion of an
|
||||
// email address (part before '@') or the original
|
||||
// input if it can't find the "@" symbol.
|
||||
func emailUsername(email string) string {
|
||||
at := strings.Index(email, "@")
|
||||
if at == -1 {
|
||||
return email
|
||||
} else if at == 0 {
|
||||
return email[1:]
|
||||
}
|
||||
return email[:at]
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStorage(t *testing.T) {
|
||||
storage = Storage("./le_test")
|
||||
|
||||
if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected {
|
||||
t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("test.com"); actual != expected {
|
||||
t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected {
|
||||
t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected {
|
||||
t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected {
|
||||
t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected {
|
||||
t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("me@example.com"); actual != expected {
|
||||
t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected {
|
||||
t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected {
|
||||
t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test with empty emails
|
||||
if expected, actual := filepath.Join("le_test", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
|
||||
t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected {
|
||||
t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected {
|
||||
t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailUsername(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input, expect string
|
||||
}{
|
||||
{
|
||||
input: "username@example.com",
|
||||
expect: "username",
|
||||
},
|
||||
{
|
||||
input: "plus+addressing@example.com",
|
||||
expect: "plus+addressing",
|
||||
},
|
||||
{
|
||||
input: "me+plus-addressing@example.com",
|
||||
expect: "me+plus-addressing",
|
||||
},
|
||||
{
|
||||
input: "not-an-email",
|
||||
expect: "not-an-email",
|
||||
},
|
||||
{
|
||||
input: "@foobar.com",
|
||||
expect: "foobar.com",
|
||||
},
|
||||
{
|
||||
input: emptyEmail,
|
||||
expect: emptyEmail,
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expect: "",
|
||||
},
|
||||
} {
|
||||
if actual := emailUsername(test.input); actual != test.expect {
|
||||
t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// User represents a Let's Encrypt user account.
|
||||
type User struct {
|
||||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// GetEmail gets u's email.
|
||||
func (u User) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
// GetRegistration gets u's registration resource.
|
||||
func (u User) GetRegistration() *acme.RegistrationResource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
// GetPrivateKey gets u's private key.
|
||||
func (u User) GetPrivateKey() *rsa.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
// getUser loads the user with the given email from disk.
|
||||
// If the user does not exist, it will create a new one,
|
||||
// but it does NOT save new users to the disk or register
|
||||
// them via ACME. It does NOT prompt the user.
|
||||
func getUser(email string) (User, error) {
|
||||
var user User
|
||||
|
||||
// open user file
|
||||
regFile, err := os.Open(storage.UserRegFile(email))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// create a new user
|
||||
return newUser(email)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
defer regFile.Close()
|
||||
|
||||
// load user information
|
||||
err = json.NewDecoder(regFile).Decode(&user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// load their private key
|
||||
user.key, err = loadRSAPrivateKey(storage.UserKeyFile(email))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// saveUser persists a user's key and account registration
|
||||
// to the file system. It does NOT register the user via ACME
|
||||
// or prompt the user.
|
||||
func saveUser(user User) error {
|
||||
// make user account folder
|
||||
err := os.MkdirAll(storage.User(user.Email), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save private key file
|
||||
err = saveRSAPrivateKey(user.key, storage.UserKeyFile(user.Email))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save registration file
|
||||
jsonBytes, err := json.MarshalIndent(&user, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600)
|
||||
}
|
||||
|
||||
// newUser creates a new User for the given email address
|
||||
// with a new private key. This function does NOT save the
|
||||
// user to disk or register it via ACME. If you want to use
|
||||
// a user account that might already exist, call getUser
|
||||
// instead. It does NOT prompt the user.
|
||||
func newUser(email string) (User, error) {
|
||||
user := User{Email: email}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
|
||||
if err != nil {
|
||||
return user, errors.New("error generating private key: " + err.Error())
|
||||
}
|
||||
user.key = privateKey
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// getEmail does everything it can to obtain an email
|
||||
// address from the user to use for TLS for cfg. If it
|
||||
// cannot get an email address, it returns empty string.
|
||||
// (It will warn the user of the consequences of an
|
||||
// empty email.) This function MAY prompt the user for
|
||||
// input. If userPresent is false, the operator will
|
||||
// NOT be prompted and an empty email may be returned.
|
||||
func getEmail(cfg server.Config, userPresent bool) string {
|
||||
// First try the tls directive from the Caddyfile
|
||||
leEmail := cfg.TLS.LetsEncryptEmail
|
||||
if leEmail == "" {
|
||||
// Then try memory (command line flag or typed by user previously)
|
||||
leEmail = DefaultEmail
|
||||
}
|
||||
if leEmail == "" {
|
||||
// Then try to get most recent user email ~/.caddy/users file
|
||||
userDirs, err := ioutil.ReadDir(storage.Users())
|
||||
if err == nil {
|
||||
var mostRecent os.FileInfo
|
||||
for _, dir := range userDirs {
|
||||
if !dir.IsDir() {
|
||||
continue
|
||||
}
|
||||
if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
|
||||
leEmail = dir.Name()
|
||||
DefaultEmail = leEmail // save for next time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if leEmail == "" && userPresent {
|
||||
// Alas, we must bother the user and ask for an email address;
|
||||
// if they proceed they also agree to the SA.
|
||||
reader := bufio.NewReader(stdin)
|
||||
fmt.Println("\nYour sites will be served over HTTPS automatically using Let's Encrypt.")
|
||||
fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:")
|
||||
fmt.Println(" " + saURL) // TODO: Show current SA link
|
||||
fmt.Println("Please enter your email address so you can recover your account if needed.")
|
||||
fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.")
|
||||
fmt.Print("Email address: ")
|
||||
var err error
|
||||
leEmail, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
leEmail = strings.TrimSpace(leEmail)
|
||||
DefaultEmail = leEmail
|
||||
Agreed = true
|
||||
}
|
||||
return leEmail
|
||||
}
|
||||
|
||||
// promptUserAgreement prompts the user to agree to the agreement
|
||||
// at agreementURL via stdin. If the agreement has changed, then pass
|
||||
// true as the second argument. If this is the user's first time
|
||||
// agreeing, pass false. It returns whether the user agreed or not.
|
||||
func promptUserAgreement(agreementURL string, changed bool) bool {
|
||||
if changed {
|
||||
fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL)
|
||||
fmt.Print("Do you agree to the new terms? (y/n): ")
|
||||
} else {
|
||||
fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL)
|
||||
fmt.Print("Do you agree to the terms? (y/n): ")
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
answer = strings.ToLower(strings.TrimSpace(answer))
|
||||
|
||||
return answer == "y" || answer == "yes"
|
||||
}
|
||||
|
||||
// stdin is used to read the user's input if prompted;
|
||||
// this is changed by tests during tests.
|
||||
var stdin = io.ReadWriter(os.Stdin)
|
||||
|
||||
// The name of the folder for accounts where the email
|
||||
// address was not provided; default 'username' if you will.
|
||||
const emptyEmail = "default"
|
||||
|
||||
// TODO: Use latest
|
||||
const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
|
||||
@@ -1,196 +0,0 @@
|
||||
package https
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate test private key: %v", err)
|
||||
}
|
||||
u := User{
|
||||
Email: "me@mine.com",
|
||||
Registration: new(acme.RegistrationResource),
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
if expected, actual := "me@mine.com", u.GetEmail(); actual != expected {
|
||||
t.Errorf("Expected email '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if u.GetRegistration() == nil {
|
||||
t.Error("Expected a registration resource, but got nil")
|
||||
}
|
||||
if expected, actual := privateKey, u.GetPrivateKey(); actual != expected {
|
||||
t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUser(t *testing.T) {
|
||||
email := "me@foobar.com"
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
if user.key == nil {
|
||||
t.Error("Private key is nil")
|
||||
}
|
||||
if user.Email != email {
|
||||
t.Errorf("Expected email to be %s, but was %s", email, user.Email)
|
||||
}
|
||||
if user.Registration != nil {
|
||||
t.Error("New user already has a registration resource; it shouldn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUser(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
|
||||
email := "me@foobar.com"
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
|
||||
err = saveUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user: %v", err)
|
||||
}
|
||||
_, err = os.Stat(storage.UserRegFile(email))
|
||||
if err != nil {
|
||||
t.Errorf("Cannot access user registration file, error: %v", err)
|
||||
}
|
||||
_, err = os.Stat(storage.UserKeyFile(email))
|
||||
if err != nil {
|
||||
t.Errorf("Cannot access user private key file, error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserDoesNotAlreadyExist(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
|
||||
user, err := getUser("user_does_not_exist@foobar.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting user: %v", err)
|
||||
}
|
||||
|
||||
if user.key == nil {
|
||||
t.Error("Expected user to have a private key, but it was nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserAlreadyExists(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
|
||||
email := "me@foobar.com"
|
||||
|
||||
// Set up test
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
err = saveUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user: %v", err)
|
||||
}
|
||||
|
||||
// Expect to load user from disk
|
||||
user2, err := getUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting user: %v", err)
|
||||
}
|
||||
|
||||
// Assert keys are the same
|
||||
if !rsaPrivateKeysSame(user.key, user2.key) {
|
||||
t.Error("Expected private key to be the same after loading, but it wasn't")
|
||||
}
|
||||
|
||||
// Assert emails are the same
|
||||
if user.Email != user2.Email {
|
||||
t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEmail(t *testing.T) {
|
||||
// let's not clutter up the output
|
||||
origStdout := os.Stdout
|
||||
os.Stdout = nil
|
||||
defer func() { os.Stdout = origStdout }()
|
||||
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
DefaultEmail = "test2@foo.com"
|
||||
|
||||
// Test1: Use email in config
|
||||
config := server.Config{
|
||||
TLS: server.TLSConfig{
|
||||
LetsEncryptEmail: "test1@foo.com",
|
||||
},
|
||||
}
|
||||
actual := getEmail(config, true)
|
||||
if actual != "test1@foo.com" {
|
||||
t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "test1@foo.com", actual)
|
||||
}
|
||||
|
||||
// Test2: Use default email from flag (or user previously typing it)
|
||||
actual = getEmail(server.Config{}, true)
|
||||
if actual != DefaultEmail {
|
||||
t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual)
|
||||
}
|
||||
|
||||
// Test3: Get input from user
|
||||
DefaultEmail = ""
|
||||
stdin = new(bytes.Buffer)
|
||||
_, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not simulate user input, error: %v", err)
|
||||
}
|
||||
actual = getEmail(server.Config{}, true)
|
||||
if actual != "test3@foo.com" {
|
||||
t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual)
|
||||
}
|
||||
|
||||
// Test4: Get most recent email from before
|
||||
DefaultEmail = ""
|
||||
for i, eml := range []string{
|
||||
"test4-3@foo.com",
|
||||
"test4-2@foo.com",
|
||||
"test4-1@foo.com",
|
||||
} {
|
||||
u, err := newUser(eml)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user %d: %v", i, err)
|
||||
}
|
||||
err = saveUser(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Change modified time so they're all different, so the test becomes deterministic
|
||||
f, err := os.Stat(storage.User(eml))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not access user folder for '%s': %v", eml, err)
|
||||
}
|
||||
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
|
||||
if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil {
|
||||
t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err)
|
||||
}
|
||||
}
|
||||
actual = getEmail(server.Config{}, true)
|
||||
if actual != "test4-3@foo.com" {
|
||||
t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual)
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure and has
|
||||
// some really convenient methods.
|
||||
type Dispenser struct {
|
||||
filename string
|
||||
tokens []token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
|
||||
func NewDispenser(filename string, input io.Reader) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: allTokens(input),
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDispenserTokens returns a Dispenser filled with the given tokens.
|
||||
func NewDispenserTokens(filename string, tokens []token) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line. Returns true if a token was loaded; false
|
||||
// otherwise. If false, all tokens on the line have
|
||||
// been consumed. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].file == d.tokens[d.cursor+1].file &&
|
||||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file ||
|
||||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block. It returns true if a token was
|
||||
// loaded, or false when the block's closing curly brace
|
||||
// was loaded and thus the block ended. Nested blocks are
|
||||
// not supported.
|
||||
func (d *Dispenser) NextBlock() bool {
|
||||
if d.nesting > 0 {
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
d.nesting--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !d.NextArg() { // block must open on same line
|
||||
return false
|
||||
}
|
||||
if d.Val() != "{" {
|
||||
d.cursor-- // roll back if not opening brace
|
||||
return false
|
||||
}
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
// Open and then closed right away
|
||||
return false
|
||||
}
|
||||
d.nesting++
|
||||
return true
|
||||
}
|
||||
|
||||
// IncrNest adds a level of nesting to the dispenser.
|
||||
func (d *Dispenser) IncrNest() {
|
||||
d.nesting++
|
||||
return
|
||||
}
|
||||
|
||||
// Val gets the text of the current token. If there is no token
|
||||
// loaded, it returns empty string.
|
||||
func (d *Dispenser) Val() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token. If there is no token
|
||||
// loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].line
|
||||
}
|
||||
|
||||
// File gets the filename of the current token. If there is no token loaded,
|
||||
// it returns the filename originally given when parsing started.
|
||||
func (d *Dispenser) File() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return d.filename
|
||||
}
|
||||
if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" {
|
||||
return tokenFilename
|
||||
}
|
||||
return d.filename
|
||||
}
|
||||
|
||||
// Args is a convenience function that loads the next arguments
|
||||
// (tokens on the same line) into an arbitrary number of strings
|
||||
// pointed to in targets. If there are fewer tokens available
|
||||
// than string pointers, the remaining strings will not be changed
|
||||
// and false will be returned. If there were enough tokens available
|
||||
// to fill the arguments, then true will be returned.
|
||||
func (d *Dispenser) Args(targets ...*string) bool {
|
||||
enough := true
|
||||
for i := 0; i < len(targets); i++ {
|
||||
if !d.NextArg() {
|
||||
enough = false
|
||||
break
|
||||
}
|
||||
*targets[i] = d.Val()
|
||||
}
|
||||
return enough
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
d.cursor--
|
||||
break
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ArgErr returns an argument error, meaning that another
|
||||
// argument was expected but not found. In other words,
|
||||
// a line break or open curly brace was encountered instead of
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Parse error: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file ||
|
||||
d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexerTestCase struct {
|
||||
input string
|
||||
expected []token
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testCases := []lexerTestCase{
|
||||
{
|
||||
input: `host:123`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123
|
||||
|
||||
directive`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 3, text: "directive"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
directive
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 2, text: "directive"},
|
||||
{line: 3, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 { directive }`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 1, text: "directive"},
|
||||
{line: 1, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
#comment
|
||||
directive
|
||||
# comment
|
||||
foobar # another comment
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 3, text: "directive"},
|
||||
{line: 5, text: "foobar"},
|
||||
{line: 6, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `a "quoted value" b
|
||||
foobar`,
|
||||
expected: []token{
|
||||
{line: 1, text: "a"},
|
||||
{line: 1, text: "quoted value"},
|
||||
{line: 1, text: "b"},
|
||||
{line: 2, text: "foobar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted \"value\" inside" B`,
|
||||
expected: []token{
|
||||
{line: 1, text: "A"},
|
||||
{line: 1, text: `quoted "value" inside`},
|
||||
{line: 1, text: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `don't\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\\escape"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
break inside" {
|
||||
foobar
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "A"},
|
||||
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||
{line: 2, text: "{"},
|
||||
{line: 3, text: "foobar"},
|
||||
{line: 4, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"C:\php\php-cgi.exe"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `C:\php\php-cgi.exe`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `empty "" string`,
|
||||
expected: []token{
|
||||
{line: 1, text: `empty`},
|
||||
{line: 1, text: ``},
|
||||
{line: 1, text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "skip those\r\nCR characters",
|
||||
expected: []token{
|
||||
{line: 1, text: "skip"},
|
||||
{line: 1, text: "those"},
|
||||
{line: 2, text: "CR"},
|
||||
{line: 2, text: "characters"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := tokenize(testCase.input)
|
||||
lexerCompare(t, i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(input string) (tokens []token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lexerCompare(t *testing.T, n int, expected, actual []token) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||
}
|
||||
|
||||
for i := 0; i < len(actual) && i < len(expected); i++ {
|
||||
if actual[i].line != expected[i].line {
|
||||
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
||||
n, i, expected[i].text, expected[i].line, actual[i].line)
|
||||
break
|
||||
}
|
||||
if actual[i].text != expected[i].text {
|
||||
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
|
||||
n, i, expected[i].text, actual[i].text)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Package parse provides facilities for parsing configuration files.
|
||||
package parse
|
||||
|
||||
import "io"
|
||||
|
||||
// ServerBlocks parses the input just enough to organize tokens,
|
||||
// in order, by server block. No further parsing is performed.
|
||||
// If checkDirectives is true, only valid directives will be allowed
|
||||
// otherwise we consider it a parse error. Server blocks are returned
|
||||
// in the order in which they appear.
|
||||
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input)}
|
||||
p.checkDirectives = checkDirectives
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ValidDirectives is a set of directives that are valid (unordered). Populated
|
||||
// by config package's init function.
|
||||
var ValidDirectives = make(map[string]struct{})
|
||||
@@ -1,22 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type parser struct {
|
||||
Dispenser
|
||||
block ServerBlock // current server block being parsed
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
checkDirectives bool // if true, directives must be known
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
var blocks []ServerBlock
|
||||
|
||||
for p.Next() {
|
||||
err := p.parseOne()
|
||||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Addresses) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseOne() error {
|
||||
p.block = ServerBlock{Tokens: make(map[string][]token)}
|
||||
|
||||
err := p.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) begin() error {
|
||||
if len(p.tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := p.addresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.eof {
|
||||
// this happens if the Caddyfile consists of only
|
||||
// a line of addresses and nothing else
|
||||
return nil
|
||||
}
|
||||
|
||||
err = p.blockContents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) addresses() error {
|
||||
var expectingAnother bool
|
||||
|
||||
for {
|
||||
tkn := replaceEnvVars(p.Val())
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if tkn == "import" && p.isNewLine() {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Open brace definitely indicates end of addresses
|
||||
if tkn == "{" {
|
||||
if expectingAnother {
|
||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed "" in Caddyfile
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
if tkn[len(tkn)-1] == ',' {
|
||||
tkn = tkn[:len(tkn)-1]
|
||||
expectingAnother = true
|
||||
} else {
|
||||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
// Parse and save this address
|
||||
addr, err := standardAddress(tkn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.block.Addresses = append(p.block.Addresses, addr)
|
||||
}
|
||||
|
||||
// Advance token and possibly break out of loop or return error
|
||||
hasNext := p.Next()
|
||||
if expectingAnother && !hasNext {
|
||||
return p.EOFErr()
|
||||
}
|
||||
if !hasNext {
|
||||
p.eof = true
|
||||
break // EOF
|
||||
}
|
||||
if !expectingAnother && p.isNewLine() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) blockContents() error {
|
||||
errOpenCurlyBrace := p.openCurlyBrace()
|
||||
if errOpenCurlyBrace != nil {
|
||||
// single-server configs don't need curly braces
|
||||
p.cursor--
|
||||
}
|
||||
|
||||
err := p.directives()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only look for close curly brace if there was an opening
|
||||
if errOpenCurlyBrace == nil {
|
||||
err = p.closeCurlyBrace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// directives parses through all the lines for directives
|
||||
// and it expects the next token to be the first
|
||||
// directive. It goes until EOF or closing curly brace
|
||||
// which ends the server block.
|
||||
func (p *parser) directives() error {
|
||||
for p.Next() {
|
||||
// end of server block
|
||||
if p.Val() == "}" {
|
||||
break
|
||||
}
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if p.Val() == "import" {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
|
||||
// normal case: parse a directive on this line
|
||||
if err := p.directive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// doImport swaps out the import directive and its argument
|
||||
// (a total of 2 tokens) with the tokens in the specified file
|
||||
// or globbing pattern. When the function returns, the cursor
|
||||
// is on the token before where the import directive was. In
|
||||
// other words, call Next() to access the first token that was
|
||||
// imported.
|
||||
func (p *parser) doImport() error {
|
||||
// syntax check
|
||||
if !p.NextArg() {
|
||||
return p.ArgErr()
|
||||
}
|
||||
importPattern := p.Val()
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
}
|
||||
|
||||
// do glob
|
||||
matches, err := filepath.Glob(importPattern)
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return p.Errf("No files matching import pattern %s", importPattern)
|
||||
}
|
||||
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
|
||||
// collect all the imported tokens
|
||||
var importedTokens []token
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
// and rewind cursor so Next() will land on first imported token
|
||||
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
|
||||
p.cursor--
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doSingleImport lexes the individual file at importFile and returns
|
||||
// its tokens or an error, if any.
|
||||
func (p *parser) doSingleImport(importFile string) ([]token, error) {
|
||||
file, err := os.Open(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
||||
}
|
||||
defer file.Close()
|
||||
importedTokens := allTokens(file)
|
||||
|
||||
// Tack the filename onto these tokens so errors show the imported file's name
|
||||
filename := filepath.Base(importFile)
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].file = filename
|
||||
}
|
||||
|
||||
return importedTokens, nil
|
||||
}
|
||||
|
||||
// directive collects tokens until the directive's scope
|
||||
// closes (either end of line or end of curly brace block).
|
||||
// It expects the currently-loaded token to be a directive
|
||||
// (or } that ends a server block). The collected tokens
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := p.Val()
|
||||
nesting := 0
|
||||
|
||||
if p.checkDirectives {
|
||||
if _, ok := ValidDirectives[dir]; !ok {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
}
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
nesting++
|
||||
} else if p.isNewLine() && nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
} else if p.Val() == "}" && nesting > 0 {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
}
|
||||
p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
}
|
||||
|
||||
if nesting > 0 {
|
||||
return p.EOFErr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openCurlyBrace expects the current token to be an
|
||||
// opening curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a opening curly brace. It does NOT advance the token.
|
||||
func (p *parser) openCurlyBrace() error {
|
||||
if p.Val() != "{" {
|
||||
return p.SyntaxErr("{")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeCurlyBrace expects the current token to be
|
||||
// a closing curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a closing curly brace. It does NOT advance the token.
|
||||
func (p *parser) closeCurlyBrace() error {
|
||||
if p.Val() != "}" {
|
||||
return p.SyntaxErr("}")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// standardAddress parses an address string into a structured format with separate
|
||||
// scheme, host, and port portions, as well as the original input string.
|
||||
func standardAddress(str string) (address, error) {
|
||||
var scheme string
|
||||
var err error
|
||||
|
||||
// first check for scheme and strip it off
|
||||
input := str
|
||||
if strings.HasPrefix(str, "https://") {
|
||||
scheme = "https"
|
||||
str = str[8:]
|
||||
} else if strings.HasPrefix(str, "http://") {
|
||||
scheme = "http"
|
||||
str = str[7:]
|
||||
}
|
||||
|
||||
// separate host and port
|
||||
host, port, err := net.SplitHostPort(str)
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(str + ":")
|
||||
// no error check here; return err at end of function
|
||||
}
|
||||
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
} else if scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// repeated or conflicting scheme is confusing, so error
|
||||
if scheme != "" && (port == "http" || port == "https") {
|
||||
return address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (scheme == "http" && port == "443") || (scheme == "https" && port == "80") {
|
||||
return address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
// standardize http and https ports to their respective port numbers
|
||||
if port == "http" {
|
||||
scheme = "http"
|
||||
port = "80"
|
||||
} else if port == "https" {
|
||||
scheme = "https"
|
||||
port = "443"
|
||||
}
|
||||
|
||||
return address{Original: input, Scheme: scheme, Host: host, Port: port}, err
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces environment variables that appear in the token
|
||||
// and understands both the $UNIX and %WINDOWS% syntaxes.
|
||||
func replaceEnvVars(s string) string {
|
||||
s = replaceEnvReferences(s, "{%", "%}")
|
||||
s = replaceEnvReferences(s, "{$", "}")
|
||||
return s
|
||||
}
|
||||
|
||||
// replaceEnvReferences performs the actual replacement of env variables
|
||||
// in s, given the placeholder start and placeholder end strings.
|
||||
func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
index := strings.Index(s, refStart)
|
||||
for index != -1 {
|
||||
endIndex := strings.Index(s, refEnd)
|
||||
if endIndex != -1 {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
index = strings.Index(s, refStart)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type (
|
||||
// ServerBlock associates tokens with a list of addresses
|
||||
// and groups tokens by directive name.
|
||||
ServerBlock struct {
|
||||
Addresses []address
|
||||
Tokens map[string][]token
|
||||
}
|
||||
|
||||
address struct {
|
||||
Original, Scheme, Host, Port string
|
||||
}
|
||||
)
|
||||
|
||||
// HostList converts the list of addresses that are
|
||||
// associated with this server block into a slice of
|
||||
// strings, where each address is as it was originally
|
||||
// read from the input.
|
||||
func (sb ServerBlock) HostList() []string {
|
||||
sbHosts := make([]string, len(sb.Addresses))
|
||||
for j, addr := range sb.Addresses {
|
||||
sbHosts[j] = addr.Original
|
||||
}
|
||||
return sbHosts
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "", "localhost", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", false},
|
||||
{`localhost:`, "", "localhost", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false},
|
||||
{`:1234`, "", "", "1234", false},
|
||||
{`[::1]`, "", "::1", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", false},
|
||||
{`:`, "", "", "", false},
|
||||
{`localhost:http`, "http", "localhost", "80", false},
|
||||
{`localhost:https`, "https", "localhost", "443", false},
|
||||
{`:http`, "http", "", "80", false},
|
||||
{`:https`, "https", "", "443", false},
|
||||
{`http://localhost:https`, "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", false},
|
||||
{`https://localhost`, "https", "localhost", "443", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", false},
|
||||
{`http://[::1]`, "http", "::1", "80", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", false},
|
||||
{``, "", "", "", false},
|
||||
{`::1`, "", "::1", "", true},
|
||||
{`localhost::`, "", "localhost::", "", true},
|
||||
{`#$%@`, "", "#$%@", "", true},
|
||||
} {
|
||||
actual, err := standardAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
|
||||
}
|
||||
|
||||
if actual.Scheme != test.scheme {
|
||||
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != test.host {
|
||||
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
|
||||
}
|
||||
if actual.Port != test.port {
|
||||
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses []address
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []address{
|
||||
{"localhost:1234", "", "localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []address{
|
||||
{"localhost:1234", "", "localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []address{
|
||||
{"host1:80", "", "host1", "80"},
|
||||
{"http://host2.com", "http", "host2.com", "80"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []address{
|
||||
{"http://host1.com", "http", "host1.com", "80"},
|
||||
{"http://host2.com", "http", "host2.com", "80"},
|
||||
{"https://host3.com", "https", "host3.com", "443"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []address{
|
||||
{"http://host1.com:1234", "http", "host1.com", "1234"},
|
||||
{"https://host2.com", "https", "host2.com", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []address{
|
||||
{"127.0.0.1", "", "127.0.0.1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
unknown_directive`, true, []address{
|
||||
{"127.0.0.1", "", "127.0.0.1", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import import_test1.txt`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import import_test2.txt`, false, []address{
|
||||
{"host1", "", "host1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`import not_found.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []address{}, map[string]int{}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Addresses) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d addresses, got %d",
|
||||
i, len(test.addresses), len(result.Addresses))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Addresses {
|
||||
if addr.Host != test.addresses[j].Host {
|
||||
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j].Port {
|
||||
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses [][]address // addresses per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]address{
|
||||
{{"localhost", "", "localhost", ""}},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}},
|
||||
{{"localhost:2015", "", "localhost", "2015"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]address{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]address{
|
||||
{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}},
|
||||
{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}},
|
||||
}},
|
||||
|
||||
{`import import_glob*.txt`, false, [][]address{
|
||||
{{"glob0.host0", "", "glob0.host0", ""}},
|
||||
{{"glob0.host1", "", "glob0.host1", ""}},
|
||||
{{"glob1.host0", "", "glob1.host0", ""}},
|
||||
{{"glob2.host0", "", "glob2.host0", ""}},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.addresses), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Addresses) != len(test.addresses[j]) {
|
||||
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
|
||||
i, len(test.addresses[j]), j, len(block.Addresses))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Addresses {
|
||||
if addr.Host != test.addresses[j][k].Host {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j][k].Port {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func setupParseTests() {
|
||||
// Set up some bogus directives for testing
|
||||
ValidDirectives = map[string]struct{}{
|
||||
"dir1": {},
|
||||
"dir2": {},
|
||||
"dir3": {},
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
|
||||
return p
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(CaddyfileInput{})
|
||||
}
|
||||
|
||||
// Restart restarts the entire application; gracefully with zero
|
||||
// downtime if on a POSIX-compatible system, or forcefully if on
|
||||
// Windows but with imperceptibly-short downtime.
|
||||
//
|
||||
// The restarted application will use newCaddyfile as its input
|
||||
// configuration. If newCaddyfile is nil, the current (existing)
|
||||
// Caddyfile configuration will be used.
|
||||
//
|
||||
// Note: The process must exist in the same place on the disk in
|
||||
// order for this to work. Thus, multiple graceful restarts don't
|
||||
// work if executing with `go run`, since the binary is cleaned up
|
||||
// when `go run` sees the initial parent process exit.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
log.Println("[INFO] Restarting")
|
||||
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
// Get certificates for any new hosts in the new Caddyfile without causing downtime
|
||||
err := getCertsForNewCaddyfile(newCaddyfile)
|
||||
if err != nil {
|
||||
return errors.New("TLS preload: " + err.Error())
|
||||
}
|
||||
|
||||
if len(os.Args) == 0 { // this should never happen, but...
|
||||
os.Args = []string{""}
|
||||
}
|
||||
|
||||
// Tell the child that it's a restart
|
||||
os.Setenv("CADDY_RESTART", "true")
|
||||
|
||||
// Prepare our payload to the child process
|
||||
cdyfileGob := caddyfileGob{
|
||||
ListenerFds: make(map[string]uintptr),
|
||||
Caddyfile: newCaddyfile,
|
||||
OnDemandTLSCertsIssued: atomic.LoadInt32(https.OnDemandIssuedCount),
|
||||
}
|
||||
|
||||
// Prepare a pipe to the fork's stdin so it can get the Caddyfile
|
||||
rpipe, wpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare a pipe that the child process will use to communicate
|
||||
// its success with us by sending > 0 bytes
|
||||
sigrpipe, sigwpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Pass along relevant file descriptors to child process; ordering
|
||||
// is very important since we rely on these being in certain positions.
|
||||
extraFiles := []*os.File{sigwpipe} // fd 3
|
||||
|
||||
// Add file descriptors of all the sockets
|
||||
serversMu.Lock()
|
||||
for i, s := range servers {
|
||||
extraFiles = append(extraFiles, s.ListenerFd())
|
||||
cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
// Set up the command
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
cmd.Stdin = rpipe // fd 0
|
||||
cmd.Stdout = os.Stdout // fd 1
|
||||
cmd.Stderr = os.Stderr // fd 2
|
||||
cmd.ExtraFiles = extraFiles
|
||||
|
||||
// Spawn the child process
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Immediately close our dup'ed fds and the write end of our signal pipe
|
||||
for _, f := range extraFiles {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Feed Caddyfile to the child
|
||||
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wpipe.Close()
|
||||
|
||||
// Determine whether child startup succeeded
|
||||
answer, readErr := ioutil.ReadAll(sigrpipe)
|
||||
if answer == nil || len(answer) == 0 {
|
||||
cmdErr := cmd.Wait() // get exit status
|
||||
log.Printf("[ERROR] Restart: child failed to initialize (%v) - changes not applied", cmdErr)
|
||||
if readErr != nil {
|
||||
log.Printf("[ERROR] Restart: additionally, error communicating with child process: %v", readErr)
|
||||
}
|
||||
return errIncompleteRestart
|
||||
}
|
||||
|
||||
// Looks like child is successful; we can exit gracefully.
|
||||
return Stop()
|
||||
}
|
||||
|
||||
func getCertsForNewCaddyfile(newCaddyfile Input) error {
|
||||
// parse the new caddyfile only up to (and including) TLS
|
||||
// so we can know what we need to get certs for.
|
||||
configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
|
||||
if err != nil {
|
||||
return errors.New("loading Caddyfile: " + err.Error())
|
||||
}
|
||||
|
||||
// first mark the configs that are qualified for managed TLS
|
||||
https.MarkQualified(configs)
|
||||
|
||||
// since we group by bind address to obtain certs, we must call
|
||||
// EnableTLS to make sure the port is set properly first
|
||||
// (can ignore error since we aren't actually using the certs)
|
||||
https.EnableTLS(configs, false)
|
||||
|
||||
// find out if we can let the acme package start its own challenge listener
|
||||
// on port 80
|
||||
var proxyACME bool
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
_, port, _ := net.SplitHostPort(s.Addr)
|
||||
if port == "80" {
|
||||
proxyACME = true
|
||||
break
|
||||
}
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
// place certs on the disk
|
||||
err = https.ObtainCerts(configs, false, proxyACME)
|
||||
if err != nil {
|
||||
return errors.New("obtaining certs: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import "log"
|
||||
|
||||
// Restart restarts Caddy forcefully using newCaddyfile,
|
||||
// or, if nil, the current/existing Caddyfile is reused.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
log.Println("[INFO] Restarting")
|
||||
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
wg.Add(1) // barrier so Wait() doesn't unblock
|
||||
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = Start(newCaddyfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg.Done() // take down our barrier
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
)
|
||||
|
||||
// BasicAuth configures a new BasicAuth middleware instance.
|
||||
func BasicAuth(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
rules, err := basicAuthParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basic := basicauth.BasicAuth{Rules: rules}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
basic.Next = next
|
||||
basic.SiteRoot = root
|
||||
return basic
|
||||
}, nil
|
||||
}
|
||||
|
||||
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
var rules []basicauth.Rule
|
||||
|
||||
var err error
|
||||
for c.Next() {
|
||||
var rule basicauth.Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule.Username = args[0]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
rule.Resources = append(rule.Resources, c.Val())
|
||||
if c.NextArg() {
|
||||
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
rule.Resources = append(rule.Resources, args[0])
|
||||
rule.Username = args[1]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
default:
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
return basicauth.PlainMatcher(passw), nil
|
||||
}
|
||||
|
||||
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
c := NewTestController(`basicauth user pwd`)
|
||||
|
||||
mid, err := BasicAuth(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(basicauth.BasicAuth)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthParse(t *testing.T) {
|
||||
htpasswdPasswd := "IedFOuGmTpT8"
|
||||
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
var skipHtpassword bool
|
||||
htfh, err := ioutil.TempFile(".", "basicauth-")
|
||||
if err != nil {
|
||||
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
||||
skipHtpassword = true
|
||||
} else {
|
||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||
}
|
||||
htfh.Close()
|
||||
defer os.Remove(htfh.Name())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
password string
|
||||
expected []basicauth.Rule
|
||||
}{
|
||||
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
{`basicauth /res1 user1 pwd1
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user1", Resources: []string{"/res1"}},
|
||||
{Username: "user2", Resources: []string{"/res2"}},
|
||||
}},
|
||||
{`basicauth user`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}},
|
||||
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
|
||||
{Username: "sha1"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := basicAuthParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Username != expectedRule.Username {
|
||||
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
|
||||
i, j, expectedRule.Username, actualRule.Username)
|
||||
}
|
||||
|
||||
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
|
||||
continue
|
||||
}
|
||||
pwd := test.password
|
||||
if len(actual) > 1 {
|
||||
pwd = fmt.Sprintf("%s%d", pwd, j+1)
|
||||
}
|
||||
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
|
||||
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
|
||||
i, j, test.password, actualRule.Password(""))
|
||||
}
|
||||
|
||||
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
|
||||
actualRes := fmt.Sprintf("%v", actualRule.Resources)
|
||||
if actualRes != expectedRes {
|
||||
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
|
||||
i, j, expectedRes, actualRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package setup
|
||||
|
||||
import "github.com/mholt/caddy/middleware"
|
||||
|
||||
// BindHost sets the host to bind the listener to.
|
||||
func BindHost(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.Args(&c.BindHost) {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
)
|
||||
|
||||
// Browse configures a new Browse middleware instance.
|
||||
func Browse(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := browseParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
browse := browse.Browse{
|
||||
Root: c.Root,
|
||||
Configs: configs,
|
||||
IgnoreIndexes: false,
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
browse.Next = next
|
||||
return browse
|
||||
}, nil
|
||||
}
|
||||
|
||||
func browseParse(c *Controller) ([]browse.Config, error) {
|
||||
var configs []browse.Config
|
||||
|
||||
appendCfg := func(bc browse.Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == bc.PathScope {
|
||||
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
|
||||
}
|
||||
}
|
||||
configs = append(configs, bc)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var bc browse.Config
|
||||
|
||||
// First argument is directory to allow browsing; default is site root
|
||||
if c.NextArg() {
|
||||
bc.PathScope = c.Val()
|
||||
} else {
|
||||
bc.PathScope = "/"
|
||||
}
|
||||
|
||||
// Second argument would be the template file to use
|
||||
var tplText string
|
||||
if c.NextArg() {
|
||||
tplBytes, err := ioutil.ReadFile(c.Val())
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
} else {
|
||||
tplText = defaultTemplate
|
||||
}
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
bc.Template = tpl
|
||||
|
||||
// Save configuration
|
||||
err = appendCfg(bc)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// The default template to use when serving up directory listings
|
||||
const defaultTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Name}}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { padding: 0; margin: 0; }
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #006ed3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
h1 a:hover {
|
||||
color: #319cff;
|
||||
}
|
||||
|
||||
header,
|
||||
.content {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 5%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-top: 25px;
|
||||
padding-bottom: 15px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
font-family: Verdana, sans-serif;
|
||||
border-bottom: 1px solid #9C9C9C;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px dashed #dadada;
|
||||
}
|
||||
|
||||
tr:not(:first-child):hover {
|
||||
background-color: #ffffec;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
th {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
th svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:first-child svg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
td .name {
|
||||
margin-left: 1.75em;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 40px 20px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hideable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
padding-right: 5%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
|
||||
<defs>
|
||||
<!-- Folder -->
|
||||
<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84">
|
||||
<stop stop-color="#b3ddfd" offset="0"/>
|
||||
<stop stop-color="#69c" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26">
|
||||
<stop stop-color="#ace" offset="0"/>
|
||||
<stop stop-color="#369" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="folder" transform="translate(-266.06 -193.36)">
|
||||
<g transform="matrix(.066019 0 0 .066019 264.2 170.93)">
|
||||
<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)">
|
||||
<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/>
|
||||
<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
||||
<!-- File -->
|
||||
<linearGradient id="a">
|
||||
<stop stop-color="#cbcbcb" offset="0"/>
|
||||
<stop stop-color="#f0f0f0" offset=".34923"/>
|
||||
<stop stop-color="#e2e2e2" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/>
|
||||
<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/>
|
||||
<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96">
|
||||
<stop stop-color="#fff" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="file" transform="translate(-278.15 -216.59)">
|
||||
<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)">
|
||||
<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/>
|
||||
<g>
|
||||
<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/>
|
||||
<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/>
|
||||
<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Up arrow -->
|
||||
<g id="up-arrow" transform="translate(-279.22 -208.12)">
|
||||
<path transform="matrix(.22413 0 0 .12089 335.67 164.35)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
|
||||
</g>
|
||||
|
||||
<!-- Down arrow -->
|
||||
<g id="down-arrow" transform="translate(-279.22 -208.12)">
|
||||
<path transform="matrix(.22413 0 0 -.12089 335.67 257.93)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
|
||||
</g>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
<header>
|
||||
<h1>{{.LinkedPath}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="meta">
|
||||
<div class="content">
|
||||
<span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span>
|
||||
<span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listing">
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
{{if and (eq .Sort "name") (ne .Order "desc")}}
|
||||
<a href="?sort=name&order=desc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
{{else if and (eq .Sort "name") (ne .Order "asc")}}
|
||||
<a href="?sort=name&order=asc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
|
||||
{{else}}
|
||||
<a href="?sort=name&order=asc">Name</a>
|
||||
{{end}}
|
||||
</th>
|
||||
<th>
|
||||
{{if and (eq .Sort "size") (ne .Order "desc")}}
|
||||
<a href="?sort=size&order=desc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
|
||||
{{else if and (eq .Sort "size") (ne .Order "asc")}}
|
||||
<a href="?sort=size&order=asc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
|
||||
{{else}}
|
||||
<a href="?sort=size&order=asc">Size</a>
|
||||
{{end}}
|
||||
</th>
|
||||
<th class="hideable">
|
||||
{{if and (eq .Sort "time") (ne .Order "desc")}}
|
||||
<a href="?sort=time&order=desc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
|
||||
{{else if and (eq .Sort "time") (ne .Order "asc")}}
|
||||
<a href="?sort=time&order=asc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
|
||||
{{else}}
|
||||
<a href="?sort=time&order=asc">Modified</a>
|
||||
{{end}}
|
||||
</th>
|
||||
</tr>
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{.URL}}">
|
||||
{{if .IsDir}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
|
||||
{{else}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
|
||||
{{end}}
|
||||
<span class="name">{{.Name}}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{.HumanSize}}</td>
|
||||
<td class="hideable">{{.HumanModTime "01/02/2006 03:04:05 PM"}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Served with <a href="https://caddyserver.com">Caddy</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -1,65 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
)
|
||||
|
||||
func TestBrowse(t *testing.T) {
|
||||
|
||||
tempDirPath, err := getTempDirPath()
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
|
||||
|
||||
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
|
||||
}
|
||||
defer os.Remove(tempTemplate.Name())
|
||||
|
||||
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expectedPathScope []string
|
||||
shouldErr bool
|
||||
}{
|
||||
// test case #0 tests handling of multiple pathscopes
|
||||
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
|
||||
|
||||
// test case #1 tests instantiation of browse.Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detectaction of custom template
|
||||
{"browse . " + tempTemplatePath, []string{"."}, false},
|
||||
|
||||
// test case #3 tests detection of non-existent template
|
||||
{"browse . " + nonExistantDirPath, nil, true},
|
||||
|
||||
// test case #4 tests detection of duplicate pathscopes
|
||||
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
|
||||
} {
|
||||
|
||||
recievedFunc, err := Browse(NewTestController(test.input))
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d recieved an error of %v", i, err)
|
||||
}
|
||||
if test.expectedPathScope == nil {
|
||||
continue
|
||||
}
|
||||
recievedConfigs := recievedFunc(nil).(browse.Browse).Configs
|
||||
for j, config := range recievedConfigs {
|
||||
if config.PathScope != test.expectedPathScope[j] {
|
||||
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Controller is given to the setup function of middlewares which
|
||||
// gives them access to be able to read tokens and set config. Each
|
||||
// virtualhost gets their own server config and dispenser.
|
||||
type Controller struct {
|
||||
*server.Config
|
||||
parse.Dispenser
|
||||
|
||||
// OncePerServerBlock is a function that executes f
|
||||
// exactly once per server block, no matter how many
|
||||
// hosts are associated with it. If it is the first
|
||||
// time, the function f is executed immediately
|
||||
// (not deferred) and may return an error which is
|
||||
// returned by OncePerServerBlock.
|
||||
OncePerServerBlock func(f func() error) error
|
||||
|
||||
// ServerBlockIndex is the 0-based index of the
|
||||
// server block as it appeared in the input.
|
||||
ServerBlockIndex int
|
||||
|
||||
// ServerBlockHostIndex is the 0-based index of this
|
||||
// host as it appeared in the input at the head of the
|
||||
// server block.
|
||||
ServerBlockHostIndex int
|
||||
|
||||
// ServerBlockHosts is a list of hosts that are
|
||||
// associated with this server block. All these
|
||||
// hosts, consequently, share the same tokens.
|
||||
ServerBlockHosts []string
|
||||
|
||||
// ServerBlockStorage is used by a directive's
|
||||
// setup function to persist state between all
|
||||
// the hosts on a server block.
|
||||
ServerBlockStorage interface{}
|
||||
}
|
||||
|
||||
// NewTestController creates a new *Controller for
|
||||
// the input specified, with a filename of "Testfile".
|
||||
// The Config is bare, consisting only of a Root of cwd.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience. Does not initialize
|
||||
// the server-block-related fields.
|
||||
func NewTestController(input string) *Controller {
|
||||
return &Controller{
|
||||
Config: &server.Config{
|
||||
Root: ".",
|
||||
},
|
||||
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
return f()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// middleware.Middleware functions so that the assignment
|
||||
// to the Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
func SameNext(next1, next2 middleware.Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
)
|
||||
|
||||
// Errors configures a new errors middleware instance.
|
||||
func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
handler, err := errorsParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the log file for writing when the server starts
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
switch handler.LogFile {
|
||||
case "visible":
|
||||
handler.Debug = true
|
||||
case "stdout":
|
||||
writer = os.Stdout
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "syslog":
|
||||
writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if handler.LogFile == "" {
|
||||
writer = os.Stderr // default
|
||||
break
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if handler.LogRoller != nil {
|
||||
file.Close()
|
||||
|
||||
handler.LogRoller.Filename = handler.LogFile
|
||||
|
||||
writer = handler.LogRoller.GetLogWriter()
|
||||
} else {
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
|
||||
handler.Log = log.New(writer, "", 0)
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
}, nil
|
||||
}
|
||||
|
||||
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
// Very important that we make a pointer because the Startup
|
||||
// function that opens the log file must have access to the
|
||||
// same instance of the handler, not a copy.
|
||||
handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return hadBlock, c.ArgErr()
|
||||
}
|
||||
where := c.Val()
|
||||
|
||||
if what == "log" {
|
||||
if where == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = where
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
c.IncrNest()
|
||||
logRoller, err := parseRoller(c)
|
||||
if err != nil {
|
||||
return hadBlock, err
|
||||
}
|
||||
handler.LogRoller = logRoller
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Error page; ensure it exists
|
||||
where = filepath.Join(c.Root, where)
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
whatInt, err := strconv.Atoi(what)
|
||||
if err != nil {
|
||||
return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
|
||||
}
|
||||
handler.ErrorPages[whatInt] = where
|
||||
}
|
||||
}
|
||||
return hadBlock, nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
// weird hack to avoid having the handler values overwritten.
|
||||
if c.Val() == "}" {
|
||||
continue
|
||||
}
|
||||
// Configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
|
||||
// Otherwise, the only argument would be an error log file name or 'visible'
|
||||
if !hadBlock {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = c.Val()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
c := NewTestController(`errors`)
|
||||
mid, err := Errors(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(*errors.ErrorHandler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.LogFile != "" {
|
||||
t.Errorf("Expected '%s' as the default LogFile", "")
|
||||
}
|
||||
if myHandler.LogRoller != nil {
|
||||
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
// Test Startup function
|
||||
if len(c.Startup) == 0 {
|
||||
t.Fatal("Expected 1 startup function, had 0")
|
||||
}
|
||||
err = c.Startup[0]()
|
||||
if myHandler.Log == nil {
|
||||
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler errors.ErrorHandler
|
||||
}{
|
||||
{`errors`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
}},
|
||||
{`errors errors.txt`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
}},
|
||||
{`errors visible`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log visible }`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
500: "500.html",
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
LogRoller: &middleware.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
LocalTime: true,
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt {
|
||||
size 3
|
||||
age 11
|
||||
keep 5
|
||||
}
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
LogRoller: &middleware.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
LocalTime: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputErrorsRules)
|
||||
actualErrorsRule, err := errorsParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
|
||||
t.Errorf("Test %d expected LogFile to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
|
||||
}
|
||||
if actualErrorsRule.Debug != test.expectedErrorHandler.Debug {
|
||||
t.Errorf("Test %d expected Debug to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug)
|
||||
}
|
||||
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil {
|
||||
t.Fatalf("Test %d expected LogRoller to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller)
|
||||
}
|
||||
if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) {
|
||||
t.Fatalf("Test %d expected %d no of Error pages, but got %d ",
|
||||
i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages))
|
||||
}
|
||||
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil {
|
||||
if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename {
|
||||
t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge {
|
||||
t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups {
|
||||
t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize {
|
||||
t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime {
|
||||
t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t",
|
||||
i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
// Ext configures a new instance of 'extensions' middleware for clean URLs.
|
||||
func Ext(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
exts, err := extParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return extensions.Ext{
|
||||
Next: next,
|
||||
Extensions: exts,
|
||||
Root: root,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extParse sets up an instance of extension middleware
|
||||
// from a middleware controller and returns a list of extensions.
|
||||
func extParse(c *Controller) ([]string, error) {
|
||||
var exts []string
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
if !c.NextArg() {
|
||||
return exts, c.ArgErr()
|
||||
}
|
||||
exts = append(exts, c.Val())
|
||||
|
||||
// Tack on any other extensions that may have been listed
|
||||
exts = append(exts, c.RemainingArgs()...)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
// resourceExists returns true if the file specified at
|
||||
// root + path exists; false otherwise.
|
||||
func resourceExists(root, path string) bool {
|
||||
_, err := os.Stat(root + path)
|
||||
// technically we should use os.IsNotExist(err)
|
||||
// but we don't handle any other kinds of errors anyway
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
c := NewTestController(`ext .html .htm .php`)
|
||||
|
||||
mid, err := Ext(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(extensions.Ext)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Extensions[0] != ".html" {
|
||||
t.Errorf("Expected .html in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[1] != ".htm" {
|
||||
t.Errorf("Expected .htm in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[2] != ".php" {
|
||||
t.Errorf("Expected .php in the list of Extensions")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputExts string
|
||||
shouldErr bool
|
||||
expectedExts []string
|
||||
}{
|
||||
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
|
||||
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
|
||||
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputExts)
|
||||
actualExts, err := extParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualExts) != len(test.expectedExts) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expectedExts), len(actualExts))
|
||||
}
|
||||
for j, actualExt := range actualExts {
|
||||
if actualExt != test.expectedExts[j] {
|
||||
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
|
||||
i, j, test.expectedExts[j], actualExt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
)
|
||||
|
||||
// FastCGI configures a new FastCGI middleware instance.
|
||||
func FastCGI(c *Controller) (middleware.Middleware, error) {
|
||||
absRoot, err := filepath.Abs(c.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, err := fastcgiParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return fastcgi.Handler{
|
||||
Next: next,
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
AbsRoot: absRoot,
|
||||
FileSys: http.Dir(c.Root),
|
||||
SoftwareName: c.AppName,
|
||||
SoftwareVersion: c.AppVersion,
|
||||
ServerName: c.Host,
|
||||
ServerPort: c.Port,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
var rules []fastcgi.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule fastcgi.Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return rules, c.ArgErr()
|
||||
case 1:
|
||||
rule.Path = "/"
|
||||
rule.Address = args[0]
|
||||
case 2:
|
||||
rule.Path = args[0]
|
||||
rule.Address = args[1]
|
||||
case 3:
|
||||
rule.Path = args[0]
|
||||
rule.Address = args[1]
|
||||
err := fastcgiPreset(args[2], &rule)
|
||||
if err != nil {
|
||||
return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'")
|
||||
}
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.Ext = c.Val()
|
||||
case "split":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.SplitPath = c.Val()
|
||||
case "index":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.IndexFiles = args
|
||||
case "env":
|
||||
envArgs := c.RemainingArgs()
|
||||
if len(envArgs) < 2 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
|
||||
switch name {
|
||||
case "php":
|
||||
rule.Ext = ".php"
|
||||
rule.SplitPath = ".php"
|
||||
rule.IndexFiles = []string{"index.php"}
|
||||
default:
|
||||
return errors.New(name + " is not a valid preset name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFastCGI(t *testing.T) {
|
||||
|
||||
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
|
||||
|
||||
mid, err := FastCGI(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(fastcgi.Handler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type , got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != "/" {
|
||||
t.Errorf("Expected / as the Path")
|
||||
}
|
||||
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []fastcgi.Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputFastcgiConfig)
|
||||
actualFastcgiConfigs, err := fastcgiParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
|
||||
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
|
||||
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
|
||||
}
|
||||
for j, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
|
||||
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
|
||||
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
)
|
||||
|
||||
// Gzip configures a new gzip middleware instance.
|
||||
func Gzip(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := gzipParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return gzip.Gzip{Next: next, Configs: configs}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
var configs []gzip.Config
|
||||
|
||||
for c.Next() {
|
||||
config := gzip.Config{}
|
||||
|
||||
// Request Filters
|
||||
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
|
||||
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
|
||||
|
||||
// Response Filters
|
||||
lengthFilter := gzip.LengthFilter(0)
|
||||
|
||||
// No extra args expected
|
||||
if len(c.RemainingArgs()) > 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
exts := c.RemainingArgs()
|
||||
if len(exts) == 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, e := range exts {
|
||||
if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard && e != "" {
|
||||
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
|
||||
}
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
case "not":
|
||||
paths := c.RemainingArgs()
|
||||
if len(paths) == 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, p := range paths {
|
||||
if p == "/" {
|
||||
return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`)
|
||||
}
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p)
|
||||
}
|
||||
pathFilter.IgnoredPaths.Add(p)
|
||||
}
|
||||
case "level":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
level, _ := strconv.Atoi(c.Val())
|
||||
config.Level = level
|
||||
case "min_length":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
length, err := strconv.ParseInt(c.Val(), 10, 64)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
} else if length == 0 {
|
||||
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
|
||||
}
|
||||
lengthFilter = gzip.LengthFilter(length)
|
||||
default:
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Request Filters
|
||||
config.RequestFilters = []gzip.RequestFilter{}
|
||||
|
||||
// If ignored paths are specified, put in front to filter with path first
|
||||
if len(pathFilter.IgnoredPaths) > 0 {
|
||||
config.RequestFilters = []gzip.RequestFilter{pathFilter}
|
||||
}
|
||||
|
||||
// Then, if extensions are specified, use those to filter.
|
||||
// Otherwise, use default extensions filter.
|
||||
if len(extFilter.Exts) > 0 {
|
||||
config.RequestFilters = append(config.RequestFilters, extFilter)
|
||||
} else {
|
||||
config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter())
|
||||
}
|
||||
|
||||
// Response Filters
|
||||
// If min_length is specified, use it.
|
||||
if int64(lengthFilter) != 0 {
|
||||
config.ResponseFilters = append(config.ResponseFilters, lengthFilter)
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
)
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
c := NewTestController(`gzip`)
|
||||
|
||||
mid, err := Gzip(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(gzip.Gzip)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`gzip {`, true},
|
||||
{`gzip {}`, true},
|
||||
{`gzip a b`, true},
|
||||
{`gzip a {`, true},
|
||||
{`gzip { not f } `, true},
|
||||
{`gzip { not } `, true},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
} `, false},
|
||||
{`gzip { level 9 } `, false},
|
||||
{`gzip { ext } `, true},
|
||||
{`gzip { ext /f
|
||||
} `, true},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip`, false},
|
||||
{`gzip {
|
||||
ext ""
|
||||
}`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip { not /file1
|
||||
ext .htm
|
||||
level 3
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip { not /file1
|
||||
ext .htm
|
||||
level 3
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
min_length ab
|
||||
}
|
||||
`, true},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
min_length 1000
|
||||
}
|
||||
`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err := gzipParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
)
|
||||
|
||||
// Headers configures a new Headers middleware instance.
|
||||
func Headers(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return headers.Headers{Next: next, Rules: rules}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
var rules []headers.Rule
|
||||
|
||||
for c.NextLine() {
|
||||
var head headers.Rule
|
||||
var isNewPattern bool
|
||||
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
pattern := c.Val()
|
||||
|
||||
// See if we already have a definition for this Path pattern...
|
||||
for _, h := range rules {
|
||||
if h.Path == pattern {
|
||||
head = h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ...otherwise, this is a new pattern
|
||||
if head.Path == "" {
|
||||
head.Path = pattern
|
||||
isNewPattern = true
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
// A block of headers was opened...
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers = append(head.Headers, h)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// ... or single header was defined as an argument instead.
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
|
||||
h.Value = c.Val()
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers = append(head.Headers, h)
|
||||
}
|
||||
|
||||
if isNewPattern {
|
||||
rules = append(rules, head)
|
||||
} else {
|
||||
for i := 0; i < len(rules); i++ {
|
||||
if rules[i].Path == pattern {
|
||||
rules[i] = head
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
c := NewTestController(`header / Foo Bar`)
|
||||
|
||||
mid, err := Headers(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(headers.Headers)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadersParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []headers.Rule
|
||||
}{
|
||||
{`header /foo Foo "Bar Baz"`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/foo", Headers: []headers.Header{
|
||||
{"Foo", "Bar Baz"},
|
||||
}},
|
||||
}},
|
||||
{`header /bar { Foo "Bar Baz" Baz Qux }`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/bar", Headers: []headers.Header{
|
||||
{"Foo", "Bar Baz"},
|
||||
{"Baz", "Qux"},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := headersParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Path != expectedRule.Path {
|
||||
t.Errorf("Test %d, rule %d: Expected path %s, but got %s",
|
||||
i, j, expectedRule.Path, actualRule.Path)
|
||||
}
|
||||
|
||||
expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers)
|
||||
actualHeaders := fmt.Sprintf("%v", actualRule.Headers)
|
||||
|
||||
if actualHeaders != expectedHeaders {
|
||||
t.Errorf("Test %d, rule %d: Expected headers %s, but got %s",
|
||||
i, j, expectedHeaders, actualHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
// Internal configures a new Internal middleware instance.
|
||||
func Internal(c *Controller) (middleware.Middleware, error) {
|
||||
paths, err := internalParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return inner.Internal{Next: next, Paths: paths}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func internalParse(c *Controller) ([]string, error) {
|
||||
var paths []string
|
||||
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return paths, c.ArgErr()
|
||||
}
|
||||
paths = append(paths, c.Val())
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
func TestInternal(t *testing.T) {
|
||||
c := NewTestController(`internal /internal`)
|
||||
|
||||
mid, err := Internal(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(inner.Internal)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Internal, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Paths[0] != "/internal" {
|
||||
t.Errorf("Expected internal in the list of internal Paths")
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInternalParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputInternalPaths string
|
||||
shouldErr bool
|
||||
expectedInternalPaths []string
|
||||
}{
|
||||
{`internal /internal`, false, []string{"/internal"}},
|
||||
|
||||
{`internal /internal1
|
||||
internal /internal2`, false, []string{"/internal1", "/internal2"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputInternalPaths)
|
||||
actualInternalPaths, err := internalParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualInternalPaths) != len(test.expectedInternalPaths) {
|
||||
t.Fatalf("Test %d expected %d InternalPaths, but got %d",
|
||||
i, len(test.expectedInternalPaths), len(actualInternalPaths))
|
||||
}
|
||||
for j, actualInternalPath := range actualInternalPaths {
|
||||
if actualInternalPath != test.expectedInternalPaths[j] {
|
||||
t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s",
|
||||
i, j, test.expectedInternalPaths[j], actualInternalPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
caddylog "github.com/mholt/caddy/middleware/log"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Log sets up the logging middleware.
|
||||
func Log(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := logParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the log files for writing when the server starts
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
for i := 0; i < len(rules); i++ {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
if rules[i].OutputFile == "stdout" {
|
||||
writer = os.Stdout
|
||||
} else if rules[i].OutputFile == "stderr" {
|
||||
writer = os.Stderr
|
||||
} else if rules[i].OutputFile == "syslog" {
|
||||
writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var file *os.File
|
||||
file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rules[i].Roller != nil {
|
||||
file.Close()
|
||||
rules[i].Roller.Filename = rules[i].OutputFile
|
||||
writer = rules[i].Roller.GetLogWriter()
|
||||
} else {
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
|
||||
rules[i].Log = log.New(writer, "", 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func logParse(c *Controller) ([]caddylog.Rule, error) {
|
||||
var rules []caddylog.Rule
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var logRoller *middleware.LogRoller
|
||||
if c.NextBlock() {
|
||||
if c.Val() == "rotate" {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
var err error
|
||||
logRoller, err = parseRoller(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This part doesn't allow having something after the rotate block
|
||||
if c.Next() {
|
||||
if c.Val() != "}" {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) == 0 {
|
||||
// Nothing specified; use defaults
|
||||
rules = append(rules, caddylog.Rule{
|
||||
PathScope: "/",
|
||||
OutputFile: caddylog.DefaultLogFilename,
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
Roller: logRoller,
|
||||
})
|
||||
} else if len(args) == 1 {
|
||||
// Only an output file specified
|
||||
rules = append(rules, caddylog.Rule{
|
||||
PathScope: "/",
|
||||
OutputFile: args[0],
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
Roller: logRoller,
|
||||
})
|
||||
} else {
|
||||
// Path scope, output file, and maybe a format specified
|
||||
|
||||
format := caddylog.DefaultLogFormat
|
||||
|
||||
if len(args) > 2 {
|
||||
switch args[2] {
|
||||
case "{common}":
|
||||
format = caddylog.CommonLogFormat
|
||||
case "{combined}":
|
||||
format = caddylog.CombinedLogFormat
|
||||
default:
|
||||
format = args[2]
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, caddylog.Rule{
|
||||
PathScope: args[0],
|
||||
OutputFile: args[1],
|
||||
Format: format,
|
||||
Roller: logRoller,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
caddylog "github.com/mholt/caddy/middleware/log"
|
||||
)
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
|
||||
c := NewTestController(`log`)
|
||||
|
||||
mid, err := Log(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(caddylog.Logger)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Logger, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].PathScope != "/" {
|
||||
t.Errorf("Expected / as the default PathScope")
|
||||
}
|
||||
if myHandler.Rules[0].OutputFile != caddylog.DefaultLogFilename {
|
||||
t.Errorf("Expected %s as the default OutputFile", caddylog.DefaultLogFilename)
|
||||
}
|
||||
if myHandler.Rules[0].Format != caddylog.DefaultLogFormat {
|
||||
t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat)
|
||||
}
|
||||
if myHandler.Rules[0].Roller != nil {
|
||||
t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLogParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputLogRules string
|
||||
shouldErr bool
|
||||
expectedLogRules []caddylog.Rule
|
||||
}{
|
||||
{`log`, false, []caddylog.Rule{{
|
||||
PathScope: "/",
|
||||
OutputFile: caddylog.DefaultLogFilename,
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log log.txt`, false, []caddylog.Rule{{
|
||||
PathScope: "/",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log /api log.txt`, false, []caddylog.Rule{{
|
||||
PathScope: "/api",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log /serve stdout`, false, []caddylog.Rule{{
|
||||
PathScope: "/serve",
|
||||
OutputFile: "stdout",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log /myapi log.txt {common}`, false, []caddylog.Rule{{
|
||||
PathScope: "/myapi",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.CommonLogFormat,
|
||||
}}},
|
||||
{`log /test accesslog.txt {combined}`, false, []caddylog.Rule{{
|
||||
PathScope: "/test",
|
||||
OutputFile: "accesslog.txt",
|
||||
Format: caddylog.CombinedLogFormat,
|
||||
}}},
|
||||
{`log /api1 log.txt
|
||||
log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{
|
||||
PathScope: "/api1",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}, {
|
||||
PathScope: "/api2",
|
||||
OutputFile: "accesslog.txt",
|
||||
Format: caddylog.CombinedLogFormat,
|
||||
}}},
|
||||
{`log /api3 stdout {host}
|
||||
log /api4 log.txt {when}`, false, []caddylog.Rule{{
|
||||
PathScope: "/api3",
|
||||
OutputFile: "stdout",
|
||||
Format: "{host}",
|
||||
}, {
|
||||
PathScope: "/api4",
|
||||
OutputFile: "log.txt",
|
||||
Format: "{when}",
|
||||
}}},
|
||||
{`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{
|
||||
PathScope: "/",
|
||||
OutputFile: "access.log",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
Roller: &middleware.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
LocalTime: true,
|
||||
},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputLogRules)
|
||||
actualLogRules, err := logParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualLogRules) != len(test.expectedLogRules) {
|
||||
t.Fatalf("Test %d expected %d no of Log rules, but got %d ",
|
||||
i, len(test.expectedLogRules), len(actualLogRules))
|
||||
}
|
||||
for j, actualLogRule := range actualLogRules {
|
||||
|
||||
if actualLogRule.PathScope != test.expectedLogRules[j].PathScope {
|
||||
t.Errorf("Test %d expected %dth LogRule PathScope to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].PathScope, actualLogRule.PathScope)
|
||||
}
|
||||
|
||||
if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
|
||||
t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
|
||||
}
|
||||
|
||||
if actualLogRule.Format != test.expectedLogRules[j].Format {
|
||||
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
|
||||
}
|
||||
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v",
|
||||
i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller)
|
||||
}
|
||||
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil {
|
||||
if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s",
|
||||
i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename)
|
||||
}
|
||||
if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d",
|
||||
i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge)
|
||||
}
|
||||
if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d",
|
||||
i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups)
|
||||
}
|
||||
if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d",
|
||||
i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize)
|
||||
}
|
||||
if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t",
|
||||
i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// Markdown configures a new Markdown middleware instance.
|
||||
func Markdown(c *Controller) (middleware.Middleware, error) {
|
||||
mdconfigs, err := markdownParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := markdown.Markdown{
|
||||
Root: c.Root,
|
||||
FileSys: http.Dir(c.Root),
|
||||
Configs: mdconfigs,
|
||||
IndexFiles: []string{"index.md"},
|
||||
}
|
||||
|
||||
// Sweep the whole path at startup to at least generate link index, maybe generate static site
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
for i := range mdconfigs {
|
||||
cfg := mdconfigs[i]
|
||||
|
||||
// Generate link index and static files (if enabled)
|
||||
if err := markdown.GenerateStatic(md, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch file changes for static site generation if not in development mode.
|
||||
if !cfg.Development {
|
||||
markdown.Watch(md, cfg, markdown.DefaultInterval)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
md.Next = next
|
||||
return md
|
||||
}, nil
|
||||
}
|
||||
|
||||
func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
||||
var mdconfigs []*markdown.Config
|
||||
|
||||
for c.Next() {
|
||||
md := &markdown.Config{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
Templates: make(map[string]string),
|
||||
StaticFiles: make(map[string]string),
|
||||
}
|
||||
|
||||
// Get the path scope
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
md.PathScope = "/"
|
||||
case 1:
|
||||
md.PathScope = args[0]
|
||||
default:
|
||||
return mdconfigs, c.ArgErr()
|
||||
}
|
||||
|
||||
// Load any other configuration parameters
|
||||
for c.NextBlock() {
|
||||
if err := loadParams(c, md); err != nil {
|
||||
return mdconfigs, err
|
||||
}
|
||||
}
|
||||
|
||||
// If no extensions were specified, assume some defaults
|
||||
if len(md.Extensions) == 0 {
|
||||
md.Extensions = []string{".md", ".markdown", ".mdown"}
|
||||
}
|
||||
|
||||
mdconfigs = append(mdconfigs, md)
|
||||
}
|
||||
|
||||
return mdconfigs, nil
|
||||
}
|
||||
|
||||
func loadParams(c *Controller, mdc *markdown.Config) error {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
exts := c.RemainingArgs()
|
||||
if len(exts) == 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Extensions = append(mdc.Extensions, exts...)
|
||||
return nil
|
||||
case "css":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Styles = append(mdc.Styles, c.Val())
|
||||
return nil
|
||||
case "js":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Scripts = append(mdc.Scripts, c.Val())
|
||||
return nil
|
||||
case "template":
|
||||
tArgs := c.RemainingArgs()
|
||||
switch len(tArgs) {
|
||||
case 0:
|
||||
return c.ArgErr()
|
||||
case 1:
|
||||
if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok {
|
||||
return c.Err("only one default template is allowed, use alias.")
|
||||
}
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
|
||||
mdc.Templates[markdown.DefaultTemplate] = fpath
|
||||
return nil
|
||||
case 2:
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
|
||||
mdc.Templates[tArgs[0]] = fpath
|
||||
return nil
|
||||
default:
|
||||
return c.ArgErr()
|
||||
}
|
||||
case "sitegen":
|
||||
if c.NextArg() {
|
||||
mdc.StaticDir = path.Join(c.Root, c.Val())
|
||||
} else {
|
||||
mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// only 1 argument allowed
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
case "dev":
|
||||
if c.NextArg() {
|
||||
mdc.Development = strings.ToLower(c.Val()) == "true"
|
||||
} else {
|
||||
mdc.Development = true
|
||||
}
|
||||
if c.NextArg() {
|
||||
// only 1 argument allowed
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return c.Err("Expected valid markdown configuration property")
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
)
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
|
||||
c := NewTestController(`markdown /blog`)
|
||||
|
||||
mid, err := Markdown(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(markdown.Markdown)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Configs[0].PathScope != "/blog" {
|
||||
t.Errorf("Expected /blog as the Path Scope")
|
||||
}
|
||||
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md", ".markdown", ".mdown"}) {
|
||||
t.Errorf("Expected .md, .markdown, and .mdown as default extensions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownStaticGen(t *testing.T) {
|
||||
c := NewTestController(`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
sitegen
|
||||
}`)
|
||||
|
||||
c.Root = "./testdata"
|
||||
mid, err := Markdown(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
for _, start := range c.Startup {
|
||||
err := start()
|
||||
if err != nil {
|
||||
t.Errorf("Startup error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
next := middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
})
|
||||
hndlr := mid(next)
|
||||
mkdwn, ok := hndlr.(markdown.Markdown)
|
||||
if !ok {
|
||||
t.Fatalf("Was expecting a markdown.Markdown but got %T", hndlr)
|
||||
}
|
||||
|
||||
expectedStaticFiles := map[string]string{"/blog/first_post.md": "testdata/generated_site/blog/first_post.md/index.html"}
|
||||
if fmt.Sprint(expectedStaticFiles) != fmt.Sprint(mkdwn.Configs[0].StaticFiles) {
|
||||
t.Fatalf("Test expected StaticFiles to be %s, but got %s",
|
||||
fmt.Sprint(expectedStaticFiles), fmt.Sprint(mkdwn.Configs[0].StaticFiles))
|
||||
}
|
||||
|
||||
filePath := "testdata/generated_site/blog/first_post.md/index.html"
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
t.Fatalf("An error occured when getting the file information: %v", err)
|
||||
}
|
||||
|
||||
html, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("An error occured when getting the file content: %v", err)
|
||||
}
|
||||
|
||||
expectedBody := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>first_post</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Header title</h1>
|
||||
|
||||
<h1>Test h1</h1>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
if !bytes.Equal(html, expectedBody) {
|
||||
t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html))
|
||||
}
|
||||
|
||||
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
|
||||
if err = os.RemoveAll(fp); err != nil {
|
||||
t.Errorf("Error while removing the generated static files: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputMarkdownConfig string
|
||||
shouldErr bool
|
||||
expectedMarkdownConfig []markdown.Config
|
||||
}{
|
||||
|
||||
{`markdown /blog {
|
||||
ext .md .txt
|
||||
css /resources/css/blog.css
|
||||
js /resources/js/blog.js
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md", ".txt"},
|
||||
Styles: []string{"/resources/css/blog.css"},
|
||||
Scripts: []string{"/resources/js/blog.js"},
|
||||
}}},
|
||||
{`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
sitegen
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md"},
|
||||
Templates: map[string]string{markdown.DefaultTemplate: "testdata/tpl_with_include.html"},
|
||||
StaticDir: markdown.DefaultStaticDir,
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputMarkdownConfig)
|
||||
c.Root = "./testdata"
|
||||
actualMarkdownConfigs, err := markdownParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
|
||||
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
|
||||
i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
|
||||
}
|
||||
for j, actualMarkdownConfig := range actualMarkdownConfigs {
|
||||
|
||||
if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
|
||||
t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
|
||||
i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
|
||||
}
|
||||
if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
|
||||
}
|
||||
if fmt.Sprint(actualMarkdownConfig.Templates) != fmt.Sprint(test.expectedMarkdownConfig[j].Templates) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Templates to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Templates), fmt.Sprint(actualMarkdownConfig.Templates))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/mime"
|
||||
)
|
||||
|
||||
// Mime configures a new mime middleware instance.
|
||||
func Mime(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := mimeParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return mime.Mime{Next: next, Configs: configs}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mimeParse(c *Controller) ([]mime.Config, error) {
|
||||
var configs []mime.Config
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 2:
|
||||
if err := validateExt(args[0]); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
configs = append(configs, mime.Config{Ext: args[0], ContentType: args[1]})
|
||||
case 1:
|
||||
return configs, c.ArgErr()
|
||||
case 0:
|
||||
for c.NextBlock() {
|
||||
ext := c.Val()
|
||||
if err := validateExt(ext); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
configs = append(configs, mime.Config{Ext: ext, ContentType: c.Val()})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// validateExt checks for valid file name extension.
|
||||
func validateExt(ext string) error {
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/mime"
|
||||
)
|
||||
|
||||
func TestMime(t *testing.T) {
|
||||
|
||||
c := NewTestController(`mime .txt text/plain`)
|
||||
|
||||
mid, err := Mime(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(mime.Mime)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Mime, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`mime {`, true},
|
||||
{`mime {}`, true},
|
||||
{`mime a b`, true},
|
||||
{`mime a {`, true},
|
||||
{`mime { txt f } `, true},
|
||||
{`mime { html } `, true},
|
||||
{`mime {
|
||||
.html text/html
|
||||
.txt text/plain
|
||||
} `, false},
|
||||
{`mime { .html text/html } `, false},
|
||||
{`mime { .html
|
||||
} `, true},
|
||||
{`mime .txt text/plain`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
m, err := mimeParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil %v", i, m)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/proxy"
|
||||
)
|
||||
|
||||
// Proxy configures a new Proxy middleware instance.
|
||||
func Proxy(c *Controller) (middleware.Middleware, error) {
|
||||
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return proxy.Proxy{Next: next, Upstreams: upstreams}
|
||||
}, nil
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
)
|
||||
|
||||
// Redir configures a new Redirect middleware instance.
|
||||
func Redir(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := redirParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: rules}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||
var redirects []redirect.Rule
|
||||
|
||||
// setRedirCode sets the redirect code for rule if it can, or returns an error
|
||||
setRedirCode := func(code string, rule *redirect.Rule) error {
|
||||
if code == "meta" {
|
||||
rule.Meta = true
|
||||
} else if codeNumber, ok := httpRedirs[code]; ok {
|
||||
rule.Code = codeNumber
|
||||
} else {
|
||||
return c.Errf("Invalid redirect code '%v'", code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAndSaveRule checks the rule for validity (except the redir code)
|
||||
// and saves it if it's valid, or returns an error.
|
||||
checkAndSaveRule := func(rule redirect.Rule) error {
|
||||
if rule.FromPath == rule.To {
|
||||
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
|
||||
}
|
||||
|
||||
for _, otherRule := range redirects {
|
||||
if otherRule.FromPath == rule.FromPath {
|
||||
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To)
|
||||
}
|
||||
}
|
||||
|
||||
redirects = append(redirects, rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var hadOptionalBlock bool
|
||||
for c.NextBlock() {
|
||||
hadOptionalBlock = true
|
||||
|
||||
var rule redirect.Rule
|
||||
|
||||
if c.Config.TLS.Enabled {
|
||||
rule.FromScheme = "https"
|
||||
} else {
|
||||
rule.FromScheme = "http"
|
||||
}
|
||||
|
||||
// Set initial redirect code
|
||||
// BUG: If the code is specified for a whole block and that code is invalid,
|
||||
// the line number will appear on the first line inside the block, even if that
|
||||
// line overwrites the block-level code with a valid redirect code. The program
|
||||
// still functions correctly, but the line number in the error reporting is
|
||||
// misleading to the user.
|
||||
if len(args) == 1 {
|
||||
err := setRedirCode(args[0], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
} else {
|
||||
rule.Code = http.StatusMovedPermanently // default code
|
||||
}
|
||||
|
||||
// RemainingArgs only gets the values after the current token, but in our
|
||||
// case we want to include the current token to get an accurate count.
|
||||
insideArgs := append([]string{c.Val()}, c.RemainingArgs()...)
|
||||
|
||||
switch len(insideArgs) {
|
||||
case 1:
|
||||
// To specified (catch-all redirect)
|
||||
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
|
||||
// As such, this feature remains undocumented.
|
||||
rule.FromPath = "/"
|
||||
rule.To = insideArgs[0]
|
||||
case 2:
|
||||
// From and To specified
|
||||
rule.FromPath = insideArgs[0]
|
||||
rule.To = insideArgs[1]
|
||||
case 3:
|
||||
// From, To, and Code specified
|
||||
rule.FromPath = insideArgs[0]
|
||||
rule.To = insideArgs[1]
|
||||
err := setRedirCode(insideArgs[2], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
default:
|
||||
return redirects, c.ArgErr()
|
||||
}
|
||||
|
||||
err := checkAndSaveRule(rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
}
|
||||
|
||||
if !hadOptionalBlock {
|
||||
var rule redirect.Rule
|
||||
|
||||
if c.Config.TLS.Enabled {
|
||||
rule.FromScheme = "https"
|
||||
} else {
|
||||
rule.FromScheme = "http"
|
||||
}
|
||||
|
||||
rule.Code = http.StatusMovedPermanently // default
|
||||
|
||||
switch len(args) {
|
||||
case 1:
|
||||
// To specified (catch-all redirect)
|
||||
rule.FromPath = "/"
|
||||
rule.To = args[0]
|
||||
case 2:
|
||||
// To and Code specified (catch-all redirect)
|
||||
rule.FromPath = "/"
|
||||
rule.To = args[0]
|
||||
err := setRedirCode(args[1], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
case 3:
|
||||
// From, To, and Code specified
|
||||
rule.FromPath = args[0]
|
||||
rule.To = args[1]
|
||||
err := setRedirCode(args[2], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
default:
|
||||
return redirects, c.ArgErr()
|
||||
}
|
||||
|
||||
err := checkAndSaveRule(rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirects, nil
|
||||
}
|
||||
|
||||
// httpRedirs is a list of supported HTTP redirect codes.
|
||||
var httpRedirs = map[string]int{
|
||||
"300": http.StatusMultipleChoices,
|
||||
"301": http.StatusMovedPermanently,
|
||||
"302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307)
|
||||
"303": http.StatusSeeOther,
|
||||
"304": http.StatusNotModified,
|
||||
"305": http.StatusUseProxy,
|
||||
"307": http.StatusTemporaryRedirect,
|
||||
"308": 308, // Permanent Redirect
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
)
|
||||
|
||||
func TestRedir(t *testing.T) {
|
||||
|
||||
for j, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedRules []redirect.Rule
|
||||
}{
|
||||
// test case #0 tests the recognition of a valid HTTP status code defined outside of block statement
|
||||
{"redir 300 {\n/ /foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 300}}},
|
||||
|
||||
// test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement
|
||||
{"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{{}}},
|
||||
|
||||
// test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement
|
||||
{"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{{}}},
|
||||
|
||||
// test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement
|
||||
{"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{{}}},
|
||||
|
||||
// test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently
|
||||
{"redir 302 {\n/foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 302}}},
|
||||
|
||||
// test case #5 tests the recognition of a TO and From redirection in a block statement
|
||||
{"redir {\n/bar /foo 303\n}", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
|
||||
|
||||
// test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently
|
||||
{"redir /foo", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 301}}},
|
||||
|
||||
// test case #7 tests the recognition of a TO and From redirection in a non-block statement
|
||||
{"redir /bar /foo 303", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
|
||||
|
||||
// test case #8 tests the recognition of multiple redirections
|
||||
{"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}},
|
||||
|
||||
// test case #9 tests the detection of duplicate redirections
|
||||
{"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{{}}},
|
||||
} {
|
||||
recievedFunc, err := Redir(NewTestController(test.input))
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d recieved an error of %v", j, err)
|
||||
} else if test.shouldErr {
|
||||
continue
|
||||
}
|
||||
recievedRules := recievedFunc(nil).(redirect.Redirect).Rules
|
||||
|
||||
for i, recievedRule := range recievedRules {
|
||||
if recievedRule.FromPath != test.expectedRules[i].FromPath {
|
||||
t.Errorf("Test case #%d.%d expected a from path of %s, but recieved a from path of %s", j, i, test.expectedRules[i].FromPath, recievedRule.FromPath)
|
||||
}
|
||||
if recievedRule.To != test.expectedRules[i].To {
|
||||
t.Errorf("Test case #%d.%d expected a TO path of %s, but recieved a TO path of %s", j, i, test.expectedRules[i].To, recievedRule.To)
|
||||
}
|
||||
if recievedRule.Code != test.expectedRules[i].Code {
|
||||
t.Errorf("Test case #%d.%d expected a HTTP status code of %d, but recieved a code of %d", j, i, test.expectedRules[i].Code, recievedRule.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/rewrite"
|
||||
)
|
||||
|
||||
// Rewrite configures a new Rewrite middleware instance.
|
||||
func Rewrite(c *Controller) (middleware.Middleware, error) {
|
||||
rewrites, err := rewriteParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return rewrite.Rewrite{
|
||||
Next: next,
|
||||
FileSys: http.Dir(c.Root),
|
||||
Rules: rewrites,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
|
||||
var simpleRules []rewrite.Rule
|
||||
var regexpRules []rewrite.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule rewrite.Rule
|
||||
var err error
|
||||
var base = "/"
|
||||
var pattern, to string
|
||||
var status int
|
||||
var ext []string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var ifs []rewrite.If
|
||||
|
||||
switch len(args) {
|
||||
case 1:
|
||||
base = args[0]
|
||||
fallthrough
|
||||
case 0:
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "r", "regexp":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
pattern = c.Val()
|
||||
case "to":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
to = strings.Join(args1, " ")
|
||||
case "ext":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ext = args1
|
||||
case "if":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) != 3 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ifCond, err := rewrite.NewIf(args1[0], args1[1], args1[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ifs = append(ifs, ifCond)
|
||||
case "status":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
status, _ = strconv.Atoi(c.Val())
|
||||
if status < 200 || (status > 299 && status < 400) || status > 499 {
|
||||
return nil, c.Err("status must be 2xx or 4xx")
|
||||
}
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
// ensure to or status is specified
|
||||
if to == "" && status == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regexpRules = append(regexpRules, rule)
|
||||
|
||||
// the only unhandled case is 2 and above
|
||||
default:
|
||||
rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " "))
|
||||
simpleRules = append(simpleRules, rule)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// put simple rules in front to avoid regexp computation for them
|
||||
return append(simpleRules, regexpRules...), nil
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/rewrite"
|
||||
)
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
c := NewTestController(`rewrite /from /to`)
|
||||
|
||||
mid, err := Rewrite(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(rewrite.Rewrite)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
if len(myHandler.Rules) != 1 {
|
||||
t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteParse(t *testing.T) {
|
||||
simpleTests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []rewrite.Rule
|
||||
}{
|
||||
{`rewrite /from /to`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "/from", To: "/to"},
|
||||
}},
|
||||
{`rewrite /from /to
|
||||
rewrite a b`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "/from", To: "/to"},
|
||||
rewrite.SimpleRule{From: "a", To: "b"},
|
||||
}},
|
||||
{`rewrite a`, true, []rewrite.Rule{}},
|
||||
{`rewrite`, true, []rewrite.Rule{}},
|
||||
{`rewrite a b c`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "a", To: "b c"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range simpleTests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := rewriteParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, e := range test.expected {
|
||||
actualRule := actual[j].(rewrite.SimpleRule)
|
||||
expectedRule := e.(rewrite.SimpleRule)
|
||||
|
||||
if actualRule.From != expectedRule.From {
|
||||
t.Errorf("Test %d, rule %d: Expected From=%s, got %s",
|
||||
i, j, expectedRule.From, actualRule.From)
|
||||
}
|
||||
|
||||
if actualRule.To != expectedRule.To {
|
||||
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regexpTests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []rewrite.Rule
|
||||
}{
|
||||
{`rewrite {
|
||||
r .*
|
||||
to /to /index.php?
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")},
|
||||
}},
|
||||
{`rewrite {
|
||||
regexp .*
|
||||
to /to
|
||||
ext / html txt
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
|
||||
}},
|
||||
{`rewrite /path {
|
||||
r rr
|
||||
to /dest
|
||||
}
|
||||
rewrite / {
|
||||
regexp [a-z]+
|
||||
to /to /to2
|
||||
}
|
||||
`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")},
|
||||
}},
|
||||
{`rewrite {
|
||||
r .*
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite /`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
if {path} is a
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{{A: "{path}", Operator: "is", B: "a"}}},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 500
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 400
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", Status: 400},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
status 400
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 400},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 399
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 200
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", Status: 200},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
status 200
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 200},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 199
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
status 0
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
status 0
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.ComplexRule{},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range regexpTests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := rewriteParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, e := range test.expected {
|
||||
actualRule := actual[j].(*rewrite.ComplexRule)
|
||||
expectedRule := e.(*rewrite.ComplexRule)
|
||||
|
||||
if actualRule.Base != expectedRule.Base {
|
||||
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
|
||||
i, j, expectedRule.Base, actualRule.Base)
|
||||
}
|
||||
|
||||
if actualRule.To != expectedRule.To {
|
||||
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualRule.Exts) != fmt.Sprint(expectedRule.Exts) {
|
||||
t.Errorf("Test %d, rule %d: Expected Ext=%v, got %v",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
|
||||
if actualRule.Regexp != nil {
|
||||
if actualRule.String() != expectedRule.String() {
|
||||
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
|
||||
i, j, expectedRule.String(), actualRule.String())
|
||||
}
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualRule.Ifs) != fmt.Sprint(expectedRule.Ifs) {
|
||||
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
|
||||
i, j, fmt.Sprint(expectedRule.Ifs), fmt.Sprint(actualRule.Ifs))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func parseRoller(c *Controller) (*middleware.LogRoller, error) {
|
||||
var size, age, keep int
|
||||
// This is kind of a hack to support nested blocks:
|
||||
// As we are already in a block: either log or errors,
|
||||
// c.nesting > 0 but, as soon as c meets a }, it thinks
|
||||
// the block is over and return false for c.NextBlock.
|
||||
for c.NextBlock() {
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value := c.Val()
|
||||
var err error
|
||||
switch what {
|
||||
case "size":
|
||||
size, err = strconv.Atoi(value)
|
||||
case "age":
|
||||
age, err = strconv.Atoi(value)
|
||||
case "keep":
|
||||
keep, err = strconv.Atoi(value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &middleware.LogRoller{
|
||||
MaxSize: size,
|
||||
MaxAge: age,
|
||||
MaxBackups: keep,
|
||||
LocalTime: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// Root sets up the root file path of the server.
|
||||
func Root(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
c.Root = c.Val()
|
||||
}
|
||||
|
||||
// Check if root path exists
|
||||
_, err := os.Stat(c.Root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Allow this, because the folder might appear later.
|
||||
// But make sure the user knows!
|
||||
log.Printf("[WARNING] Root path does not exist: %s", c.Root)
|
||||
} else {
|
||||
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRoot(t *testing.T) {
|
||||
|
||||
// Predefined error substrings
|
||||
parseErrContent := "Parse error:"
|
||||
unableToAccessErrContent := "Unable to access root path"
|
||||
|
||||
existingDirPath, err := getTempDirPath()
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
|
||||
nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir")
|
||||
|
||||
existingFile, err := ioutil.TempFile("", "root_test")
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
existingFile.Close()
|
||||
os.Remove(existingFile.Name())
|
||||
}()
|
||||
|
||||
inaccessiblePath := getInaccessiblePath(existingFile.Name())
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedRoot string // expected root, set to the controller. Empty for negative cases.
|
||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||
}{
|
||||
// positive
|
||||
{
|
||||
fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "",
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "",
|
||||
},
|
||||
// negative
|
||||
{
|
||||
`root `, true, "", parseErrContent,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`root {
|
||||
%s
|
||||
}`, existingDirPath), true, "", parseErrContent,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
mid, err := Root(c)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
|
||||
}
|
||||
}
|
||||
|
||||
// the Root method always returns a nil middleware
|
||||
if mid != nil {
|
||||
t.Errorf("Middware, returned from Root() was not nil: %v", mid)
|
||||
}
|
||||
|
||||
// check c.Root only if we are in a positive test.
|
||||
if !test.shouldErr && test.expectedRoot != c.Root {
|
||||
t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned.
|
||||
func getTempDirPath() (string, error) {
|
||||
tempDir := os.TempDir()
|
||||
|
||||
_, err := os.Stat(tempDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tempDir, nil
|
||||
}
|
||||
|
||||
func getInaccessiblePath(file string) string {
|
||||
// null byte in filename is not allowed on Windows AND unix
|
||||
return filepath.Join("C:", "file\x00name")
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// Startup registers a startup callback to execute during server start.
|
||||
func Startup(c *Controller) (middleware.Middleware, error) {
|
||||
return nil, registerCallback(c, &c.FirstStartup)
|
||||
}
|
||||
|
||||
// Shutdown registers a shutdown callback to execute during process exit.
|
||||
func Shutdown(c *Controller) (middleware.Middleware, error) {
|
||||
return nil, registerCallback(c, &c.Shutdown)
|
||||
}
|
||||
|
||||
// registerCallback registers a callback function to execute by
|
||||
// using c to parse the line. It appends the callback function
|
||||
// to the list of callback functions passed in by reference.
|
||||
func registerCallback(c *Controller, list *[]func() error) error {
|
||||
var funcs []func() error
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
|
||||
nonblock := false
|
||||
if len(args) > 1 && args[len(args)-1] == "&" {
|
||||
// Run command in background; non-blocking
|
||||
nonblock = true
|
||||
args = args[:len(args)-1]
|
||||
}
|
||||
|
||||
command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " "))
|
||||
if err != nil {
|
||||
return c.Err(err.Error())
|
||||
}
|
||||
|
||||
fn := func() error {
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if nonblock {
|
||||
return cmd.Start()
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
|
||||
return c.OncePerServerBlock(func() error {
|
||||
*list = append(*list, funcs...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The Startup function's tests are symmetrical to Shutdown tests,
|
||||
// because the Startup and Shutdown functions share virtually the
|
||||
// same functionality
|
||||
func TestStartup(t *testing.T) {
|
||||
tempDirPath, err := getTempDirPath()
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
|
||||
testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown")
|
||||
defer func() {
|
||||
// clean up after non-blocking startup function quits
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
os.RemoveAll(testDir)
|
||||
}()
|
||||
osSenitiveTestDir := filepath.FromSlash(testDir)
|
||||
os.RemoveAll(osSenitiveTestDir) // start with a clean slate
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldExecutionErr bool
|
||||
shouldRemoveErr bool
|
||||
}{
|
||||
// test case #0 tests proper functionality blocking commands
|
||||
{"startup mkdir " + osSenitiveTestDir, false, false},
|
||||
|
||||
// test case #1 tests proper functionality of non-blocking commands
|
||||
{"startup mkdir " + osSenitiveTestDir + " &", false, true},
|
||||
|
||||
// test case #2 tests handling of non-existent commands
|
||||
{"startup " + strconv.Itoa(int(time.Now().UnixNano())), true, true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err = Startup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
err = c.FirstStartup[0]()
|
||||
if err != nil && !test.shouldExecutionErr {
|
||||
t.Errorf("Test %d recieved an error of:\n%v", i, err)
|
||||
}
|
||||
err = os.Remove(osSenitiveTestDir)
|
||||
if err != nil && !test.shouldRemoveErr {
|
||||
t.Errorf("Test %d recieved an error of:\n%v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/templates"
|
||||
)
|
||||
|
||||
// Templates configures a new Templates middleware instance.
|
||||
func Templates(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := templatesParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmpls := templates.Templates{
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
FileSys: http.Dir(c.Root),
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
tmpls.Next = next
|
||||
return tmpls
|
||||
}, nil
|
||||
}
|
||||
|
||||
func templatesParse(c *Controller) ([]templates.Rule, error) {
|
||||
var rules []templates.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule templates.Rule
|
||||
|
||||
rule.Path = defaultTemplatePath
|
||||
rule.Extensions = defaultTemplateExtensions
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// Optional block
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "path":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Path = args[0]
|
||||
|
||||
case "ext":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Extensions = args
|
||||
|
||||
case "between":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Delims[0] = args[0]
|
||||
rule.Delims[1] = args[1]
|
||||
}
|
||||
}
|
||||
default:
|
||||
// First argument would be the path
|
||||
rule.Path = args[0]
|
||||
|
||||
// Any remaining arguments are extensions
|
||||
rule.Extensions = args[1:]
|
||||
if len(rule.Extensions) == 0 {
|
||||
rule.Extensions = defaultTemplateExtensions
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range rule.Extensions {
|
||||
rule.IndexFiles = append(rule.IndexFiles, "index"+ext)
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
const defaultTemplatePath = "/"
|
||||
|
||||
var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"}
|
||||
@@ -1,112 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/templates"
|
||||
)
|
||||
|
||||
func TestTemplates(t *testing.T) {
|
||||
|
||||
c := NewTestController(`templates`)
|
||||
|
||||
mid, err := Templates(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(templates.Templates)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Templates, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != defaultTemplatePath {
|
||||
t.Errorf("Expected / as the default Path")
|
||||
}
|
||||
if fmt.Sprint(myHandler.Rules[0].Extensions) != fmt.Sprint(defaultTemplateExtensions) {
|
||||
t.Errorf("Expected %v to be the Default Extensions", defaultTemplateExtensions)
|
||||
}
|
||||
var indexFiles []string
|
||||
for _, extension := range defaultTemplateExtensions {
|
||||
indexFiles = append(indexFiles, "index"+extension)
|
||||
}
|
||||
if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
|
||||
t.Errorf("Expected %v to be the Default Index files", indexFiles)
|
||||
}
|
||||
if myHandler.Rules[0].Delims != [2]string{} {
|
||||
t.Errorf("Expected %v to be the Default Delims", [2]string{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplatesParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputTemplateConfig string
|
||||
shouldErr bool
|
||||
expectedTemplateConfig []templates.Rule
|
||||
}{
|
||||
{`templates /api1`, false, []templates.Rule{{
|
||||
Path: "/api1",
|
||||
Extensions: defaultTemplateExtensions,
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
{`templates /api2 .txt .htm`, false, []templates.Rule{{
|
||||
Path: "/api2",
|
||||
Extensions: []string{".txt", ".htm"},
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
|
||||
{`templates /api3 .htm .html
|
||||
templates /api4 .txt .tpl `, false, []templates.Rule{{
|
||||
Path: "/api3",
|
||||
Extensions: []string{".htm", ".html"},
|
||||
Delims: [2]string{},
|
||||
}, {
|
||||
Path: "/api4",
|
||||
Extensions: []string{".txt", ".tpl"},
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
{`templates {
|
||||
path /api5
|
||||
ext .html
|
||||
between {% %}
|
||||
}`, false, []templates.Rule{{
|
||||
Path: "/api5",
|
||||
Extensions: []string{".html"},
|
||||
Delims: [2]string{"{%", "%}"},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputTemplateConfig)
|
||||
actualTemplateConfigs, err := templatesParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualTemplateConfigs) != len(test.expectedTemplateConfig) {
|
||||
t.Fatalf("Test %d expected %d no of Template configs, but got %d ",
|
||||
i, len(test.expectedTemplateConfig), len(actualTemplateConfigs))
|
||||
}
|
||||
for j, actualTemplateConfig := range actualTemplateConfigs {
|
||||
|
||||
if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s",
|
||||
i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) {
|
||||
t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
# Test h1
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
<h1>Header title</h1>
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Doc.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{.Include "header.html"}}
|
||||
{{.Doc.body}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,87 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/websocket"
|
||||
)
|
||||
|
||||
// WebSocket configures a new WebSocket middleware instance.
|
||||
func WebSocket(c *Controller) (middleware.Middleware, error) {
|
||||
|
||||
websocks, err := webSocketParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websocket.GatewayInterface = c.AppName + "-CGI/1.1"
|
||||
websocket.ServerSoftware = c.AppName + "/" + c.AppVersion
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return websocket.WebSocket{Next: next, Sockets: websocks}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func webSocketParse(c *Controller) ([]websocket.Config, error) {
|
||||
var websocks []websocket.Config
|
||||
var respawn bool
|
||||
|
||||
optionalBlock := func() (hadBlock bool, err error) {
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
if c.Val() == "respawn" {
|
||||
respawn = true
|
||||
} else {
|
||||
return true, c.Err("Expected websocket configuration parameter in block")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var val, path, command string
|
||||
|
||||
// Path or command; not sure which yet
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
val = c.Val()
|
||||
|
||||
// Extra configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hadBlock {
|
||||
// The next argument on this line will be the command or an open curly brace
|
||||
if c.NextArg() {
|
||||
path = val
|
||||
command = c.Val()
|
||||
} else {
|
||||
path = "/"
|
||||
command = val
|
||||
}
|
||||
|
||||
// Okay, check again for optional block
|
||||
hadBlock, err = optionalBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Split command into the actual command and its arguments
|
||||
cmd, args, err := middleware.SplitCommandAndArgs(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
websocks = append(websocks, websocket.Config{
|
||||
Path: path,
|
||||
Command: cmd,
|
||||
Arguments: args,
|
||||
Respawn: respawn, // TODO: This isn't used currently
|
||||
})
|
||||
}
|
||||
|
||||
return websocks, nil
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/websocket"
|
||||
)
|
||||
|
||||
func TestWebSocket(t *testing.T) {
|
||||
|
||||
c := NewTestController(`websocket cat`)
|
||||
|
||||
mid, err := WebSocket(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(websocket.WebSocket)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Sockets[0].Path != "/" {
|
||||
t.Errorf("Expected / as the default Path")
|
||||
}
|
||||
if myHandler.Sockets[0].Command != "cat" {
|
||||
t.Errorf("Expected %s as the command", "cat")
|
||||
}
|
||||
|
||||
}
|
||||
func TestWebSocketParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputWebSocketConfig string
|
||||
shouldErr bool
|
||||
expectedWebSocketConfig []websocket.Config
|
||||
}{
|
||||
{`websocket /api1 cat`, false, []websocket.Config{{
|
||||
Path: "/api1",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
{`websocket /api3 cat
|
||||
websocket /api4 cat `, false, []websocket.Config{{
|
||||
Path: "/api3",
|
||||
Command: "cat",
|
||||
}, {
|
||||
Path: "/api4",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
{`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{
|
||||
Path: "/api5",
|
||||
Command: "cmd",
|
||||
Arguments: []string{"arg1", "arg2", "arg3"},
|
||||
}}},
|
||||
|
||||
// accept respawn
|
||||
{`websocket /api6 cat {
|
||||
respawn
|
||||
}`, false, []websocket.Config{{
|
||||
Path: "/api6",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
// invalid configuration
|
||||
{`websocket /api7 cat {
|
||||
invalid
|
||||
}`, true, []websocket.Config{}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputWebSocketConfig)
|
||||
actualWebSocketConfigs, err := webSocketParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) {
|
||||
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
|
||||
i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs))
|
||||
}
|
||||
for j, actualWebSocketConfig := range actualWebSocketConfigs {
|
||||
|
||||
if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s",
|
||||
i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path)
|
||||
}
|
||||
|
||||
if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command {
|
||||
t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s",
|
||||
i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// TrapSignals create signal handlers for all applicable signals for this
|
||||
// system. If your Go program uses signals, this is a rather invasive
|
||||
// function; best to implement them yourself in that case. Signals are not
|
||||
// required for the caddy package to function properly, but this is a
|
||||
// convenient way to allow the user to control this package of your program.
|
||||
func TrapSignals() {
|
||||
trapSignalsCrossPlatform()
|
||||
trapSignalsPosix()
|
||||
}
|
||||
|
||||
// trapSignalsCrossPlatform captures SIGINT, which triggers forceful
|
||||
// shutdown that executes shutdown callbacks first. A second interrupt
|
||||
// signal will exit the process immediately.
|
||||
func trapSignalsCrossPlatform() {
|
||||
go func() {
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt)
|
||||
|
||||
for i := 0; true; i++ {
|
||||
<-shutdown
|
||||
|
||||
if i > 0 {
|
||||
log.Println("[INFO] SIGINT: Force quit")
|
||||
if PidFile != "" {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Println("[INFO] SIGINT: Shutting down")
|
||||
|
||||
if PidFile != "" {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
|
||||
go os.Exit(executeShutdownCallbacks("SIGINT"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// executeShutdownCallbacks executes the shutdown callbacks as initiated
|
||||
// by signame. It logs any errors and returns the recommended exit status.
|
||||
// This function is idempotent; subsequent invocations always return 0.
|
||||
func executeShutdownCallbacks(signame string) (exitCode int) {
|
||||
shutdownCallbacksOnce.Do(func() {
|
||||
serversMu.Lock()
|
||||
errs := server.ShutdownCallbacks(servers)
|
||||
serversMu.Unlock()
|
||||
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Printf("[ERROR] %s shutdown: %v", signame, err)
|
||||
}
|
||||
exitCode = 1
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var shutdownCallbacksOnce sync.Once
|
||||
@@ -1,79 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// trapSignalsPosix captures POSIX-only signals.
|
||||
func trapSignalsPosix() {
|
||||
go func() {
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1)
|
||||
|
||||
for sig := range sigchan {
|
||||
switch sig {
|
||||
case syscall.SIGTERM:
|
||||
log.Println("[INFO] SIGTERM: Terminating process")
|
||||
if PidFile != "" {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
case syscall.SIGQUIT:
|
||||
log.Println("[INFO] SIGQUIT: Shutting down")
|
||||
exitCode := executeShutdownCallbacks("SIGQUIT")
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGQUIT stop: %v", err)
|
||||
exitCode = 1
|
||||
}
|
||||
if PidFile != "" {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
os.Exit(exitCode)
|
||||
|
||||
case syscall.SIGHUP:
|
||||
log.Println("[INFO] SIGHUP: Hanging up")
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGHUP stop: %v", err)
|
||||
}
|
||||
|
||||
case syscall.SIGUSR1:
|
||||
log.Println("[INFO] SIGUSR1: Reloading")
|
||||
|
||||
var updatedCaddyfile Input
|
||||
|
||||
caddyfileMu.Lock()
|
||||
if caddyfile == nil {
|
||||
// Hmm, did spawing process forget to close stdin? Anyhow, this is unusual.
|
||||
log.Println("[ERROR] SIGUSR1: no Caddyfile to reload (was stdin left open?)")
|
||||
caddyfileMu.Unlock()
|
||||
continue
|
||||
}
|
||||
if caddyfile.IsFile() {
|
||||
body, err := ioutil.ReadFile(caddyfile.Path())
|
||||
if err == nil {
|
||||
updatedCaddyfile = CaddyfileInput{
|
||||
Filepath: caddyfile.Path(),
|
||||
Contents: body,
|
||||
RealFile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
err := Restart(updatedCaddyfile)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGUSR1: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package caddy
|
||||
|
||||
func trapSignalsPosix() {}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
)
|
||||
|
||||
// Adapter adapts Caddyfile to Caddy JSON.
|
||||
type Adapter struct {
|
||||
ServerType ServerType
|
||||
}
|
||||
|
||||
// Adapt converts the Caddyfile config in body to Caddy JSON.
|
||||
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
|
||||
if a.ServerType == nil {
|
||||
return nil, nil, fmt.Errorf("no server type")
|
||||
}
|
||||
if options == nil {
|
||||
options = make(map[string]interface{})
|
||||
}
|
||||
|
||||
filename, _ := options["filename"].(string)
|
||||
if filename == "" {
|
||||
filename = "Caddyfile"
|
||||
}
|
||||
|
||||
serverBlocks, err := Parse(filename, body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cfg, warnings, err := a.ServerType.Setup(serverBlocks, options)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
marshalFunc := json.Marshal
|
||||
if options["pretty"] == "true" {
|
||||
marshalFunc = caddyconfig.JSONIndent
|
||||
}
|
||||
result, err := marshalFunc(cfg)
|
||||
|
||||
return result, warnings, err
|
||||
}
|
||||
|
||||
// Unmarshaler is a type that can unmarshal
|
||||
// Caddyfile tokens to set itself up for a
|
||||
// JSON encoding. The goal of an unmarshaler
|
||||
// is not to set itself up for actual use,
|
||||
// but to set itself up for being marshaled
|
||||
// into JSON. Caddyfile-unmarshaled values
|
||||
// will not be used directly; they will be
|
||||
// encoded as JSON and then used from that.
|
||||
// Implementations must be able to support
|
||||
// multiple segments (instances of their
|
||||
// directive or batch of tokens); typically
|
||||
// this means wrapping all token logic in
|
||||
// a loop: `for d.Next() { ... }`.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalCaddyfile(d *Dispenser) error
|
||||
}
|
||||
|
||||
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
|
||||
type ServerType interface {
|
||||
// Setup takes the server blocks which
|
||||
// contain tokens, as well as options
|
||||
// (e.g. CLI flags) and creates a Caddy
|
||||
// config, along with any warnings or
|
||||
// an error.
|
||||
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ caddyconfig.Adapter = (*Adapter)(nil)
|
||||
Executable
+384
@@ -0,0 +1,384 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure. An empty
|
||||
// Dispenser is invalid; call NewDispenser to make a proper instance.
|
||||
type Dispenser struct {
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser filled with the given tokens.
|
||||
func NewDispenser(tokens []Token) *Dispenser {
|
||||
return &Dispenser{
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Prev moves to the previous token. It does the inverse
|
||||
// of Next(), except this function may decrement the cursor
|
||||
// to -1 so that the next call to Next() points to the
|
||||
// first token; this allows dispensing to "start over". This
|
||||
// method returns true if the cursor ends up pointing to a
|
||||
// valid token.
|
||||
func (d *Dispenser) Prev() bool {
|
||||
if d.cursor > -1 {
|
||||
d.cursor--
|
||||
return d.cursor > -1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line and if it is not a block opening (open curly
|
||||
// brace). Returns true if an argument token was
|
||||
// loaded; false otherwise. If false, all tokens on
|
||||
// the line have been consumed except for potentially
|
||||
// a block opening. It handles imported tokens
|
||||
// correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if !d.nextOnSameLine() {
|
||||
return false
|
||||
}
|
||||
if d.Val() == "{" {
|
||||
// roll back; a block opening is not an argument
|
||||
d.cursor--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nextOnSameLine advances the cursor if the next
|
||||
// token is on the same line of the same file.
|
||||
func (d *Dispenser) nextOnSameLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block nested more than initialNestingLevel.
|
||||
// In other words, a loop over NextBlock() will iterate
|
||||
// all tokens in the block assuming the next token is an
|
||||
// open curly brace, until the matching closing brace.
|
||||
// The open and closing brace tokens for the outer-most
|
||||
// block will be consumed internally and omitted from
|
||||
// the iteration.
|
||||
//
|
||||
// Proper use of this method looks like this:
|
||||
//
|
||||
// for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
// }
|
||||
//
|
||||
// However, in simple cases where it is known that the
|
||||
// Dispenser is new and has not already traversed state
|
||||
// by a loop over NextBlock(), this will do:
|
||||
//
|
||||
// for d.NextBlock(0) {
|
||||
// }
|
||||
//
|
||||
// As with other token parsing logic, a loop over
|
||||
// NextBlock() should be contained within a loop over
|
||||
// Next(), as it is usually prudent to skip the initial
|
||||
// token.
|
||||
func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
|
||||
if d.nesting > initialNestingLevel {
|
||||
if !d.Next() {
|
||||
return false // should be EOF error
|
||||
}
|
||||
if d.Val() == "}" && !d.nextOnSameLine() {
|
||||
d.nesting--
|
||||
} else if d.Val() == "{" && !d.nextOnSameLine() {
|
||||
d.nesting++
|
||||
}
|
||||
return d.nesting > initialNestingLevel
|
||||
}
|
||||
if !d.nextOnSameLine() { // block must open on same line
|
||||
return false
|
||||
}
|
||||
if d.Val() != "{" {
|
||||
d.cursor-- // roll back if not opening brace
|
||||
return false
|
||||
}
|
||||
d.Next() // consume open curly brace
|
||||
if d.Val() == "}" {
|
||||
return false // open and then closed right away
|
||||
}
|
||||
d.nesting++
|
||||
return true
|
||||
}
|
||||
|
||||
// Nesting returns the current nesting level. Necessary
|
||||
// if using NextBlock()
|
||||
func (d *Dispenser) Nesting() int {
|
||||
return d.nesting
|
||||
}
|
||||
|
||||
// Val gets the text of the current token. If there is no token
|
||||
// loaded, it returns empty string.
|
||||
func (d *Dispenser) Val() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token.
|
||||
// If there is no token loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].Line
|
||||
}
|
||||
|
||||
// File gets the filename where the current token originated.
|
||||
func (d *Dispenser) File() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].File
|
||||
}
|
||||
|
||||
// Args is a convenience function that loads the next arguments
|
||||
// (tokens on the same line) into an arbitrary number of strings
|
||||
// pointed to in targets. If there are not enough argument tokens
|
||||
// available to fill targets, false is returned and the remaining
|
||||
// targets are left unchanged. If all the targets are filled,
|
||||
// then true is returned.
|
||||
func (d *Dispenser) Args(targets ...*string) bool {
|
||||
for i := 0; i < len(targets); i++ {
|
||||
if !d.NextArg() {
|
||||
return false
|
||||
}
|
||||
*targets[i] = d.Val()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AllArgs is like Args, but if there are more argument tokens
|
||||
// available than there are targets, false is returned. The
|
||||
// number of available argument tokens must match the number of
|
||||
// targets exactly to return true.
|
||||
func (d *Dispenser) AllArgs(targets ...*string) bool {
|
||||
if !d.Args(targets...) {
|
||||
return false
|
||||
}
|
||||
if d.NextArg() {
|
||||
d.Prev()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// NewFromNextSegment returns a new dispenser with a copy of
|
||||
// the tokens from the current token until the end of the
|
||||
// "directive" whether that be to the end of the line or
|
||||
// the end of a block that starts at the end of the line;
|
||||
// in other words, until the end of the segment.
|
||||
func (d *Dispenser) NewFromNextSegment() *Dispenser {
|
||||
return NewDispenser(d.NextSegment())
|
||||
}
|
||||
|
||||
// NextSegment returns a copy of the tokens from the current
|
||||
// token until the end of the line or block that starts at
|
||||
// the end of the line.
|
||||
func (d *Dispenser) NextSegment() Segment {
|
||||
tkns := Segment{d.Token()}
|
||||
for d.NextArg() {
|
||||
tkns = append(tkns, d.Token())
|
||||
}
|
||||
var openedBlock bool
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
if !openedBlock {
|
||||
// because NextBlock() consumes the initial open
|
||||
// curly brace, we rewind here to append it, since
|
||||
// our case is special in that we want the new
|
||||
// dispenser to have all the tokens including
|
||||
// surrounding curly braces
|
||||
d.Prev()
|
||||
tkns = append(tkns, d.Token())
|
||||
d.Next()
|
||||
openedBlock = true
|
||||
}
|
||||
tkns = append(tkns, d.Token())
|
||||
}
|
||||
if openedBlock {
|
||||
// include closing brace
|
||||
tkns = append(tkns, d.Token())
|
||||
|
||||
// do not consume the closing curly brace; the
|
||||
// next iteration of the enclosing loop will
|
||||
// call Next() and consume it
|
||||
}
|
||||
return tkns
|
||||
}
|
||||
|
||||
// Token returns the current token.
|
||||
func (d *Dispenser) Token() Token {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return Token{}
|
||||
}
|
||||
return d.tokens[d.cursor]
|
||||
}
|
||||
|
||||
// Reset sets d's cursor to the beginning, as
|
||||
// if this was a new and unused dispenser.
|
||||
func (d *Dispenser) Reset() {
|
||||
d.cursor = -1
|
||||
d.nesting = 0
|
||||
}
|
||||
|
||||
// ArgErr returns an argument error, meaning that another
|
||||
// argument was expected but not found. In other words,
|
||||
// a line break or open curly brace was encountered instead of
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Delete deletes the current token and returns the updated slice
|
||||
// of tokens. The cursor is not advanced to the next token.
|
||||
// Because deletion modifies the underlying slice, this method
|
||||
// should only be called if you have access to the original slice
|
||||
// of tokens and/or are using the slice of tokens outside this
|
||||
// Dispenser instance. If you do not re-assign the slice with the
|
||||
// return value of this method, inconsistencies in the token
|
||||
// array will become apparent (or worse, hide from you like they
|
||||
// did me for 3 and a half freaking hours late one night).
|
||||
func (d *Dispenser) Delete() []Token {
|
||||
if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 {
|
||||
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
|
||||
d.cursor--
|
||||
}
|
||||
return d.tokens
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
}
|
||||
Regular → Executable
+37
-11
@@ -1,6 +1,22 @@
|
||||
package parse
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -11,7 +27,7 @@ func TestDispenser_Val_Next(t *testing.T) {
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
if val := d.Val(); val != "" {
|
||||
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
|
||||
@@ -49,7 +65,7 @@ func TestDispenser_NextArg(t *testing.T) {
|
||||
input := `dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.Next() != shouldLoad {
|
||||
@@ -64,7 +80,7 @@ func TestDispenser_NextArg(t *testing.T) {
|
||||
}
|
||||
|
||||
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
|
||||
if d.NextArg() != true {
|
||||
if !d.NextArg() {
|
||||
t.Error("NextArg(): Should load next argument but got false instead")
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
@@ -74,7 +90,7 @@ func TestDispenser_NextArg(t *testing.T) {
|
||||
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
|
||||
}
|
||||
if !loadAnother {
|
||||
if d.NextArg() != false {
|
||||
if d.NextArg() {
|
||||
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
@@ -96,7 +112,7 @@ func TestDispenser_NextLine(t *testing.T) {
|
||||
input := `host:port
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.NextLine() != shouldLoad {
|
||||
@@ -129,10 +145,10 @@ func TestDispenser_NextBlock(t *testing.T) {
|
||||
}
|
||||
foobar2 {
|
||||
}`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
|
||||
if loaded := d.NextBlock(); loaded != shouldLoad {
|
||||
if loaded := d.NextBlock(0); loaded != shouldLoad {
|
||||
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
@@ -159,7 +175,7 @@ func TestDispenser_Args(t *testing.T) {
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 arg7
|
||||
dir4`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
@@ -226,7 +242,7 @@ func TestDispenser_RemainingArgs(t *testing.T) {
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 { arg7
|
||||
dir4`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
@@ -263,7 +279,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
input := `dir1 {
|
||||
}
|
||||
dir2 arg1 arg2`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.cursor = 1 // {
|
||||
|
||||
@@ -290,3 +306,13 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestDispenser parses input into tokens and creates a new
|
||||
// Disenser for test purposes only; any errors are fatal.
|
||||
func NewTestDispenser(input string) *Dispenser {
|
||||
tokens, err := allTokens("Testfile", []byte(input))
|
||||
if err != nil && err != io.EOF {
|
||||
log.Fatalf("getting all tokens from input: %v", err)
|
||||
}
|
||||
return NewDispenser(tokens)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Format formats the input Caddyfile to a standard, nice-looking
|
||||
// appearance. It works by reading each rune of the input and taking
|
||||
// control over all the bracing and whitespace that is written; otherwise,
|
||||
// words, comments, placeholders, and escaped characters are all treated
|
||||
// literally and written as they appear in the input.
|
||||
func Format(input []byte) []byte {
|
||||
input = bytes.TrimSpace(input)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
rdr := bytes.NewReader(input)
|
||||
|
||||
var (
|
||||
last rune // the last character that was written to the result
|
||||
|
||||
space = true // whether current/previous character was whitespace (beginning of input counts as space)
|
||||
beginningOfLine = true // whether we are at beginning of line
|
||||
|
||||
openBrace bool // whether current word/token is or started with open curly brace
|
||||
openBraceWritten bool // if openBrace, whether that brace was written or not
|
||||
openBraceSpace bool // whether there was a non-newline space before open brace
|
||||
|
||||
newLines int // count of newlines consumed
|
||||
|
||||
comment bool // whether we're in a comment
|
||||
quoted bool // whether we're in a quoted segment
|
||||
escaped bool // whether current char is escaped
|
||||
|
||||
nesting int // indentation level
|
||||
)
|
||||
|
||||
write := func(ch rune) {
|
||||
out.WriteRune(ch)
|
||||
last = ch
|
||||
}
|
||||
|
||||
indent := func() {
|
||||
for tabs := nesting; tabs > 0; tabs-- {
|
||||
write('\t')
|
||||
}
|
||||
}
|
||||
|
||||
nextLine := func() {
|
||||
write('\n')
|
||||
beginningOfLine = true
|
||||
}
|
||||
|
||||
for {
|
||||
ch, _, err := rdr.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if comment {
|
||||
if ch == '\n' {
|
||||
comment = false
|
||||
} else {
|
||||
write(ch)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !escaped && ch == '\\' {
|
||||
if space {
|
||||
write(' ')
|
||||
space = false
|
||||
}
|
||||
write(ch)
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if escaped {
|
||||
write(ch)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if ch == '"' {
|
||||
quoted = false
|
||||
}
|
||||
write(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if space && ch == '"' {
|
||||
quoted = true
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
space = true
|
||||
if ch == '\n' {
|
||||
newLines++
|
||||
}
|
||||
continue
|
||||
}
|
||||
spacePrior := space
|
||||
space = false
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// I find it helpful to think of the formatting loop in two
|
||||
// main sections; by the time we reach this point, we
|
||||
// know we are in a "regular" part of the file: we know
|
||||
// the character is not a space, not in a literal segment
|
||||
// like a comment or quoted, it's not escaped, etc.
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
if ch == '#' {
|
||||
if !spacePrior && !beginningOfLine {
|
||||
write(' ')
|
||||
}
|
||||
comment = true
|
||||
}
|
||||
|
||||
if openBrace && spacePrior && !openBraceWritten {
|
||||
if nesting == 0 && last == '}' {
|
||||
nextLine()
|
||||
nextLine()
|
||||
}
|
||||
|
||||
openBrace = false
|
||||
if beginningOfLine {
|
||||
indent()
|
||||
} else if !openBraceSpace {
|
||||
write(' ')
|
||||
}
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
nextLine()
|
||||
newLines = 0
|
||||
nesting++
|
||||
}
|
||||
|
||||
switch {
|
||||
case ch == '{':
|
||||
openBrace = true
|
||||
openBraceWritten = false
|
||||
openBraceSpace = spacePrior && !beginningOfLine
|
||||
if openBraceSpace {
|
||||
write(' ')
|
||||
}
|
||||
continue
|
||||
|
||||
case ch == '}' && (spacePrior || !openBrace):
|
||||
if last != '\n' {
|
||||
nextLine()
|
||||
}
|
||||
if nesting > 0 {
|
||||
nesting--
|
||||
}
|
||||
indent()
|
||||
write('}')
|
||||
newLines = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if newLines > 2 {
|
||||
newLines = 2
|
||||
}
|
||||
for i := 0; i < newLines; i++ {
|
||||
nextLine()
|
||||
}
|
||||
newLines = 0
|
||||
if beginningOfLine {
|
||||
indent()
|
||||
}
|
||||
if nesting == 0 && last == '}' && beginningOfLine {
|
||||
nextLine()
|
||||
nextLine()
|
||||
}
|
||||
|
||||
if !beginningOfLine && spacePrior {
|
||||
write(' ')
|
||||
}
|
||||
|
||||
if openBrace && !openBraceWritten {
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
}
|
||||
write(ch)
|
||||
|
||||
beginningOfLine = false
|
||||
}
|
||||
|
||||
// the Caddyfile does not need any leading or trailing spaces, but...
|
||||
trimmedResult := bytes.TrimSpace(out.Bytes())
|
||||
|
||||
// ...Caddyfiles should, however, end with a newline because
|
||||
// newlines are significant to the syntax of the file
|
||||
return append(trimmedResult, '\n')
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user