mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
1004 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d26cd24a11 | |||
| 8c803b503c | |||
| 7d483ff3ef | |||
| 02b8640e44 | |||
| d395633749 | |||
| 4715bbfd64 | |||
| c6f2979986 | |||
| a211c656f1 | |||
| 48ce47f1d4 | |||
| ef4e0224a8 | |||
| c8a76d003f | |||
| dd5decabe7 | |||
| d7564d632f | |||
| 88fd5f3491 | |||
| 2ae58ac13e | |||
| 01be1b54a8 | |||
| 41f5dd56e1 | |||
| 16724842d9 | |||
| 792f1c7ed7 | |||
| c8adb1b553 | |||
| 9b4acc2449 | |||
| f3aead0e4d | |||
| 571f88d86f | |||
| 0e829bc418 | |||
| 4b1a9b6cc1 | |||
| 1a345b4fa6 | |||
| 22c98ea165 | |||
| 2faeac0a10 | |||
| 9dda8fbf84 | |||
| ff67b97126 | |||
| 5b44d6cea8 | |||
| 6ab9fb6f74 | |||
| f4bf4e0097 | |||
| 21f9c20a04 | |||
| 2d12fb7ac6 | |||
| 91e62db666 | |||
| c050a37e1c | |||
| 5c47c2f147 | |||
| ffd28be90a | |||
| 141c785420 | |||
| dcbf38d0b3 | |||
| 2028da4e74 | |||
| 4ade967005 | |||
| 8af646730b | |||
| 098897bdea | |||
| 8ccfedf2bb | |||
| 2bb2ecc549 | |||
| 54a0c8f948 | |||
| 043fe41ab8 | |||
| 9ddb78fadc | |||
| 3a48b03369 | |||
| 7cf8376e63 | |||
| 21af88fefc | |||
| 52bad45181 | |||
| b85b6c6469 | |||
| 59cbb2c83a | |||
| a8b0dfa8da | |||
| b198678174 | |||
| 840094ac65 | |||
| 976469ca0d | |||
| 9cc26ee7bf | |||
| 3579815a6c | |||
| 61fe152c60 | |||
| 3afa02ba4e | |||
| 806f5b1117 | |||
| b2492f8567 | |||
| 07c863637d | |||
| dc2a5d5c52 | |||
| 4943a4fc52 | |||
| 630c62b313 | |||
| 9338741ca7 | |||
| 88c7e53da5 | |||
| 4ef360745d | |||
| 7142d7c1e4 | |||
| c3fb5f4d3f | |||
| 15d986e1c9 | |||
| f350e001b6 | |||
| 0287009ee5 | |||
| f8861ca16b | |||
| c2ccf8690f | |||
| 99dcdf7e42 | |||
| fab6375a8b | |||
| aca4002fd8 | |||
| 8e0d3e1ec5 | |||
| d85cc2ec10 | |||
| 04fb9fe87f | |||
| 0bc27e5fb1 | |||
| 9be4f194e0 | |||
| a10117f8bd | |||
| 101d3e7407 | |||
| 3f1add6c9f | |||
| 5db2f81695 | |||
| 243351b2b1 | |||
| 198f4385d2 | |||
| e7ecc7ede2 | |||
| 7088605cc1 | |||
| 15faeacb60 | |||
| f8a2c60297 | |||
| 01308b4bae | |||
| b7280e6949 | |||
| a63767d3f8 | |||
| 40c582ce82 | |||
| a52917a37d | |||
| e6f46c8d78 | |||
| f6d2c293e7 | |||
| 2ce5c65269 | |||
| 61917c3443 | |||
| 224316eaec | |||
| 5f6758dab5 | |||
| a6a45ff6c5 | |||
| 73e094e1dd | |||
| d79c0f0dec | |||
| db3e19b7b5 | |||
| 1fc151faec | |||
| 9ba999141b | |||
| f98f449f05 | |||
| e66040a6f0 | |||
| 44860482d2 | |||
| 4c90f1427f | |||
| fb63e2e40c | |||
| 583c585c81 | |||
| 4356635d12 | |||
| 4af38e5ac8 | |||
| 399186abfc | |||
| 6dce4934f0 | |||
| 874d0ce822 | |||
| abdf1ae15c | |||
| d7e3a1974b | |||
| e60148ecc3 | |||
| 0b5720faa5 | |||
| dd203ad41f | |||
| b2b29dcd49 | |||
| c97292b255 | |||
| b52271061d | |||
| d05d715a00 | |||
| 8d7ac18402 | |||
| 7e2510ef43 | |||
| feeb6af403 | |||
| d129ae6aec | |||
| 87c7127c28 | |||
| 2fc620d38d | |||
| a46ff50a1c | |||
| cabb5d71c4 | |||
| ba5811467a | |||
| 1b9042bcdd | |||
| 4d6370bf92 | |||
| c6eb186064 | |||
| 76c4cf5a56 | |||
| 797973944f | |||
| 6d97d8d87b | |||
| d404005339 | |||
| 868af6a062 | |||
| d2668cdbb0 | |||
| 6a02999054 | |||
| 9f97df2275 | |||
| d93e027e01 | |||
| 613d544a47 | |||
| 726a9a8fde | |||
| d00824f4a6 | |||
| 8f87c5d993 | |||
| c6673ad4d8 | |||
| 9ab09433de | |||
| 3067074d9c | |||
| 3efda6fb3a | |||
| 9cd472c031 | |||
| e0daa39cd3 | |||
| 70953e873a | |||
| eafc875ea9 | |||
| 03e0a010d1 | |||
| 3609a4af75 | |||
| 26748d06b4 | |||
| b40cacf5ce | |||
| 81413caea2 | |||
| dc9dd2e4b3 | |||
| 567d96c624 | |||
| 5d8b45c9fb | |||
| 0b381eb766 | |||
| 83ef61de10 | |||
| e1f4b83ffa | |||
| 185ed6fe7c | |||
| 4a0492f3e1 | |||
| 654a3bb090 | |||
| f4840cfeb8 | |||
| a4a64a6f6e | |||
| 88d65967b5 | |||
| 1c4a807667 | |||
| 45132c5b24 | |||
| 1217449609 | |||
| e0bf179c1a | |||
| 7b48ce0e7e | |||
| 924010cd3d | |||
| 74949fb091 | |||
| ddb1d2c2b1 | |||
| 7f227b9d39 | |||
| 0dd0487eba | |||
| db9d167354 | |||
| 29f57faa86 | |||
| 0c01547037 | |||
| e7336cc3bf | |||
| 97a56d860a | |||
| d13258423d | |||
| 32f7dd44ae | |||
| 63d597c09d | |||
| e65b97f55b | |||
| a9768d2fde | |||
| 52822a41cb | |||
| 5b5f8feaf7 | |||
| c93e30454f | |||
| 1bd598e90c | |||
| e698ec5139 | |||
| c27425ef5d | |||
| 258d906140 | |||
| 69290d232d | |||
| 277472d081 | |||
| 5a4374bea0 | |||
| 0d44e3ecba | |||
| 2a78c9c5e4 | |||
| 01d5568b20 | |||
| 1f4a6fa7e7 | |||
| 5ed8689629 | |||
| 3ae07a73dc | |||
| e473ae6803 | |||
| 72ce78d9af | |||
| 8f8204708a | |||
| 46c5db92da | |||
| de4959fe7b | |||
| 03f703a00e | |||
| 931656bd68 | |||
| da6a569e85 | |||
| 4512be49a9 | |||
| f8143a3af1 | |||
| 8bbf8ec629 | |||
| 4284e39a17 | |||
| 53f7035299 | |||
| b893c8c5f8 | |||
| 127788807f | |||
| 2c48dda109 | |||
| 30d63648f5 | |||
| 21744b6c4c | |||
| f9e11158bc | |||
| 91ec75441a | |||
| e7a534d0a3 | |||
| c78ebb3d6a | |||
| a6d9f9be5b | |||
| 2348ac897a | |||
| d3f23a8eeb | |||
| 60abd72c7a | |||
| b8f729b88f | |||
| e1aa862e6a | |||
| 8c2a72ad07 | |||
| bde46211e3 | |||
| bc1e63198d | |||
| feb07a7b59 | |||
| a7479302fc | |||
| 223f314331 | |||
| 1919c08ecc | |||
| 57c5b921a4 | |||
| e1b9a9d7b0 | |||
| 697cc593a1 | |||
| 2fe69a828f | |||
| c369df5c37 | |||
| 7c48b5fdbb | |||
| e965b111cd | |||
| b9c40e7111 | |||
| f5344f8cad | |||
| 750d0b8331 | |||
| 54823f52bc | |||
| ed7e3c906a | |||
| c0273f1f04 | |||
| dba556fe4b | |||
| d9aded016c | |||
| 4181c79a81 | |||
| 5e2f1b5ced | |||
| f3e849e49f | |||
| f658fd05ac | |||
| cc0c0cf03e | |||
| 80acf1bf23 | |||
| c839a98ff5 | |||
| b359ca565c | |||
| c2d889f85e | |||
| cb86319bd5 | |||
| ed41c924cf | |||
| d9ff7b1872 | |||
| 76611fa150 | |||
| 8a50f191bf | |||
| 4f3f6e35e8 | |||
| 787f6b257f | |||
| b568a10dd4 | |||
| 8f9ffc587e | |||
| f976c84d9e | |||
| 1bf72db6ff | |||
| d54dcf1598 | |||
| 3248e4c89f | |||
| da7d8cb26d | |||
| 387545a895 | |||
| b49ec05161 | |||
| b16aba5c27 | |||
| 362f33daae | |||
| 3d7d60f7cf | |||
| dc12bd9743 | |||
| 56c6b3f673 | |||
| cbbd1df904 | |||
| 7d919af01b | |||
| 4a09cf0dc0 | |||
| b24ae63ea6 | |||
| 4173e2c77a | |||
| 18f34290d2 | |||
| 22eecdb90c | |||
| 4de2c1c65e | |||
| 878d491834 | |||
| 96f638eaad | |||
| 7e52db8280 | |||
| 3b3d678714 | |||
| ee358550e4 | |||
| 3f55efcfde | |||
| f71d779009 | |||
| d949caf459 | |||
| ac0ad4da84 | |||
| 4c10a05431 | |||
| fe2a02bf7a | |||
| 9fc55a9792 | |||
| 4e8245df0b | |||
| ac1f20b9e4 | |||
| 174c19a953 | |||
| c8559c4485 | |||
| 24b0ecc310 | |||
| 7c82e265da | |||
| 0900844c81 | |||
| 7984e6f6fd | |||
| d70608b656 | |||
| 1f60328e17 | |||
| 0e204b730a | |||
| fae195ac7e | |||
| 130f6d1f83 | |||
| 289934f3d1 | |||
| 3a3182fba3 | |||
| e8b8d4a8cd | |||
| a8586b05aa | |||
| 05dbe1c171 | |||
| 33d8d2c6b5 | |||
| 9c419f1e1a | |||
| b245ecd325 | |||
| 2a6859a5e4 | |||
| df99502977 | |||
| e0aaefab80 | |||
| fa5a579b60 | |||
| 88b4fbf244 | |||
| 5653c36bc2 | |||
| 4feac4d83c | |||
| 82c356f254 | |||
| 1405683c2b | |||
| 89c407aa34 | |||
| 58ab3a01a0 | |||
| a306c5f769 | |||
| 1e0dea59ef | |||
| 2cac3c5491 | |||
| f2ab7099db | |||
| 50cea4e263 | |||
| 1b73e3862d | |||
| c46ec3b500 | |||
| ed8bb13c5d | |||
| b7e472d548 | |||
| 7103ea096f | |||
| 888c6d7e93 | |||
| b377208ede | |||
| 4776f62caa | |||
| 38a7b6b3d0 | |||
| 84d5e1c5d6 | |||
| 288216e1fb | |||
| 10053f7570 | |||
| 0a6d3333b2 | |||
| 568fd2b286 | |||
| f11c3c9f5a | |||
| 936ee918ee | |||
| d6f86cccf5 | |||
| 2d7d806fcf | |||
| d8135505d3 | |||
| 11166889c5 | |||
| 080db93817 | |||
| a8492c064d | |||
| 6cdcc2a782 | |||
| fbb0ecfa32 | |||
| 5b9c850ab3 | |||
| b32f265eca | |||
| 431adc0980 | |||
| a8cc5d1a7d | |||
| 8d304a4566 | |||
| 65e33fc1ee | |||
| 9f34383c02 | |||
| b07b198764 | |||
| 51b1bfb125 | |||
| c049bab458 | |||
| e2fc08bd34 | |||
| 4aa4f3ac70 | |||
| 1913930783 | |||
| cd486c25d1 | |||
| e198c605bd | |||
| f66493efef | |||
| 5c51c1db2c | |||
| da23501457 | |||
| 94749e119a | |||
| d7d16360d4 | |||
| 4df27a20c8 | |||
| 18c309b5fa | |||
| e041962b66 | |||
| f45a6de20d | |||
| b51dc5d5d0 | |||
| f857b32d65 | |||
| 4e36b4c9d1 | |||
| 27bc16abed | |||
| bbe1952a59 | |||
| 0e2c7e1d35 | |||
| 7ceef91295 | |||
| 5dec11f2a0 | |||
| 66114cb155 | |||
| 7914ba3573 | |||
| dfe17c33ef | |||
| 710824c3ce | |||
| d8ae801068 | |||
| 119e8794bc | |||
| 22927e278d | |||
| 7a69ae7571 | |||
| 2b2addebb8 | |||
| 9563666bfb | |||
| 806341e089 | |||
| 0468508e92 | |||
| 415d1e7b6f | |||
| 1a36b06cd4 | |||
| 398c12ae9b | |||
| 361946eb0c | |||
| 424ae0f420 | |||
| 4548b7de8e | |||
| 3b19aa2b5a | |||
| 6a41b62e70 | |||
| 2ddb717144 | |||
| 56af1ceb32 | |||
| 4ba03c9d38 | |||
| 078f130a51 | |||
| 9c180a5988 | |||
| 467b7e3a9c | |||
| 31d75acc9c | |||
| 9cde715525 | |||
| 942fbb37ec | |||
| cee4441cb1 | |||
| 5bd9c49042 | |||
| cdd3884b32 | |||
| 2615c9c524 | |||
| 5336bc0fb6 | |||
| 29452647d8 | |||
| bd34cb6b4e | |||
| 2d236ead3e | |||
| 38cb587e0f | |||
| ca14b6edd9 | |||
| cbf16f6d9e | |||
| 13a37688dc | |||
| e8352aef38 | |||
| 36546cd8b9 | |||
| 75b690d248 | |||
| 52d7335c2b | |||
| 96919acc9d | |||
| e96aafe1ca | |||
| a02ecb0f88 | |||
| 5ebb7d496d | |||
| cfc85ae8ca | |||
| faf0399e80 | |||
| 808b05c3b4 | |||
| 12b2f22092 | |||
| 571fc034d3 | |||
| bef1a739db | |||
| 0de6064c3b | |||
| 774f228868 | |||
| b19946f6af | |||
| 335cd2e8a4 | |||
| 48598e1f2a | |||
| cdce452edc | |||
| f3e8b9d95f | |||
| c8032867b1 | |||
| 3f20a7c9f3 | |||
| 1af419e7ec | |||
| f0e3981774 | |||
| 1c9ea0113d | |||
| 2b04e09fa7 | |||
| 3443a8a056 | |||
| 2943c41884 | |||
| 53b6fab125 | |||
| c6ac350a3b | |||
| b301a3df70 | |||
| 998c6e06a7 | |||
| 4636109ce1 | |||
| 205b142614 | |||
| ff35ba9ec3 | |||
| d8d87a378f | |||
| f8b59e77f8 | |||
| 508cf2aa22 | |||
| f9bd2d3e92 | |||
| b1366c7e46 | |||
| b6fe5d4b41 | |||
| 66e571e687 | |||
| 2b3046de36 | |||
| 1aef807c71 | |||
| e16a886814 | |||
| dd86171d67 | |||
| f5a13a4ab4 | |||
| 10b265d252 | |||
| 05e9974570 | |||
| 330be2d8c7 | |||
| 0cc49c053f | |||
| a7db0cfe55 | |||
| 2182270a2c | |||
| a7af7c486e | |||
| b97c76fb47 | |||
| 6cc3cbbc69 | |||
| 9e943319b4 | |||
| b420561737 | |||
| c05e3898b9 | |||
| b3f0cea2c3 | |||
| 94d41a9d86 | |||
| 99d47050e9 | |||
| 85375861f6 | |||
| f6bab8ba85 | |||
| 941eae5f61 | |||
| 096971e313 | |||
| f3379f650a | |||
| 960150bb03 | |||
| 9e6919550b | |||
| 167981d258 | |||
| 8cb1bb4af3 | |||
| e3909cc385 | |||
| be53e432fc | |||
| 79de6df93d | |||
| 8bc05e598d | |||
| bf54892a73 | |||
| 5ded580444 | |||
| 0db29e2ce9 | |||
| 4b119a475f | |||
| 90798f3eea | |||
| 536c28d4dc | |||
| c77a6bea66 | |||
| 12bcbe2c49 | |||
| f6f1d8fc89 | |||
| 8d3a1b8bcb | |||
| ac83b7e218 | |||
| e62b5fb586 | |||
| 94b8d56096 | |||
| 8c0b49bf03 | |||
| 201b9b41f9 | |||
| 0a3efd1641 | |||
| d73660f7c3 | |||
| 7f2a93e6c3 | |||
| e9d95ab29f | |||
| 962310204f | |||
| 98867ac346 | |||
| 5805b3ca11 | |||
| d6d7511699 | |||
| 8d6870fd06 | |||
| c38a040e85 | |||
| e8ad9b32c9 | |||
| 62e8b21724 | |||
| 223cbe3d0b | |||
| 66ce0c5c63 | |||
| 845bc4d50b | |||
| e450a7377b | |||
| d74f6fd967 | |||
| 55035d327a | |||
| 4e9ad50f65 | |||
| 05a4637489 | |||
| bd74f94496 | |||
| b40548ff61 | |||
| 4e54e48409 | |||
| b166b90083 | |||
| dac7cacd4d | |||
| af93517c2d | |||
| 3b724a2082 | |||
| 329af5ced9 | |||
| cd49847edb | |||
| d3d76d6ac2 | |||
| c3b5b1811c | |||
| 4fe5e64e46 | |||
| e10ed7b00d | |||
| fac35db9dc | |||
| bfaf2a8201 | |||
| fef9cb3e05 | |||
| d4a7d89f56 | |||
| ae77a56ac8 | |||
| 762b02789a | |||
| 6f8fe01da1 | |||
| ac96455a9a | |||
| ee7c92ec9b | |||
| 33fdea8f26 | |||
| 6efd1b3bb1 | |||
| 087f126cf4 | |||
| 1fa4cb7ba1 | |||
| f20a8e7aa0 | |||
| 798c4a3ba4 | |||
| 817470dd66 | |||
| bbe3663167 | |||
| ed503118dd | |||
| a3ae146cbd | |||
| 4bf6cb4199 | |||
| 72e7edda1f | |||
| a999b70727 | |||
| 1cd594963e | |||
| 6bad878a22 | |||
| 3e1fd2a8d4 | |||
| 33f60da9f2 | |||
| b4e28af953 | |||
| d46ba2e27f | |||
| 498f32bab9 | |||
| ed118f2b09 | |||
| 99ffe93388 | |||
| e07a267276 | |||
| e4fac1294f | |||
| 2153a81ec8 | |||
| ea58d51907 | |||
| 9e1d964bd6 | |||
| 2be56c526c | |||
| 01e192edc9 | |||
| 2808de1e30 | |||
| 253d97c93d | |||
| c28cd29fe7 | |||
| da24f57dac | |||
| b1d04f5b39 | |||
| fe91de67b6 | |||
| 9873ff9918 | |||
| 5e52bbb136 | |||
| fcdbc69fab | |||
| 2a8c458ffe | |||
| 037dc23cad | |||
| ab720fb768 | |||
| e2991eb019 | |||
| 897a38958c | |||
| 61822f129b | |||
| e3e8aabbcf | |||
| 013b510352 | |||
| d0556929a4 | |||
| b5727b9c44 | |||
| 7041970059 | |||
| e747a9bb12 | |||
| f7c1a51efb | |||
| eead00f54a | |||
| 9206e8a738 | |||
| 1426c97da5 | |||
| 44ad0cedaf | |||
| beb7dcbf2a | |||
| 821a08a6e3 | |||
| e3d04ff86b | |||
| da8b7fe58f | |||
| 0950ba4f0b | |||
| c7a6bc5934 | |||
| 00beec2e34 | |||
| b4643994d5 | |||
| e43b6d8178 | |||
| bffc258732 | |||
| 616418281b | |||
| 74547f5bed | |||
| 258071d857 | |||
| b6cec37893 | |||
| 48d723c07c | |||
| f1f7a22674 | |||
| 49b7a25264 | |||
| e6c58fdc08 | |||
| 2dc747cf2d | |||
| e338648fed | |||
| 9ad0ebc956 | |||
| a1ad20e472 | |||
| 62b0685375 | |||
| 0b3161aeea | |||
| 754fe4f7b4 | |||
| 20d487be57 | |||
| 61c75f74de | |||
| d35f618b10 | |||
| 9fe4f93bc7 | |||
| c5df7bb6bd | |||
| 076a8b8095 | |||
| 50748e19c3 | |||
| c19f207237 | |||
| dd9813c65b | |||
| 1c9c8f6a13 | |||
| 8cc8f9fddd | |||
| 8f6a88e2b0 | |||
| fded2644f8 | |||
| 487217519c | |||
| 0499d9c1c4 | |||
| 5dfa08174a | |||
| d5ea43fb4b | |||
| ca4fae64d9 | |||
| ad69503aef | |||
| 6e3063b15a | |||
| d6b3c7d262 | |||
| 66476d8c8f | |||
| d3c3fa10bd | |||
| 83b26975bd | |||
| 005c5a6382 | |||
| 6c0d0511ba | |||
| 5c7ae5e505 | |||
| 59286d2c7e | |||
| 66959d9f18 | |||
| f2a7e7c966 | |||
| ec2a5762b0 | |||
| e77992dd99 | |||
| aefd821ae0 | |||
| d062fb4020 | |||
| 73d4a8ba02 | |||
| 7d5108d132 | |||
| 7c35bfa57c | |||
| 1edc1a45e3 | |||
| cb849bd664 | |||
| 3cd7437b3d | |||
| d4d8bbcfc6 | |||
| 68d8ac9802 | |||
| 2d5a30b908 | |||
| 687a4b9e81 | |||
| d605ebe75a | |||
| 258bc82b69 | |||
| 8cb3cf540c | |||
| e1801fdb19 | |||
| 0c57facc67 | |||
| 4c282e86da | |||
| 5fb5b81439 | |||
| 2cc5d38229 | |||
| 66596f2d74 | |||
| b540f195b1 | |||
| 3aabbc49a2 | |||
| bbc923d66b | |||
| e289ba6187 | |||
| a22c08a638 | |||
| 72541f1cb8 | |||
| fe5f5dfd6a | |||
| c7772588bd | |||
| a944de4ab7 | |||
| a479943acd | |||
| dc62d468e9 | |||
| c79c08627d | |||
| e2a5e2293a | |||
| f5dce84a70 | |||
| 922d9f5c25 | |||
| 91ab0e6066 | |||
| 085df25c7e | |||
| fe61209df2 | |||
| 7f6a328b47 | |||
| 7ab61f46f0 | |||
| 8c72f34357 | |||
| b9618b8b98 | |||
| d26559316f | |||
| 2642bd72b7 | |||
| 17ae5acaba | |||
| 1960a0dc11 | |||
| 63c7720e84 | |||
| 141872ed80 | |||
| db1aa5b5bc | |||
| f783290f40 | |||
| ebd6abcbd5 | |||
| 6668271661 | |||
| 07ed3e7c30 | |||
| 1e0cdc54f8 | |||
| 2f43aa0629 | |||
| 56c139f003 | |||
| 35a81d7c5b | |||
| 2e70d1d3bf | |||
| ff2ba6de8a | |||
| 4fced0b6e1 | |||
| 1bdd451913 | |||
| ea8df6ff11 | |||
| c833e3b249 | |||
| 7991cd1250 | |||
| 1e18afb5c8 | |||
| 0bebea0d4c | |||
| a379fa4c6c | |||
| abad9bc256 | |||
| 8bdee04651 | |||
| 7d1f7771c9 | |||
| 04a14ee37a | |||
| c2bbe42fc3 | |||
| ad3a83fb91 | |||
| 53c4d788d4 | |||
| d6bc9e0b5c | |||
| 54d1923ccb | |||
| c0f76e9ed4 | |||
| f259ed52bb | |||
| 8bac134f26 | |||
| 412dcc07d3 | |||
| 660c59b6f3 | |||
| 58e05cab15 | |||
| 10f85558ea | |||
| 98468af8b6 | |||
| 25f10511e7 | |||
| b6e96fa3c5 | |||
| 56013934a4 | |||
| 0b6f764356 | |||
| 050d6e0aeb | |||
| 0bcd02d5f6 | |||
| c82fe91104 | |||
| f9b42c3772 | |||
| aaf6794b31 | |||
| 1498132ea3 | |||
| 7f9b1f43c9 | |||
| 5e729c1e85 | |||
| 0a14f97e49 | |||
| 9864b138fb | |||
| 3d18bc56b9 | |||
| 886ba84baa | |||
| a9267791c4 | |||
| ef0aaca0d6 | |||
| 6891f7f421 | |||
| 499ad6d182 | |||
| 8e6bc36084 | |||
| 58970cae92 | |||
| 9e760e2e0c | |||
| 4b4e99bdb2 | |||
| 57d27c1b58 | |||
| 693e9b5283 | |||
| b687d7b967 | |||
| f7be0ee101 | |||
| f6900fcf53 | |||
| ec86a2f7a3 | |||
| e7fbee8c82 | |||
| e84e19a04e | |||
| 4a223f5203 | |||
| af7321511c | |||
| 0be3d99543 | |||
| 3017b245c9 | |||
| 2e4c09155a | |||
| dcc98da4d2 | |||
| 3ab648382d | |||
| 40b193fb79 | |||
| d543ad1ffd | |||
| a8bb4a665a | |||
| 3a1e0dbf47 | |||
| 77a77c0219 | |||
| db62942d63 | |||
| dadd4b59b0 | |||
| d230b33007 | |||
| 0d13173071 | |||
| c3a82f53d5 | |||
| 30b6d1f47a | |||
| bc15b4b0e7 | |||
| e2535233bb | |||
| 00234c8ac2 | |||
| 6512832f9f | |||
| 3e3bb00265 | |||
| e4ce40f8ff | |||
| afca242111 | |||
| 7d229665ed | |||
| 22d8edb984 | |||
| 734acc776a | |||
| b4f1a71397 | |||
| d06d0e79f8 | |||
| a58f240d3e | |||
| 4b75f3e2f0 | |||
| b8dbecb841 | |||
| 134b805644 | |||
| c9b5e7f77b | |||
| 79cbe7bfd0 | |||
| 55b4c12e04 | |||
| 2196c92c0e | |||
| c2327161f7 | |||
| c5fffb4ac2 | |||
| dc4d147388 | |||
| 93c99f6734 | |||
| 4e9fbee1e2 | |||
| a9c7e94a38 | |||
| 3d616e8c6d | |||
| b82e22b459 | |||
| bf6a1b7538 | |||
| c7d6c4cbb9 | |||
| d0b608af31 | |||
| d9b1d46325 | |||
| c8f2834b51 | |||
| ab0455922a | |||
| c50094fc9d | |||
| d058dee11d | |||
| 09ba9e994e | |||
| be82cc7aca | |||
| 2bb8550a4c | |||
| a72acd21b0 | |||
| a6199cf814 | |||
| ceef70dbc5 | |||
| f5e104944e | |||
| 6b385a36f9 | |||
| 9b7cdfa2f2 | |||
| 78e381b29f | |||
| de490c7cad | |||
| bbad6931e3 | |||
| 5bd96a6ac2 | |||
| ac14b64e08 | |||
| 15c95e9d5b | |||
| bc447e307f | |||
| 87a1f228b4 | |||
| acbee94708 | |||
| 7ea5b2a818 | |||
| 186fdba916 | |||
| 7778912d4e | |||
| c921e08296 | |||
| ddbb234d91 | |||
| 0de51593a6 | |||
| 26d633baf8 | |||
| ff137d17d0 | |||
| 57a708d189 | |||
| 32aad90938 | |||
| 40b54434f3 | |||
| 1d0425b26f | |||
| 7557d1d922 | |||
| ff74a0aa09 | |||
| 599c81d753 | |||
| 741b0502ee | |||
| 7ca5921a87 | |||
| da4a759bad | |||
| 042abeb431 | |||
| eb891d4683 | |||
| 44e5e9e43f | |||
| bf380d00ab | |||
| 94035c1797 | |||
| b3f7ce34b4 | |||
| a79b4055e5 | |||
| 5a07156894 | |||
| bcb7a19cd3 | |||
| 6e6ce2be6b | |||
| 1b7ff5d76c | |||
| 93a7a45e7e | |||
| 1a7a78a1f2 | |||
| 1feb65952a | |||
| 66de438a98 | |||
| 850e1605df | |||
| af1ac9cd2e | |||
| 64a3218f5c | |||
| c634bbe9cc | |||
| 4b9849c792 | |||
| 80d7a356b3 | |||
| b4bfa29be2 | |||
| 6cadb60fa2 | |||
| 2e46c2ac1d | |||
| 249adc1c87 | |||
| e9dde23024 | |||
| 3fe2c73dd0 | |||
| 5333c3528b | |||
| 180ae0cc48 | |||
| a1c41210d3 | |||
| ecac03cdcb | |||
| c04d24cafa | |||
| 81ee34e962 | |||
| 78b5356f2b | |||
| 6f9b6ad78e | |||
| 4906b9357a | |||
| e90d751732 | |||
| dce81e85d5 | |||
| a1b417c832 | |||
| 5bf0adad87 | |||
| 8e5aafa5cd | |||
| c133153447 | |||
| ec14ccdd40 | |||
| f55b123d63 | |||
| 0eb0b60f47 | |||
| 5e5af50e64 | |||
| 9ee68c1bd5 | |||
| 789efa5dee | |||
| 8887adb027 | |||
| bcac2beee7 | |||
| 1e10f6f725 | |||
| c8b5a81607 | |||
| eead337324 | |||
| 7d5047c1f1 | |||
| 7f364c777a | |||
| b47af6ef04 | |||
| e81369e220 | |||
| e7457b43e4 | |||
| f376a38b25 | |||
| 749e55c738 | |||
| 24fda7514d | |||
| 3385856966 | |||
| f73f55dba7 | |||
| 012d235314 | |||
| 997e41deae | |||
| 0ffb2229b0 | |||
| a21d5a001f | |||
| a2119c09e9 | |||
| 062657d0d8 | |||
| b092061591 | |||
| 64f8b557b1 | |||
| 95c035060f | |||
| c4790d7f9d | |||
| 837cdc566d | |||
| be5f77e84d | |||
| cbb045a121 | |||
| c48fadc4a7 | |||
| 059fc32f00 | |||
| e2d964ea30 | |||
| 501da21f20 | |||
| 3336faf254 | |||
| 16f752125f | |||
| 0a5f7a677f | |||
| d3a0259944 | |||
| 5fda9610f9 | |||
| 3f2c3ecf85 | |||
| 907e2d8d3a | |||
| 33c70f418f | |||
| 2ebfda1ae9 | |||
| 2392478bd3 | |||
| a437206643 | |||
| a779e1b383 | |||
| 46ab93be51 | |||
| e0fc46a911 | |||
| 9f6393c64c | |||
| 105dac8c2a | |||
| 4ebf100f09 |
@@ -0,0 +1,5 @@
|
|||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
[caddytest/integration/caddyfile_adapt/*.caddyfiletest]
|
||||||
|
indent_style = tab
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.go text eol=lf
|
||||||
+17
-7
@@ -1,7 +1,7 @@
|
|||||||
Contributing to Caddy
|
Contributing to Caddy
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement!
|
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be nearly as excellent without your involvement!
|
||||||
|
|
||||||
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
|
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Other menu items:
|
|||||||
|
|
||||||
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
|
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
|
||||||
|
|
||||||
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
|
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergeable.
|
||||||
|
|
||||||
Here are some of the expectations we have of contributors:
|
Here are some of the expectations we have of contributors:
|
||||||
|
|
||||||
@@ -35,19 +35,29 @@ Here are some of the expectations we have of contributors:
|
|||||||
|
|
||||||
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
|
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
|
||||||
|
|
||||||
- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
|
- **Write tests.** Good, automated tests are very valuable! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
|
||||||
|
|
||||||
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling.
|
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks and profiling.
|
||||||
|
|
||||||
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
|
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
|
||||||
|
|
||||||
- **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
|
- **Be responsible for and maintain your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
|
||||||
|
|
||||||
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
|
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
|
||||||
|
|
||||||
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a bit. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo.
|
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a lot. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo. Plus, because Caddy is extensible, it's possible your feature could make a great plugin instead!
|
||||||
|
|
||||||
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
|
- **You certify that you wrote and comprehend the code you submit.** The Caddy project welcomes original contributions that comply with [our CLA](https://cla-assistant.io/caddyserver/caddy), meaning that authors must be able to certify that they created or have rights to the code they are contributing. In addition, we require that code is not simply copy-pasted from Q/A sites or AI language models without full comprehension and rigorous testing. In other words: contributors are allowed to refer to communities for assistance and use AI tools such as language models for inspiration, but code which originates from or is assisted by these resources MUST be:
|
||||||
|
|
||||||
|
- Licensed for you to freely share
|
||||||
|
- Fully comprehended by you (be able to explain every line of code)
|
||||||
|
- Verified by automated tests when feasible, or thorough manual tests otherwise
|
||||||
|
|
||||||
|
We have found that current language models (LLMs, like ChatGPT) may understand code syntax and even problem spaces to an extent, but often fail in subtle ways to convey true knowledge and produce correct algorithms. Integrated tools such as GitHub Copilot and Sourcegraph Cody may be used for inspiration, but code generated by these tools still needs to meet our criteria for licensing, human comprehension, and testing. These tools may be used to help write code comments and tests as long as you can certify they are accurate and correct. Note that it is often more trouble than it's worth to certify that Copilot (for example) is not giving you code that is possibly plagiarised, unlicensed, or licensed with incompatible terms -- as the Caddy project cannot accept such contributions. If that's too difficult for you (or impossible), then we recommend using these resources only for inspiration and write your own code. Ultimately, you (the contributor) are responsible for the code you're submitting.
|
||||||
|
|
||||||
|
As a courtesy to reviewers, we kindly ask that you disclose when contributing code that was generated by an AI tool or copied from another website so we can be aware of what to look for in code review.
|
||||||
|
|
||||||
|
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base.
|
||||||
|
|
||||||
|
|
||||||
#### HOW TO MAKE A PULL REQUEST TO CADDY
|
#### HOW TO MAKE A PULL REQUEST TO CADDY
|
||||||
|
|||||||
+4
-4
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 2.x | :white_check_mark: |
|
| 2.x | ✔️ |
|
||||||
| 1.x | :x: |
|
| 1.x | :x: |
|
||||||
| < 1.x | :x: |
|
| < 1.x | :x: |
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ We do not accept reports if the steps imply or require a compromised system or t
|
|||||||
|
|
||||||
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
|
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
|
||||||
|
|
||||||
Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
||||||
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
@@ -42,13 +42,13 @@ We'll need enough information to verify the bug and make a patch. To speed thing
|
|||||||
- Specific minimal steps to reproduce the issue from scratch
|
- Specific minimal steps to reproduce the issue from scratch
|
||||||
- A working patch
|
- A working patch
|
||||||
|
|
||||||
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
|
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl -v` instead of web browsers.
|
||||||
|
|
||||||
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
|
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
|
||||||
|
|
||||||
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
|
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
|
||||||
|
|
||||||
When you are ready, please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com].
|
When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com.
|
||||||
|
|
||||||
Please don't encrypt the email body. It only makes the process more complicated.
|
Please don't encrypt the email body. It only makes the process more complicated.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
+93
-42
@@ -18,35 +18,53 @@ jobs:
|
|||||||
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
os:
|
||||||
go: [ '1.16', '1.17' ]
|
- linux
|
||||||
|
- mac
|
||||||
|
- windows
|
||||||
|
go:
|
||||||
|
- '1.22'
|
||||||
|
- '1.23'
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
|
- go: '1.22'
|
||||||
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
|
- go: '1.23'
|
||||||
|
GO_SEMVER: '~1.23.0'
|
||||||
|
|
||||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||||
|
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
|
||||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||||
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||||
include:
|
- os: linux
|
||||||
- os: ubuntu-latest
|
OS_LABEL: ubuntu-latest
|
||||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||||
SUCCESS: 0
|
SUCCESS: 0
|
||||||
|
|
||||||
- os: macos-latest
|
- os: mac
|
||||||
|
OS_LABEL: macos-14
|
||||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||||
SUCCESS: 0
|
SUCCESS: 0
|
||||||
|
|
||||||
- os: windows-latest
|
- os: windows
|
||||||
|
OS_LABEL: windows-latest
|
||||||
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
|
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
|
||||||
SUCCESS: 'True'
|
SUCCESS: 'True'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.OS_LABEL }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
# These tools would be useful if we later decide to reinvestigate
|
# These tools would be useful if we later decide to reinvestigate
|
||||||
# publishing test/coverage reports to some tool for easier consumption
|
# publishing test/coverage reports to some tool for easier consumption
|
||||||
@@ -55,10 +73,11 @@ jobs:
|
|||||||
# go get github.com/axw/gocov/gocov
|
# go get github.com/axw/gocov/gocov
|
||||||
# go get github.com/AlekSi/gocov-xml
|
# go get github.com/AlekSi/gocov-xml
|
||||||
# go get -u github.com/jstemmer/go-junit-report
|
# go get -u github.com/jstemmer/go-junit-report
|
||||||
# echo "::add-path::$(go env GOPATH)/bin"
|
# echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: Print Go version and environment
|
- name: Print Go version and environment
|
||||||
id: vars
|
id: vars
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
printf "Using go at: $(which go)\n"
|
printf "Using go at: $(which go)\n"
|
||||||
printf "Go version: $(go version)\n"
|
printf "Go version: $(go version)\n"
|
||||||
@@ -68,16 +87,7 @@ jobs:
|
|||||||
env
|
env
|
||||||
printf "Git version: $(git version)\n\n"
|
printf "Git version: $(git version)\n\n"
|
||||||
# Calculate the short SHA1 hash of the git commit
|
# Calculate the short SHA1 hash of the git commit
|
||||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
|
||||||
|
|
||||||
- name: Cache the build cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
|
||||||
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ matrix.go }}-go-ci
|
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -89,13 +99,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
run: |
|
run: |
|
||||||
go build -trimpath -ldflags="-w -s" -v
|
go build -tags nobadger -trimpath -ldflags="-w -s" -v
|
||||||
|
|
||||||
|
- name: Smoke test Caddy
|
||||||
|
working-directory: ./cmd/caddy
|
||||||
|
run: |
|
||||||
|
./caddy start
|
||||||
|
./caddy stop
|
||||||
|
|
||||||
- name: Publish Build Artifact
|
- name: Publish Build Artifact
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
||||||
path: ${{ matrix.CADDY_BIN_PATH }}
|
path: ${{ matrix.CADDY_BIN_PATH }}
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
# Commented bits below were useful to allow the job to continue
|
# Commented bits below were useful to allow the job to continue
|
||||||
# even if the tests fail, so we can publish the report separately
|
# even if the tests fail, so we can publish the report separately
|
||||||
@@ -105,8 +122,8 @@ jobs:
|
|||||||
# continue-on-error: true
|
# continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./...
|
||||||
# echo "::set-output name=status::$?"
|
# echo "status=$?" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Relevant step if we reinvestigate publishing test/coverage reports
|
# Relevant step if we reinvestigate publishing test/coverage reports
|
||||||
# - name: Prepare coverage reports
|
# - name: Prepare coverage reports
|
||||||
@@ -118,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
# To return the correct result even though we set 'continue-on-error: true'
|
# To return the correct result even though we set 'continue-on-error: true'
|
||||||
# - name: Coerce correct build result
|
# - name: Coerce correct build result
|
||||||
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
|
# if: matrix.os != 'windows' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
|
||||||
# run: |
|
# run: |
|
||||||
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
||||||
# exit 1
|
# exit 1
|
||||||
@@ -126,43 +143,77 @@ jobs:
|
|||||||
s390x-test:
|
s390x-test:
|
||||||
name: test (s390x on IBM Z)
|
name: test (s390x on IBM Z)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
|
||||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code into the Go module directory
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||||
|
|
||||||
# short sha is enough?
|
# short sha is enough?
|
||||||
short_sha=$(git rev-parse --short HEAD)
|
short_sha=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
# To shorten the following lines
|
||||||
|
ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||||
|
ssh_host="$CI_USER@ci-s390x.caddyserver.com"
|
||||||
|
|
||||||
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
||||||
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
|
rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha"
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
|
ssh $ssh_opts -t "$ssh_host" bash <<EOF
|
||||||
|
cd /var/tmp/$short_sha
|
||||||
|
go version
|
||||||
|
go env
|
||||||
|
printf "\n\n"
|
||||||
|
retries=3
|
||||||
|
exit_code=0
|
||||||
|
while ((retries > 0)); do
|
||||||
|
CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./...
|
||||||
|
exit_code=$?
|
||||||
|
if ((exit_code == 0)); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "\n\nTest failed: \$exit_code, retrying..."
|
||||||
|
((retries--))
|
||||||
|
done
|
||||||
|
echo "Remote exit code: \$exit_code"
|
||||||
|
exit \$exit_code
|
||||||
|
EOF
|
||||||
test_result=$?
|
test_result=$?
|
||||||
|
|
||||||
# There's no need leaving the files around
|
# There's no need leaving the files around
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null caddy-ci@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
|
ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'"
|
||||||
|
|
||||||
echo "Test exit code: $test_result"
|
echo "Test exit code: $test_result"
|
||||||
exit $test_result
|
exit $test_result
|
||||||
env:
|
env:
|
||||||
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
|
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
|
||||||
|
CI_USER: ${{ secrets.CI_USER }}
|
||||||
|
|
||||||
goreleaser-check:
|
goreleaser-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Create 'caddy-build'
|
- uses: goreleaser/goreleaser-action@v6
|
||||||
run: mkdir -p caddy-build
|
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v2
|
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "~1.23"
|
||||||
|
check-latest: true
|
||||||
|
- name: Install xcaddy
|
||||||
|
run: |
|
||||||
|
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||||
|
xcaddy version
|
||||||
|
- uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: build --single-target --snapshot
|
||||||
env:
|
env:
|
||||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
TAG: "master"
|
||||||
|
|||||||
@@ -11,19 +11,45 @@ on:
|
|||||||
- 2.*
|
- 2.*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cross-build-test:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
goos:
|
||||||
go: [ '1.16', '1.17' ]
|
- 'aix'
|
||||||
|
- 'linux'
|
||||||
|
- 'solaris'
|
||||||
|
- 'illumos'
|
||||||
|
- 'dragonfly'
|
||||||
|
- 'freebsd'
|
||||||
|
- 'openbsd'
|
||||||
|
- 'windows'
|
||||||
|
- 'darwin'
|
||||||
|
- 'netbsd'
|
||||||
|
go:
|
||||||
|
- '1.22'
|
||||||
|
- '1.23'
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
|
- go: '1.22'
|
||||||
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
|
- go: '1.23'
|
||||||
|
GO_SEMVER: '~1.23.0'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Print Go version and environment
|
- name: Print Go version and environment
|
||||||
id: vars
|
id: vars
|
||||||
@@ -34,29 +60,14 @@ jobs:
|
|||||||
go env
|
go env
|
||||||
printf "\n\nSystem environment:\n\n"
|
printf "\n\nSystem environment:\n\n"
|
||||||
env
|
env
|
||||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
|
||||||
|
|
||||||
- name: Cache the build cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
|
||||||
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
|
|
||||||
|
|
||||||
- name: Checkout code into the Go module directory
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
working-directory: ./cmd/caddy
|
working-directory: ./cmd/caddy
|
||||||
run: |
|
run: |
|
||||||
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
|
GOOS=$GOOS GOARCH=$GOARCH go build -tags nobadger -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "::warning ::$GOOS Build Failed"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -10,16 +10,58 @@ on:
|
|||||||
- master
|
- master
|
||||||
- 2.*
|
- 2.*
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# From https://github.com/golangci/golangci-lint-action
|
# From https://github.com/golangci/golangci-lint-action
|
||||||
golangci:
|
golangci:
|
||||||
|
permissions:
|
||||||
|
contents: read # for actions/checkout to fetch code
|
||||||
|
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||||
name: lint
|
name: lint
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- mac
|
||||||
|
- windows
|
||||||
|
|
||||||
|
include:
|
||||||
|
- os: linux
|
||||||
|
OS_LABEL: ubuntu-latest
|
||||||
|
|
||||||
|
- os: mac
|
||||||
|
OS_LABEL: macos-14
|
||||||
|
|
||||||
|
- os: windows
|
||||||
|
OS_LABEL: windows-latest
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.OS_LABEL }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- uses: actions/setup-go@v5
|
||||||
uses: golangci/golangci-lint-action@v2
|
|
||||||
with:
|
with:
|
||||||
version: v1.31
|
go-version: '~1.23'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
|
||||||
|
args: --timeout 10m
|
||||||
|
|
||||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
# only-new-issues: true
|
# only-new-issues: true
|
||||||
|
|
||||||
|
govulncheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: govulncheck
|
||||||
|
uses: golang/govulncheck-action@v1
|
||||||
|
with:
|
||||||
|
go-version-input: '~1.23.0'
|
||||||
|
check-latest: true
|
||||||
|
|||||||
@@ -10,23 +10,40 @@ jobs:
|
|||||||
name: Release
|
name: Release
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest ]
|
os:
|
||||||
go: [ '1.17' ]
|
- ubuntu-latest
|
||||||
|
go:
|
||||||
|
- '1.23'
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
|
- go: '1.23'
|
||||||
|
GO_SEMVER: '~1.23.0'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||||
|
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
|
||||||
|
# "Releases" is part of `contents`, so it needs the `write`
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
# tl;dr: actions/checkout@v2 runs this line:
|
# tl;dr: actions/checkout@v4 runs this line:
|
||||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||||
# git fetch --prune --unshallow
|
# git fetch --prune --unshallow
|
||||||
@@ -46,9 +63,8 @@ jobs:
|
|||||||
go env
|
go env
|
||||||
printf "\n\nSystem environment:\n\n"
|
printf "\n\nSystem environment:\n\n"
|
||||||
env
|
env
|
||||||
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
|
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
|
||||||
|
|
||||||
# Add "pip install" CLI tools to PATH
|
# Add "pip install" CLI tools to PATH
|
||||||
echo ~/.local/bin >> $GITHUB_PATH
|
echo ~/.local/bin >> $GITHUB_PATH
|
||||||
@@ -60,10 +76,10 @@ jobs:
|
|||||||
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
|
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
|
||||||
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
|
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
|
||||||
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
|
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
|
||||||
echo "::set-output name=tag_major::${TAG_MAJOR}"
|
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=tag_minor::${TAG_MINOR}"
|
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=tag_patch::${TAG_PATCH}"
|
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=tag_special::${TAG_SPECIAL}"
|
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Cloudsmith CLI tooling for pushing releases
|
# Cloudsmith CLI tooling for pushing releases
|
||||||
# See https://help.cloudsmith.io/docs/cli
|
# See https://help.cloudsmith.io/docs/cli
|
||||||
@@ -80,30 +96,31 @@ jobs:
|
|||||||
# tags are only accepted if signed by Matt's key
|
# tags are only accepted if signed by Matt's key
|
||||||
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
||||||
|
|
||||||
- name: Cache the build cache
|
- name: Install Cosign
|
||||||
uses: actions/cache@v2
|
uses: sigstore/cosign-installer@main
|
||||||
with:
|
- name: Cosign version
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
run: cosign version
|
||||||
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
|
- name: Install Syft
|
||||||
restore-keys: |
|
uses: anchore/sbom-action/download-syft@main
|
||||||
${{ runner.os }}-go${{ matrix.go }}-release
|
- name: Syft version
|
||||||
|
run: syft version
|
||||||
- name: Create the 'caddy-build' dir for GoReleaser
|
- name: Install xcaddy
|
||||||
run: |
|
run: |
|
||||||
mkdir -p caddy-build
|
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||||
|
xcaddy version
|
||||||
# GoReleaser will take care of publishing those artifacts into the release
|
# GoReleaser will take care of publishing those artifacts into the release
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist
|
args: release --clean --timeout 60m
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||||
|
COSIGN_EXPERIMENTAL: 1
|
||||||
|
|
||||||
# Only publish on non-special tags (e.g. non-beta)
|
# Only publish on non-special tags (e.g. non-beta)
|
||||||
# We will continue to push to Gemfury for the forseeable future, although
|
# We will continue to push to Gemfury for the foreseeable future, although
|
||||||
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
|
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
|
||||||
# See https://gemfury.com/caddy/deb:caddy
|
# See https://gemfury.com/caddy/deb:caddy
|
||||||
- name: Publish .deb to Gemfury
|
- name: Publish .deb to Gemfury
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ jobs:
|
|||||||
name: Release Published
|
name: Release Published
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest ]
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
# See https://github.com/peter-evans/repository-dispatch
|
# See https://github.com/peter-evans/repository-dispatch
|
||||||
- name: Trigger event on caddyserver/dist
|
- name: Trigger event on caddyserver/dist
|
||||||
uses: peter-evans/repository-dispatch@v1
|
uses: peter-evans/repository-dispatch@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/dist
|
repository: caddyserver/dist
|
||||||
@@ -25,7 +26,7 @@ jobs:
|
|||||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/caddy-docker
|
- name: Trigger event on caddyserver/caddy-docker
|
||||||
uses: peter-evans/repository-dispatch@v1
|
uses: peter-evans/repository-dispatch@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/caddy-docker
|
repository: caddyserver/caddy-docker
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
_gitignore/
|
_gitignore/
|
||||||
*.log
|
*.log
|
||||||
Caddyfile
|
Caddyfile
|
||||||
|
Caddyfile.*
|
||||||
!caddyfile/
|
!caddyfile/
|
||||||
|
!caddyfile.go
|
||||||
|
|
||||||
# artifacts from pprof tooling
|
# artifacts from pprof tooling
|
||||||
*.prof
|
*.prof
|
||||||
@@ -10,6 +12,8 @@ Caddyfile
|
|||||||
# build artifacts and helpers
|
# build artifacts and helpers
|
||||||
cmd/caddy/caddy
|
cmd/caddy/caddy
|
||||||
cmd/caddy/caddy.exe
|
cmd/caddy/caddy.exe
|
||||||
|
cmd/caddy/tmp/*.exe
|
||||||
|
cmd/caddy/.env
|
||||||
|
|
||||||
# mac specific
|
# mac specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+108
-24
@@ -1,39 +1,83 @@
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
|
exclude-functions:
|
||||||
ignoretests: true
|
- fmt.*
|
||||||
|
- (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
|
||||||
|
- (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard # Standard section: captures all standard packages.
|
||||||
|
- default # Default section: contains all imports that could not be matched to another section type.
|
||||||
|
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
|
||||||
|
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
|
||||||
|
# Skip generated files.
|
||||||
|
# Default: true
|
||||||
|
skip-generated: true
|
||||||
|
# Enable custom order of sections.
|
||||||
|
# If `true`, make the section order the same as the order of `sections`.
|
||||||
|
# Default: false
|
||||||
|
custom-order: true
|
||||||
|
exhaustive:
|
||||||
|
ignore-enum-types: reflect.Kind|svc.Cmd
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
- decorder
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- dupword
|
||||||
|
- durationcheck
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- errname
|
||||||
|
- exhaustive
|
||||||
|
- gci
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
|
- gofumpt
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
- importas
|
||||||
- misspell
|
- misspell
|
||||||
- prealloc
|
- prealloc
|
||||||
|
- promlinter
|
||||||
|
- sloglint
|
||||||
|
- sqlclosecheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
- tenv
|
||||||
|
- testableexamples
|
||||||
|
- testifylint
|
||||||
|
- tparallel
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- zerologlint
|
||||||
# these are implicitly disabled:
|
# these are implicitly disabled:
|
||||||
# - asciicheck
|
# - containedctx
|
||||||
|
# - contextcheck
|
||||||
|
# - cyclop
|
||||||
# - depguard
|
# - depguard
|
||||||
# - dogsled
|
# - errchkjson
|
||||||
# - dupl
|
# - errorlint
|
||||||
# - exhaustive
|
# - exhaustruct
|
||||||
# - exportloopref
|
# - execinquery
|
||||||
|
# - exhaustruct
|
||||||
|
# - forbidigo
|
||||||
|
# - forcetypeassert
|
||||||
# - funlen
|
# - funlen
|
||||||
# - gci
|
# - ginkgolinter
|
||||||
|
# - gocheckcompilerdirectives
|
||||||
# - gochecknoglobals
|
# - gochecknoglobals
|
||||||
# - gochecknoinits
|
# - gochecknoinits
|
||||||
|
# - gochecksumtype
|
||||||
# - gocognit
|
# - gocognit
|
||||||
# - goconst
|
# - goconst
|
||||||
# - gocritic
|
# - gocritic
|
||||||
@@ -41,58 +85,98 @@ linters:
|
|||||||
# - godot
|
# - godot
|
||||||
# - godox
|
# - godox
|
||||||
# - goerr113
|
# - goerr113
|
||||||
# - gofumpt
|
|
||||||
# - goheader
|
# - goheader
|
||||||
# - golint
|
|
||||||
# - gomnd
|
# - gomnd
|
||||||
|
# - gomoddirectives
|
||||||
# - gomodguard
|
# - gomodguard
|
||||||
# - goprintffuncname
|
# - goprintffuncname
|
||||||
# - interfacer
|
# - gosmopolitan
|
||||||
|
# - grouper
|
||||||
|
# - inamedparam
|
||||||
|
# - interfacebloat
|
||||||
|
# - ireturn
|
||||||
# - lll
|
# - lll
|
||||||
# - maligned
|
# - loggercheck
|
||||||
|
# - maintidx
|
||||||
|
# - makezero
|
||||||
|
# - mirror
|
||||||
|
# - musttag
|
||||||
# - nakedret
|
# - nakedret
|
||||||
# - nestif
|
# - nestif
|
||||||
|
# - nilerr
|
||||||
|
# - nilnil
|
||||||
# - nlreturn
|
# - nlreturn
|
||||||
# - noctx
|
# - noctx
|
||||||
# - nolintlint
|
# - nolintlint
|
||||||
|
# - nonamedreturns
|
||||||
|
# - nosprintfhostport
|
||||||
|
# - paralleltest
|
||||||
|
# - perfsprint
|
||||||
|
# - predeclared
|
||||||
|
# - protogetter
|
||||||
|
# - reassign
|
||||||
|
# - revive
|
||||||
# - rowserrcheck
|
# - rowserrcheck
|
||||||
# - scopelint
|
|
||||||
# - sqlclosecheck
|
|
||||||
# - stylecheck
|
# - stylecheck
|
||||||
|
# - tagalign
|
||||||
|
# - tagliatelle
|
||||||
# - testpackage
|
# - testpackage
|
||||||
|
# - thelper
|
||||||
# - unparam
|
# - unparam
|
||||||
# - whitespace
|
# - usestdlibvars
|
||||||
|
# - varnamelen
|
||||||
|
# - wrapcheck
|
||||||
# - wsl
|
# - wsl
|
||||||
|
|
||||||
run:
|
run:
|
||||||
# default concurrency is a available CPU number.
|
# default concurrency is a available CPU number.
|
||||||
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
||||||
deadline: 5m
|
timeout: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: false
|
tests: false
|
||||||
|
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
format: 'colored-line-number'
|
formats:
|
||||||
|
- format: 'colored-line-number'
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
print-linter-name: true
|
print-linter-name: true
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
|
- text: 'G115' # TODO: Either we should fix the issues or nuke the linter if it's bad
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
# we aren't calling unknown URL
|
# we aren't calling unknown URL
|
||||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
- text: 'G107' # G107: Url provided to HTTP request as taint input
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
# 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
|
- text: 'G203' # G203: Use of unescaped data in HTML templates
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# we're shelling out to known commands, not relying on user-defined input.
|
# we're shelling out to known commands, not relying on user-defined input.
|
||||||
- text: "G204" # G204: Audit use of command execution
|
- text: 'G204' # G204: Audit use of command execution
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||||
text: "G404" # G404: Insecure random number source (rand)
|
text: 'G404' # G404: Insecure random number source (rand)
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
- path: modules/caddyhttp/reverseproxy/streaming.go
|
||||||
|
text: 'G404' # G404: Insecure random number source (rand)
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
- path: modules/logging/filters.go
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
- path: modules/caddyhttp/matchers.go
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
- path: modules/caddyhttp/vars.go
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
|||||||
+93
-13
@@ -1,27 +1,39 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
|
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
|
||||||
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
|
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
|
||||||
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
|
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
|
||||||
# subsequently causes gorleaser to refuse running.
|
# subsequently causes gorleaser to refuse running.
|
||||||
|
- rm -rf caddy-build caddy-dist vendor
|
||||||
|
# vendor Caddy deps
|
||||||
|
- go mod vendor
|
||||||
- mkdir -p caddy-build
|
- mkdir -p caddy-build
|
||||||
- cp cmd/caddy/main.go caddy-build/main.go
|
- cp cmd/caddy/main.go caddy-build/main.go
|
||||||
- cp ./go.mod caddy-build/go.mod
|
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
||||||
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
|
# prepare syso files for windows embedding
|
||||||
|
- /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
|
||||||
|
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
|
||||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
# 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
|
# 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
|
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
||||||
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
|
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
|
||||||
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
|
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
|
||||||
- /bin/sh -c 'cd ./caddy-build && go mod tidy'
|
- /bin/sh -c 'cd ./caddy-build && go mod tidy'
|
||||||
|
# vendor the deps of the prepared to-build module
|
||||||
|
- /bin/sh -c 'cd ./caddy-build && go mod vendor'
|
||||||
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
|
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
|
||||||
|
- mkdir -p caddy-dist/man
|
||||||
- go mod download
|
- go mod download
|
||||||
|
- go run cmd/caddy/main.go manpage --directory ./caddy-dist/man
|
||||||
|
- gzip -r ./caddy-dist/man/
|
||||||
|
- /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion'
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
main: main.go
|
|
||||||
dir: ./caddy-build
|
dir: ./caddy-build
|
||||||
binary: caddy
|
binary: caddy
|
||||||
goos:
|
goos:
|
||||||
@@ -35,10 +47,11 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
- s390x
|
- s390x
|
||||||
- ppc64le
|
- ppc64le
|
||||||
|
- riscv64
|
||||||
goarm:
|
goarm:
|
||||||
- 5
|
- "5"
|
||||||
- 6
|
- "6"
|
||||||
- 7
|
- "7"
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm
|
goarch: arm
|
||||||
@@ -46,28 +59,93 @@ builds:
|
|||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: s390x
|
goarch: s390x
|
||||||
|
- goos: darwin
|
||||||
|
goarch: riscv64
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: s390x
|
goarch: s390x
|
||||||
|
- goos: windows
|
||||||
|
goarch: riscv64
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: s390x
|
goarch: s390x
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: riscv64
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 5
|
goarm: "5"
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
|
- -mod=readonly
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
tags:
|
||||||
|
- nobadger
|
||||||
|
|
||||||
|
signs:
|
||||||
|
- cmd: cosign
|
||||||
|
signature: "${artifact}.sig"
|
||||||
|
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
|
||||||
|
args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
|
||||||
|
artifacts: all
|
||||||
|
|
||||||
|
sboms:
|
||||||
|
- artifacts: binary
|
||||||
|
documents:
|
||||||
|
- >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- .Version }}_
|
||||||
|
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
|
||||||
|
{{- .Arch }}
|
||||||
|
{{- with .Arm }}v{{ . }}{{ end }}
|
||||||
|
{{- with .Mips }}_{{ . }}{{ end }}
|
||||||
|
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sbom
|
||||||
|
cmd: syft
|
||||||
|
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format_overrides:
|
- id: default
|
||||||
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
replacements:
|
name_template: >-
|
||||||
darwin: mac
|
{{ .ProjectName }}_
|
||||||
|
{{- .Version }}_
|
||||||
|
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
|
||||||
|
{{- .Arch }}
|
||||||
|
{{- with .Arm }}v{{ . }}{{ end }}
|
||||||
|
{{- with .Mips }}_{{ . }}{{ end }}
|
||||||
|
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
|
||||||
|
|
||||||
|
# package the 'caddy-build' directory into a tarball,
|
||||||
|
# allowing users to build the exact same set of files as ours.
|
||||||
|
- id: source
|
||||||
|
meta: true
|
||||||
|
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
|
||||||
|
files:
|
||||||
|
- src: LICENSE
|
||||||
|
dst: ./LICENSE
|
||||||
|
- src: README.md
|
||||||
|
dst: ./README.md
|
||||||
|
- src: AUTHORS
|
||||||
|
dst: ./AUTHORS
|
||||||
|
- src: ./caddy-build
|
||||||
|
dst: ./
|
||||||
|
|
||||||
|
source:
|
||||||
|
enabled: true
|
||||||
|
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
|
||||||
|
format: 'tar.gz'
|
||||||
|
|
||||||
|
# Additional files/template/globs you want to add to the source archive.
|
||||||
|
#
|
||||||
|
# Default: empty.
|
||||||
|
files:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
algorithm: sha512
|
algorithm: sha512
|
||||||
|
|
||||||
@@ -75,7 +153,7 @@ nfpms:
|
|||||||
- id: default
|
- id: default
|
||||||
package_name: caddy
|
package_name: caddy
|
||||||
|
|
||||||
vendor: Light Code Labs
|
vendor: Dyanim
|
||||||
homepage: https://caddyserver.com
|
homepage: https://caddyserver.com
|
||||||
maintainer: Matthew Holt <mholt@users.noreply.github.com>
|
maintainer: Matthew Holt <mholt@users.noreply.github.com>
|
||||||
description: |
|
description: |
|
||||||
@@ -97,19 +175,21 @@ nfpms:
|
|||||||
- src: ./caddy-dist/welcome/index.html
|
- src: ./caddy-dist/welcome/index.html
|
||||||
dst: /usr/share/caddy/index.html
|
dst: /usr/share/caddy/index.html
|
||||||
|
|
||||||
- src: ./caddy-dist/scripts/completions/bash-completion
|
- src: ./caddy-dist/scripts/bash-completion
|
||||||
dst: /etc/bash_completion.d/caddy
|
dst: /etc/bash_completion.d/caddy
|
||||||
|
|
||||||
- src: ./caddy-dist/config/Caddyfile
|
- src: ./caddy-dist/config/Caddyfile
|
||||||
dst: /etc/caddy/Caddyfile
|
dst: /etc/caddy/Caddyfile
|
||||||
type: config
|
type: config
|
||||||
|
|
||||||
|
- src: ./caddy-dist/man/*
|
||||||
|
dst: /usr/share/man/man8/
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: ./caddy-dist/scripts/postinstall.sh
|
postinstall: ./caddy-dist/scripts/postinstall.sh
|
||||||
preremove: ./caddy-dist/scripts/preremove.sh
|
preremove: ./caddy-dist/scripts/preremove.sh
|
||||||
postremove: ./caddy-dist/scripts/postremove.sh
|
postremove: ./caddy-dist/scripts/postremove.sh
|
||||||
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
github:
|
github:
|
||||||
owner: caddyserver
|
owner: caddyserver
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
<p align="center">
|
<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>
|
<a href="https://caddyserver.com">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1128849/210187358-e2c39003-9a5e-4dd5-a783-6deb6483ee72.svg">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg">
|
||||||
|
<img src="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg" alt="Caddy" width="550">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<h3 align="center">a <a href="https://zerossl.com"><img src="https://caddyserver.com/resources/images/zerossl-logo.svg" height="28" valign="middle"></a> project</h3>
|
<h3 align="center">a <a href="https://zerossl.com"><img src="https://user-images.githubusercontent.com/55066419/208327323-2770dc16-ec09-43a0-9035-c5b872c2ad7f.svg" height="28" style="vertical-align: -7.7px" valign="middle"></a> project</h3>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<h3 align="center">Every site on HTTPS</h3>
|
<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">Caddy is an extensible server platform that uses TLS by default.</p>
|
||||||
<p align="center">
|
<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://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
|
||||||
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
||||||
<br>
|
<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://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
|
||||||
@@ -40,11 +46,17 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<b>Powered by</b>
|
<b>Powered by</b>
|
||||||
<br>
|
<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>
|
<a href="https://github.com/caddyserver/certmagic">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
|
||||||
|
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## [Features](https://caddyserver.com/v2)
|
## [Features](https://caddyserver.com/features)
|
||||||
|
|
||||||
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
|
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
|
||||||
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
|
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
|
||||||
@@ -57,25 +69,25 @@
|
|||||||
- Multi-issuer fallback
|
- Multi-issuer fallback
|
||||||
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
||||||
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
|
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
|
||||||
- **Scales to tens of thousands of sites** ... and probably more
|
- **Scales to hundreds of thousands of sites** as proven in production
|
||||||
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
|
- **HTTP/1.1, HTTP/2, and HTTP/3** all supported by default
|
||||||
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
|
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
|
||||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||||
- Written in Go, a language with higher **memory safety guarantees** than other servers
|
- Written in Go, a language with higher **memory safety guarantees** than other servers
|
||||||
- Actually **fun to use**
|
- Actually **fun to use**
|
||||||
- So, so much more to [discover](https://caddyserver.com/v2)
|
- So much more to [discover](https://caddyserver.com/features)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
|
The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
|
||||||
|
|
||||||
For other install options, see https://caddyserver.com/docs/install.
|
See [our online documentation](https://caddyserver.com/docs/install) for other install instructions.
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- [Go 1.16 or newer](https://golang.org/dl/)
|
- [Go 1.22.3 or newer](https://golang.org/dl/)
|
||||||
|
|
||||||
### For development
|
### For development
|
||||||
|
|
||||||
@@ -164,9 +176,9 @@ The docs are also open source. You can contribute to them here: https://github.c
|
|||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
- 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.
|
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
|
||||||
|
|
||||||
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers!
|
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
|
||||||
|
|
||||||
- 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!
|
- 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!
|
||||||
|
|
||||||
@@ -176,6 +188,8 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
|
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
|
||||||
|
|
||||||
**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 Stack Holdings GmbH.
|
**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 Stack Holdings GmbH.
|
||||||
|
|
||||||
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"expvar"
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
@@ -34,17 +34,30 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/notify"
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/cespare/xxhash/v2"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// The hard-coded default `DefaultAdminListen` can be overridden
|
||||||
|
// by setting the `CADDY_ADMIN` environment variable.
|
||||||
|
// The environment variable may be used by packagers to change
|
||||||
|
// the default admin address to something more appropriate for
|
||||||
|
// that platform. See #5317 for discussion.
|
||||||
|
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
|
||||||
|
DefaultAdminListen = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AdminConfig configures Caddy's API endpoint, which is used
|
// AdminConfig configures Caddy's API endpoint, which is used
|
||||||
// to manage Caddy while it is running.
|
// to manage Caddy while it is running.
|
||||||
type AdminConfig struct {
|
type AdminConfig struct {
|
||||||
@@ -56,7 +69,14 @@ type AdminConfig struct {
|
|||||||
|
|
||||||
// The address to which the admin endpoint's listener should
|
// The address to which the admin endpoint's listener should
|
||||||
// bind itself. Can be any single network address that can be
|
// bind itself. Can be any single network address that can be
|
||||||
// parsed by Caddy. Default: localhost:2019
|
// parsed by Caddy. Accepts placeholders.
|
||||||
|
// Default: the value of the `CADDY_ADMIN` environment variable,
|
||||||
|
// or `localhost:2019` otherwise.
|
||||||
|
//
|
||||||
|
// Remember: When changing this value through a config reload,
|
||||||
|
// be sure to use the `--address` CLI flag to specify the current
|
||||||
|
// admin address if the currently-running admin endpoint is not
|
||||||
|
// the default address.
|
||||||
Listen string `json:"listen,omitempty"`
|
Listen string `json:"listen,omitempty"`
|
||||||
|
|
||||||
// If true, CORS headers will be emitted, and requests to the
|
// If true, CORS headers will be emitted, and requests to the
|
||||||
@@ -92,6 +112,10 @@ type AdminConfig struct {
|
|||||||
//
|
//
|
||||||
// EXPERIMENTAL: This feature is subject to change.
|
// EXPERIMENTAL: This feature is subject to change.
|
||||||
Remote *RemoteAdmin `json:"remote,omitempty"`
|
Remote *RemoteAdmin `json:"remote,omitempty"`
|
||||||
|
|
||||||
|
// Holds onto the routers so that we can later provision them
|
||||||
|
// if they require provisioning.
|
||||||
|
routers []AdminRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigSettings configures the management of configuration.
|
// ConfigSettings configures the management of configuration.
|
||||||
@@ -101,20 +125,26 @@ type ConfigSettings struct {
|
|||||||
// are not persisted; only configs that are pushed to Caddy get persisted.
|
// are not persisted; only configs that are pushed to Caddy get persisted.
|
||||||
Persist *bool `json:"persist,omitempty"`
|
Persist *bool `json:"persist,omitempty"`
|
||||||
|
|
||||||
// Loads a configuration to use. This is helpful if your configs are
|
// Loads a new configuration. This is helpful if your configs are
|
||||||
// managed elsewhere, and you want Caddy to pull its config dynamically
|
// managed elsewhere and you want Caddy to pull its config dynamically
|
||||||
// when it starts. The pulled config completely replaces the current
|
// when it starts. The pulled config completely replaces the current
|
||||||
// one, just like any other config load. It is an error if a pulled
|
// one, just like any other config load. It is an error if a pulled
|
||||||
// config is configured to pull another config.
|
// config is configured to pull another config without a load_delay,
|
||||||
|
// as this creates a tight loop.
|
||||||
//
|
//
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
|
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
|
||||||
|
|
||||||
// The interval to pull config. With a non-zero value, will pull config
|
// The duration after which to load config. If set, config will be pulled
|
||||||
// from config loader (eg. a http loader) with given interval.
|
// from the config loader after this duration. A delay is required if a
|
||||||
|
// dynamically-loaded config is configured to load yet another config. To
|
||||||
|
// load configs on a regular interval, ensure this value is set the same
|
||||||
|
// on all loaded configs; it can also be variable if needed, and to stop
|
||||||
|
// the loop, simply remove dynamic config loading from the next-loaded
|
||||||
|
// config.
|
||||||
//
|
//
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
LoadInterval Duration `json:"load_interval,omitempty"`
|
LoadDelay Duration `json:"load_delay,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdentityConfig configures management of this server's identity. An identity
|
// IdentityConfig configures management of this server's identity. An identity
|
||||||
@@ -145,7 +175,7 @@ type IdentityConfig struct {
|
|||||||
//
|
//
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
type RemoteAdmin struct {
|
type RemoteAdmin struct {
|
||||||
// The address on which to start the secure listener.
|
// The address on which to start the secure listener. Accepts placeholders.
|
||||||
// Default: :2021
|
// Default: :2021
|
||||||
Listen string `json:"listen,omitempty"`
|
Listen string `json:"listen,omitempty"`
|
||||||
|
|
||||||
@@ -184,7 +214,7 @@ type AdminPermissions struct {
|
|||||||
|
|
||||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||||
func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
|
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) adminHandler {
|
||||||
muxWrap := adminHandler{mux: http.NewServeMux()}
|
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||||
|
|
||||||
// secure the local or remote endpoint respectively
|
// secure the local or remote endpoint respectively
|
||||||
@@ -193,6 +223,7 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
|
|||||||
} else {
|
} else {
|
||||||
muxWrap.enforceHost = !addr.isWildcardInterface()
|
muxWrap.enforceHost = !addr.isWildcardInterface()
|
||||||
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
||||||
|
muxWrap.enforceOrigin = admin.EnforceOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
|
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
|
||||||
@@ -239,46 +270,107 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
|
|||||||
// register third-party module endpoints
|
// register third-party module endpoints
|
||||||
for _, m := range GetModules("admin.api") {
|
for _, m := range GetModules("admin.api") {
|
||||||
router := m.New().(AdminRouter)
|
router := m.New().(AdminRouter)
|
||||||
handlerLabel := m.ID.Name()
|
|
||||||
for _, route := range router.Routes() {
|
for _, route := range router.Routes() {
|
||||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||||
}
|
}
|
||||||
|
admin.routers = append(admin.routers, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
return muxWrap
|
return muxWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// provisionAdminRouters provisions all the router modules
|
||||||
|
// in the admin.api namespace that need provisioning.
|
||||||
|
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
|
||||||
|
for _, router := range admin.routers {
|
||||||
|
provisioner, ok := router.(Provisioner)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := provisioner.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer need the routers once provisioned, allow for GC
|
||||||
|
admin.routers = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// allowedOrigins returns a list of origins that are allowed.
|
// allowedOrigins returns a list of origins that are allowed.
|
||||||
// If admin.Origins is nil (null), the provided listen address
|
// If admin.Origins is nil (null), the provided listen address
|
||||||
// will be used as the default origin. If admin.Origins is
|
// will be used as the default origin. If admin.Origins is
|
||||||
// empty, no origins will be allowed, effectively bricking the
|
// empty, no origins will be allowed, effectively bricking the
|
||||||
// endpoint for non-unix-socket endpoints, but whatever.
|
// endpoint for non-unix-socket endpoints, but whatever.
|
||||||
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
|
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
||||||
uniqueOrigins := make(map[string]struct{})
|
uniqueOrigins := make(map[string]struct{})
|
||||||
for _, o := range admin.Origins {
|
for _, o := range admin.Origins {
|
||||||
uniqueOrigins[o] = struct{}{}
|
uniqueOrigins[o] = struct{}{}
|
||||||
}
|
}
|
||||||
if admin.Origins == nil {
|
if admin.Origins == nil {
|
||||||
if addr.isLoopback() {
|
if addr.isLoopback() {
|
||||||
if addr.IsUnixNetwork() {
|
if addr.IsUnixNetwork() || addr.IsFdNetwork() {
|
||||||
// RFC 2616, Section 14.26:
|
// RFC 2616, Section 14.26:
|
||||||
// "A client MUST include a Host header field in all HTTP/1.1 request
|
// "A client MUST include a Host header field in all HTTP/1.1 request
|
||||||
// messages. If the requested URI does not include an Internet host
|
// messages. If the requested URI does not include an Internet host
|
||||||
// name for the service being requested, then the Host header field MUST
|
// name for the service being requested, then the Host header field MUST
|
||||||
// be given with an empty value."
|
// be given with an empty value."
|
||||||
|
//
|
||||||
|
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
|
||||||
|
// Understandable, but frustrating. See:
|
||||||
|
// https://github.com/golang/go/issues/60374
|
||||||
|
// See also the discussion here:
|
||||||
|
// https://github.com/golang/go/issues/61431
|
||||||
|
//
|
||||||
|
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
|
||||||
|
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
|
||||||
|
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
|
||||||
|
// security checks, the infosec community assures me that it is secure to do
|
||||||
|
// so, because:
|
||||||
|
// 1) Browsers do not allow access to unix sockets
|
||||||
|
// 2) DNS is irrelevant to unix sockets
|
||||||
|
//
|
||||||
|
// I am not quite ready to trust either of those external factors, so instead
|
||||||
|
// of disabling Host/Origin checks, we now allow specific Host values when
|
||||||
|
// accessing the admin endpoint over unix sockets. I definitely don't trust
|
||||||
|
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
|
||||||
|
// and IP shouldn't even be used, but if it is for some reason, I think we can
|
||||||
|
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
|
||||||
|
// machine, meaning that a hypothetical browser origin would have to be on the
|
||||||
|
// local machine as well.
|
||||||
uniqueOrigins[""] = struct{}{}
|
uniqueOrigins[""] = struct{}{}
|
||||||
|
uniqueOrigins["127.0.0.1"] = struct{}{}
|
||||||
|
uniqueOrigins["::1"] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
||||||
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
||||||
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !addr.IsUnixNetwork() {
|
if !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
|
||||||
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
|
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allowed := make([]string, 0, len(uniqueOrigins))
|
allowed := make([]*url.URL, 0, len(uniqueOrigins))
|
||||||
for origin := range uniqueOrigins {
|
for originStr := range uniqueOrigins {
|
||||||
|
var origin *url.URL
|
||||||
|
if strings.Contains(originStr, "://") {
|
||||||
|
var err error
|
||||||
|
origin, err = url.Parse(originStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
origin.Path = ""
|
||||||
|
origin.RawPath = ""
|
||||||
|
origin.Fragment = ""
|
||||||
|
origin.RawFragment = ""
|
||||||
|
origin.RawQuery = ""
|
||||||
|
} else {
|
||||||
|
origin = &url.URL{Host: originStr}
|
||||||
|
}
|
||||||
allowed = append(allowed, origin)
|
allowed = append(allowed, origin)
|
||||||
}
|
}
|
||||||
return allowed
|
return allowed
|
||||||
@@ -289,18 +381,22 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
|
|||||||
// for the admin endpoint exists in cfg, a default one is used, so
|
// for the admin endpoint exists in cfg, a default one is used, so
|
||||||
// that there is always an admin server (unless it is explicitly
|
// that there is always an admin server (unless it is explicitly
|
||||||
// configured to be disabled).
|
// configured to be disabled).
|
||||||
func replaceLocalAdminServer(cfg *Config) error {
|
// Critically note that some elements and functionality of the context
|
||||||
// always be sure to close down the old admin endpoint
|
// may not be ready, e.g. storage. Tread carefully.
|
||||||
|
func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
||||||
|
// always* be sure to close down the old admin endpoint
|
||||||
// as gracefully as possible, even if the new one is
|
// as gracefully as possible, even if the new one is
|
||||||
// disabled -- careful to use reference to the current
|
// disabled -- careful to use reference to the current
|
||||||
// (old) admin endpoint since it will be different
|
// (old) admin endpoint since it will be different
|
||||||
// when the function returns
|
// when the function returns
|
||||||
|
// (* except if the new one fails to start)
|
||||||
oldAdminServer := localAdminServer
|
oldAdminServer := localAdminServer
|
||||||
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
// do the shutdown asynchronously so that any
|
// do the shutdown asynchronously so that any
|
||||||
// current API request gets a response; this
|
// current API request gets a response; this
|
||||||
// goroutine may last a few seconds
|
// goroutine may last a few seconds
|
||||||
if oldAdminServer != nil {
|
if oldAdminServer != nil && err == nil {
|
||||||
go func(oldAdminServer *http.Server) {
|
go func(oldAdminServer *http.Server) {
|
||||||
err := stopAdminServer(oldAdminServer)
|
err := stopAdminServer(oldAdminServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,27 +406,28 @@ func replaceLocalAdminServer(cfg *Config) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// always get a valid admin config
|
// set a default if admin wasn't otherwise configured
|
||||||
adminConfig := DefaultAdminConfig
|
if cfg.Admin == nil {
|
||||||
if cfg != nil && cfg.Admin != nil {
|
cfg.Admin = &AdminConfig{
|
||||||
adminConfig = cfg.Admin
|
Listen: DefaultAdminListen,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if new admin endpoint is to be disabled, we're done
|
// if new admin endpoint is to be disabled, we're done
|
||||||
if adminConfig.Disabled {
|
if cfg.Admin.Disabled {
|
||||||
Log().Named("admin").Warn("admin endpoint disabled")
|
Log().Named("admin").Warn("admin endpoint disabled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract a singular listener address
|
// extract a singular listener address
|
||||||
addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
|
addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := adminConfig.newAdminHandler(addr, false)
|
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||||
|
|
||||||
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -351,15 +448,15 @@ func replaceLocalAdminServer(cfg *Config) error {
|
|||||||
serverMu.Lock()
|
serverMu.Lock()
|
||||||
server := localAdminServer
|
server := localAdminServer
|
||||||
serverMu.Unlock()
|
serverMu.Unlock()
|
||||||
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
|
||||||
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
|
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
adminLogger.Info("admin endpoint started",
|
adminLogger.Info("admin endpoint started",
|
||||||
zap.String("address", addr.String()),
|
zap.String("address", addr.String()),
|
||||||
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
|
||||||
zap.Strings("origins", handler.allowedOrigins))
|
zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
|
||||||
|
|
||||||
if !handler.enforceHost {
|
if !handler.enforceHost {
|
||||||
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
|
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
|
||||||
@@ -379,7 +476,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
|
|||||||
// import the caddytls package -- but it works
|
// import the caddytls package -- but it works
|
||||||
if cfg.Admin.Identity.IssuersRaw == nil {
|
if cfg.Admin.Identity.IssuersRaw == nil {
|
||||||
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
|
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
|
||||||
json.RawMessage(`{"module": "zerossl"}`),
|
|
||||||
json.RawMessage(`{"module": "acme"}`),
|
json.RawMessage(`{"module": "acme"}`),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +486,7 @@ func manageIdentity(ctx Context, cfg *Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading identity issuer modules: %s", err)
|
return fmt.Errorf("loading identity issuer modules: %s", err)
|
||||||
}
|
}
|
||||||
for _, issVal := range val.([]interface{}) {
|
for _, issVal := range val.([]any) {
|
||||||
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
|
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,7 +546,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
|
|
||||||
// make the HTTP handler but disable Host/Origin enforcement
|
// make the HTTP handler but disable Host/Origin enforcement
|
||||||
// because we are using TLS authentication instead
|
// because we are using TLS authentication instead
|
||||||
handler := cfg.Admin.newAdminHandler(addr, true)
|
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||||
|
|
||||||
// create client certificate pool for TLS mutual auth, and extract public keys
|
// create client certificate pool for TLS mutual auth, and extract public keys
|
||||||
// so that we can enforce access controls at the application layer
|
// so that we can enforce access controls at the application layer
|
||||||
@@ -467,6 +563,9 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create TLS config that will enforce mutual authentication
|
// create TLS config that will enforce mutual authentication
|
||||||
|
if identityCertCache == nil {
|
||||||
|
return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
|
||||||
|
}
|
||||||
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
|
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
|
||||||
tlsConfig := cmCfg.TLSConfig()
|
tlsConfig := cmCfg.TLSConfig()
|
||||||
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
|
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
|
||||||
@@ -494,10 +593,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
serverMu.Unlock()
|
serverMu.Unlock()
|
||||||
|
|
||||||
// start listener
|
// start listener
|
||||||
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ln := lnAny.(net.Listener)
|
||||||
ln = tls.NewListener(ln, tlsConfig)
|
ln = tls.NewListener(ln, tlsConfig)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -516,12 +616,13 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
|
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
|
||||||
|
var cmCfg *certmagic.Config
|
||||||
if ident == nil {
|
if ident == nil {
|
||||||
// user might not have configured identity; that's OK, we can still make a
|
// user might not have configured identity; that's OK, we can still make a
|
||||||
// certmagic config, although it'll be mostly useless for remote management
|
// certmagic config, although it'll be mostly useless for remote management
|
||||||
ident = new(IdentityConfig)
|
ident = new(IdentityConfig)
|
||||||
}
|
}
|
||||||
cmCfg := &certmagic.Config{
|
template := certmagic.Config{
|
||||||
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
|
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Issuers: ident.issuers,
|
Issuers: ident.issuers,
|
||||||
@@ -531,9 +632,11 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
|
|||||||
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
|
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
return cmCfg, nil
|
return cmCfg, nil
|
||||||
},
|
},
|
||||||
|
Logger: logger.Named("cache"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return certmagic.New(identityCertCache, *cmCfg)
|
cmCfg = certmagic.New(identityCertCache, template)
|
||||||
|
return cmCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdentityCredentials returns this instance's configured, managed identity credentials
|
// IdentityCredentials returns this instance's configured, managed identity credentials
|
||||||
@@ -574,13 +677,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
// key recognized; make sure its HTTP request is permitted
|
// key recognized; make sure its HTTP request is permitted
|
||||||
for _, accessPerm := range adminAccess.Permissions {
|
for _, accessPerm := range adminAccess.Permissions {
|
||||||
// verify method
|
// verify method
|
||||||
methodFound := accessPerm.Methods == nil
|
methodFound := accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method)
|
||||||
for _, method := range accessPerm.Methods {
|
|
||||||
if method == r.Method {
|
|
||||||
methodFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !methodFound {
|
if !methodFound {
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
@@ -648,10 +745,10 @@ type AdminRoute struct {
|
|||||||
type adminHandler struct {
|
type adminHandler struct {
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
|
|
||||||
// security for local/plaintext) endpoint, on by default
|
// security for local/plaintext endpoint
|
||||||
enforceOrigin bool
|
enforceOrigin bool
|
||||||
enforceHost bool
|
enforceHost bool
|
||||||
allowedOrigins []string
|
allowedOrigins []*url.URL
|
||||||
|
|
||||||
// security for remote/encrypted endpoint
|
// security for remote/encrypted endpoint
|
||||||
remoteControl *RemoteAdmin
|
remoteControl *RemoteAdmin
|
||||||
@@ -660,11 +757,17 @@ type adminHandler struct {
|
|||||||
// ServeHTTP is the external entry point for API requests.
|
// ServeHTTP is the external entry point for API requests.
|
||||||
// It will only be called once per request.
|
// It will only be called once per request.
|
||||||
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ip, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
ip = r.RemoteAddr
|
||||||
|
port = ""
|
||||||
|
}
|
||||||
log := Log().Named("admin.api").With(
|
log := Log().Named("admin.api").With(
|
||||||
zap.String("method", r.Method),
|
zap.String("method", r.Method),
|
||||||
zap.String("host", r.Host),
|
zap.String("host", r.Host),
|
||||||
zap.String("uri", r.RequestURI),
|
zap.String("uri", r.RequestURI),
|
||||||
zap.String("remote_addr", r.RemoteAddr),
|
zap.String("remote_ip", ip),
|
||||||
|
zap.String("remote_port", port),
|
||||||
zap.Reflect("headers", r.Header),
|
zap.Reflect("headers", r.Header),
|
||||||
)
|
)
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
@@ -770,13 +873,9 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
|||||||
// a trustworthy/expected value. This helps to mitigate DNS
|
// a trustworthy/expected value. This helps to mitigate DNS
|
||||||
// rebinding attacks.
|
// rebinding attacks.
|
||||||
func (h adminHandler) checkHost(r *http.Request) error {
|
func (h adminHandler) checkHost(r *http.Request) error {
|
||||||
var allowed bool
|
allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
|
||||||
for _, allowedHost := range h.allowedOrigins {
|
return r.Host == u.Host
|
||||||
if r.Host == allowedHost {
|
})
|
||||||
allowed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
@@ -791,59 +890,96 @@ func (h adminHandler) checkHost(r *http.Request) error {
|
|||||||
// sites from issuing requests to our listener. It
|
// sites from issuing requests to our listener. It
|
||||||
// returns the origin that was obtained from r.
|
// returns the origin that was obtained from r.
|
||||||
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
||||||
origin := h.getOriginHost(r)
|
originStr, origin := h.getOrigin(r)
|
||||||
if origin == "" {
|
if origin == nil {
|
||||||
return origin, APIError{
|
return "", APIError{
|
||||||
HTTPStatus: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
Err: fmt.Errorf("missing required Origin header"),
|
Err: fmt.Errorf("required Origin header is missing or invalid"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !h.originAllowed(origin) {
|
if !h.originAllowed(origin) {
|
||||||
return origin, APIError{
|
return "", APIError{
|
||||||
HTTPStatus: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return origin, nil
|
return origin.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h adminHandler) getOriginHost(r *http.Request) string {
|
func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
origin = r.Header.Get("Referer")
|
origin = r.Header.Get("Referer")
|
||||||
}
|
}
|
||||||
originURL, err := url.Parse(origin)
|
originURL, err := url.Parse(origin)
|
||||||
if err == nil && originURL.Host != "" {
|
if err != nil {
|
||||||
origin = originURL.Host
|
return origin, nil
|
||||||
}
|
}
|
||||||
return origin
|
originURL.Path = ""
|
||||||
|
originURL.RawPath = ""
|
||||||
|
originURL.Fragment = ""
|
||||||
|
originURL.RawFragment = ""
|
||||||
|
originURL.RawQuery = ""
|
||||||
|
return origin, originURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h adminHandler) originAllowed(origin string) bool {
|
func (h adminHandler) originAllowed(origin *url.URL) bool {
|
||||||
for _, allowedOrigin := range h.allowedOrigins {
|
for _, allowedOrigin := range h.allowedOrigins {
|
||||||
originCopy := origin
|
if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
|
||||||
if !strings.Contains(allowedOrigin, "://") {
|
continue
|
||||||
// no scheme specified, so allow both
|
|
||||||
originCopy = strings.TrimPrefix(originCopy, "http://")
|
|
||||||
originCopy = strings.TrimPrefix(originCopy, "https://")
|
|
||||||
}
|
}
|
||||||
if originCopy == allowedOrigin {
|
if origin.Host == allowedOrigin.Host {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// etagHasher returns a the hasher we used on the config to both
|
||||||
|
// produce and verify ETags.
|
||||||
|
func etagHasher() hash.Hash { return xxhash.New() }
|
||||||
|
|
||||||
|
// makeEtag returns an Etag header value (including quotes) for
|
||||||
|
// the given config path and hash of contents at that path.
|
||||||
|
func makeEtag(path string, hash hash.Hash) string {
|
||||||
|
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This buffer pool is used to keep buffers for
|
||||||
|
// reading the config file during eTag header generation
|
||||||
|
var bufferPool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
hash := etagHasher()
|
||||||
|
|
||||||
err := readConfig(r.URL.Path, w)
|
// Read the config into a buffer instead of writing directly to
|
||||||
|
// the response writer, as we want to set the ETag as the header,
|
||||||
|
// not the trailer.
|
||||||
|
buf := bufferPool.Get().(*bytes.Buffer)
|
||||||
|
buf.Reset()
|
||||||
|
defer bufferPool.Put(buf)
|
||||||
|
|
||||||
|
configWriter := io.MultiWriter(buf, hash)
|
||||||
|
err := readConfig(r.URL.Path, configWriter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
|
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we could consider setting up a sync.Pool for the summed
|
||||||
|
// hashes to reduce GC pressure.
|
||||||
|
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
|
||||||
|
_, err = w.Write(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return APIError{HTTPStatus: http.StatusInternalServerError, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case http.MethodPost,
|
case http.MethodPost,
|
||||||
@@ -877,8 +1013,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||||
|
|
||||||
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
|
err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, errSameConfig) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,19 +1033,28 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
parts := strings.Split(idPath, "/")
|
parts := strings.Split(idPath, "/")
|
||||||
if len(parts) < 3 || parts[2] == "" {
|
if len(parts) < 3 || parts[2] == "" {
|
||||||
return fmt.Errorf("request path is missing object ID")
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("request path is missing object ID"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if parts[0] != "" || parts[1] != "id" {
|
if parts[0] != "" || parts[1] != "id" {
|
||||||
return fmt.Errorf("malformed object path")
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("malformed object path"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
id := parts[2]
|
id := parts[2]
|
||||||
|
|
||||||
// map the ID to the expanded path
|
// map the ID to the expanded path
|
||||||
currentCfgMu.RLock()
|
rawCfgMu.RLock()
|
||||||
expanded, ok := rawCfgIndex[id]
|
expanded, ok := rawCfgIndex[id]
|
||||||
defer currentCfgMu.RUnlock()
|
rawCfgMu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unknown object ID '%s'", id)
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusNotFound,
|
||||||
|
Err: fmt.Errorf("unknown object ID '%s'", id),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// piece the full URL path back together
|
// piece the full URL path back together
|
||||||
@@ -927,11 +1072,7 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := notify.NotifyStopping(); err != nil {
|
exitProcess(context.Background(), Log().Named("admin.api"))
|
||||||
Log().Error("unable to notify stopping to service manager", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
exitProcess(Log().Named("admin.api"))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,11 +1080,11 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
|||||||
// the operation at path according to method, using body and out as
|
// the operation at path according to method, using body and out as
|
||||||
// needed. This is a low-level, unsynchronized function; most callers
|
// needed. This is a low-level, unsynchronized function; most callers
|
||||||
// will want to use changeConfig or readConfig instead. This requires a
|
// will want to use changeConfig or readConfig instead. This requires a
|
||||||
// read or write lock on currentCfgMu, depending on method (GET needs
|
// read or write lock on currentCtxMu, depending on method (GET needs
|
||||||
// only a read lock; all others need a write lock).
|
// only a read lock; all others need a write lock).
|
||||||
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
||||||
var err error
|
var err error
|
||||||
var val interface{}
|
var val any
|
||||||
|
|
||||||
// if there is a request body, decode it into the
|
// if there is a request body, decode it into the
|
||||||
// variable that will be set in the config according
|
// variable that will be set in the config according
|
||||||
@@ -980,16 +1121,16 @@ func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error
|
|||||||
parts = parts[:len(parts)-1]
|
parts = parts[:len(parts)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
var ptr interface{} = rawCfg
|
var ptr any = rawCfg
|
||||||
|
|
||||||
traverseLoop:
|
traverseLoop:
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
switch v := ptr.(type) {
|
switch v := ptr.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
// if the next part enters a slice, and the slice is our destination,
|
// if the next part enters a slice, and the slice is our destination,
|
||||||
// handle it specially (because appending to the slice copies the slice
|
// handle it specially (because appending to the slice copies the slice
|
||||||
// header, which does not replace the original one like we want)
|
// header, which does not replace the original one like we want)
|
||||||
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
|
if arr, ok := v[part].([]any); ok && i == len(parts)-2 {
|
||||||
var idx int
|
var idx int
|
||||||
if method != http.MethodPost {
|
if method != http.MethodPost {
|
||||||
idxStr := parts[len(parts)-1]
|
idxStr := parts[len(parts)-1]
|
||||||
@@ -1011,7 +1152,7 @@ traverseLoop:
|
|||||||
}
|
}
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
if ellipses {
|
if ellipses {
|
||||||
valArray, ok := val.([]interface{})
|
valArray, ok := val.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("final element is not an array")
|
return fmt.Errorf("final element is not an array")
|
||||||
}
|
}
|
||||||
@@ -1046,9 +1187,9 @@ traverseLoop:
|
|||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
// if the part is an existing list, POST appends to
|
// if the part is an existing list, POST appends to
|
||||||
// it, otherwise it just sets or creates the value
|
// it, otherwise it just sets or creates the value
|
||||||
if arr, ok := v[part].([]interface{}); ok {
|
if arr, ok := v[part].([]any); ok {
|
||||||
if ellipses {
|
if ellipses {
|
||||||
valArray, ok := val.([]interface{})
|
valArray, ok := val.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("final element is not an array")
|
return fmt.Errorf("final element is not an array")
|
||||||
}
|
}
|
||||||
@@ -1061,15 +1202,27 @@ traverseLoop:
|
|||||||
}
|
}
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
if _, ok := v[part]; ok {
|
if _, ok := v[part]; ok {
|
||||||
return fmt.Errorf("[%s] key already exists: %s", path, part)
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusConflict,
|
||||||
|
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
v[part] = val
|
v[part] = val
|
||||||
case http.MethodPatch:
|
case http.MethodPatch:
|
||||||
if _, ok := v[part]; !ok {
|
if _, ok := v[part]; !ok {
|
||||||
return fmt.Errorf("[%s] key does not exist: %s", path, part)
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusNotFound,
|
||||||
|
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
v[part] = val
|
v[part] = val
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
|
if _, ok := v[part]; !ok {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusNotFound,
|
||||||
|
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(v, part)
|
delete(v, part)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unrecognized method %s", method)
|
return fmt.Errorf("unrecognized method %s", method)
|
||||||
@@ -1079,12 +1232,12 @@ traverseLoop:
|
|||||||
// might not exist yet; that's OK but we need to make them as
|
// 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
|
// we go, while we still have a pointer from the level above
|
||||||
if v[part] == nil && method == http.MethodPut {
|
if v[part] == nil && method == http.MethodPut {
|
||||||
v[part] = make(map[string]interface{})
|
v[part] = make(map[string]any)
|
||||||
}
|
}
|
||||||
ptr = v[part]
|
ptr = v[part]
|
||||||
}
|
}
|
||||||
|
|
||||||
case []interface{}:
|
case []any:
|
||||||
partInt, err := strconv.Atoi(part)
|
partInt, err := strconv.Atoi(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||||
@@ -1106,7 +1259,7 @@ traverseLoop:
|
|||||||
|
|
||||||
// RemoveMetaFields removes meta fields like "@id" from a JSON message
|
// RemoveMetaFields removes meta fields like "@id" from a JSON message
|
||||||
// by using a simple regular expression. (An alternate way to do this
|
// by using a simple regular expression. (An alternate way to do this
|
||||||
// would be to delete them from the raw, map[string]interface{}
|
// would be to delete them from the raw, map[string]any
|
||||||
// representation as they are indexed, then iterate the index we made
|
// representation as they are indexed, then iterate the index we made
|
||||||
// and add them back after encoding as JSON, but this is simpler.)
|
// and add them back after encoding as JSON, but this is simpler.)
|
||||||
func RemoveMetaFields(rawJSON []byte) []byte {
|
func RemoveMetaFields(rawJSON []byte) []byte {
|
||||||
@@ -1158,7 +1311,10 @@ func (e APIError) Error() string {
|
|||||||
// parseAdminListenAddr extracts a singular listen address from either addr
|
// parseAdminListenAddr extracts a singular listen address from either addr
|
||||||
// or defaultAddr, returning the network and the address of the listener.
|
// or defaultAddr, returning the network and the address of the listener.
|
||||||
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
|
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
|
||||||
input := addr
|
input, err := NewReplacer().ReplaceOrErr(addr, true, true)
|
||||||
|
if err != nil {
|
||||||
|
return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
|
||||||
|
}
|
||||||
if input == "" {
|
if input == "" {
|
||||||
input = defaultAddr
|
input = defaultAddr
|
||||||
}
|
}
|
||||||
@@ -1181,6 +1337,18 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
|
|||||||
return x509.ParseCertificate(derBytes)
|
return x509.ParseCertificate(derBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type loggableURLArray []*url.URL
|
||||||
|
|
||||||
|
func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
|
||||||
|
if ua == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, u := range ua {
|
||||||
|
enc.AppendString(u.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultAdminListen is the address for the local admin
|
// DefaultAdminListen is the address for the local admin
|
||||||
// listener, if none is specified at startup.
|
// listener, if none is specified at startup.
|
||||||
@@ -1190,19 +1358,13 @@ var (
|
|||||||
// (TLS-authenticated) admin listener, if enabled and not
|
// (TLS-authenticated) admin listener, if enabled and not
|
||||||
// specified otherwise.
|
// specified otherwise.
|
||||||
DefaultRemoteAdminListen = ":2021"
|
DefaultRemoteAdminListen = ":2021"
|
||||||
|
|
||||||
// DefaultAdminConfig is the default configuration
|
|
||||||
// for the local administration endpoint.
|
|
||||||
DefaultAdminConfig = &AdminConfig{
|
|
||||||
Listen: DefaultAdminListen,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PIDFile writes a pidfile to the file at filename. It
|
// PIDFile writes a pidfile to the file at filename. It
|
||||||
// will get deleted before the process gracefully exits.
|
// will get deleted before the process gracefully exits.
|
||||||
func PIDFile(filename string) error {
|
func PIDFile(filename string) error {
|
||||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||||
err := ioutil.WriteFile(filename, pid, 0600)
|
err := os.WriteFile(filename, pid, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1232,7 +1394,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var bufPool = sync.Pool{
|
var bufPool = sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() any {
|
||||||
return new(bytes.Buffer)
|
return new(bytes.Buffer)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-2
@@ -16,6 +16,8 @@ package caddy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -73,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
|||||||
path: "/bar/qq",
|
path: "/bar/qq",
|
||||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/bar/qq",
|
||||||
|
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/list",
|
path: "/list",
|
||||||
@@ -113,7 +121,7 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// decode the expected config so we can do a convenient DeepEqual
|
// decode the expected config so we can do a convenient DeepEqual
|
||||||
var expectedDecoded interface{}
|
var expectedDecoded any
|
||||||
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
|
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
|
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
|
||||||
@@ -139,10 +147,57 @@ func TestLoadConcurrent(t *testing.T) {
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fooModule struct {
|
||||||
|
IntField int
|
||||||
|
StrField string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fooModule) CaddyModule() ModuleInfo {
|
||||||
|
return ModuleInfo{
|
||||||
|
ID: "foo",
|
||||||
|
New: func() Module { return new(fooModule) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (fooModule) Start() error { return nil }
|
||||||
|
func (fooModule) Stop() error { return nil }
|
||||||
|
|
||||||
|
func TestETags(t *testing.T) {
|
||||||
|
RegisterModule(fooModule{})
|
||||||
|
|
||||||
|
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
|
||||||
|
t.Fatalf("loading: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = "/" + rawConfigKey + "/apps/foo"
|
||||||
|
|
||||||
|
// try update the config with the wrong etag
|
||||||
|
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
|
||||||
|
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
|
||||||
|
t.Fatalf("expected precondition failed; got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the etag
|
||||||
|
hash := etagHasher()
|
||||||
|
if err := readConfig(key, hash); err != nil {
|
||||||
|
t.Fatalf("reading: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the same update with the correct key
|
||||||
|
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected update to work; got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now try another update. The hash should no longer match and we should get precondition failed
|
||||||
|
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
|
||||||
|
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
|
||||||
|
t.Fatalf("expected precondition failed; got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkLoad(b *testing.B) {
|
func BenchmarkLoad(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
Load(testCfg, true)
|
Load(testCfg, true)
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -30,12 +32,15 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/notify"
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
||||||
|
"github.com/caddyserver/caddy/v2/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the top (or beginning) of the Caddy configuration structure.
|
// Config is the top (or beginning) of the Caddy configuration structure.
|
||||||
@@ -80,6 +85,9 @@ type Config struct {
|
|||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
|
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
|
|
||||||
|
// filesystems is a dict of filesystems that will later be loaded from and added to.
|
||||||
|
filesystems FileSystems
|
||||||
}
|
}
|
||||||
|
|
||||||
// App is a thing that Caddy runs.
|
// App is a thing that Caddy runs.
|
||||||
@@ -101,26 +109,50 @@ func Run(cfg *Config) error {
|
|||||||
// if it is different from the current config or
|
// if it is different from the current config or
|
||||||
// forceReload is true.
|
// forceReload is true.
|
||||||
func Load(cfgJSON []byte, forceReload bool) error {
|
func Load(cfgJSON []byte, forceReload bool) error {
|
||||||
if err := notify.NotifyReloading(); err != nil {
|
if err := notify.Reloading(); err != nil {
|
||||||
Log().Error("unable to notify reloading to service manager", zap.Error(err))
|
Log().Error("unable to notify service manager of reloading state", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// after reload, notify system of success or, if
|
||||||
|
// failure, update with status (error message)
|
||||||
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := notify.NotifyReadiness(); err != nil {
|
if err != nil {
|
||||||
Log().Error("unable to notify readiness to service manager", zap.Error(err))
|
if notifyErr := notify.Error(err, 0); notifyErr != nil {
|
||||||
|
Log().Error("unable to notify to service manager of reload error",
|
||||||
|
zap.Error(notifyErr),
|
||||||
|
zap.String("reload_err", err.Error()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := notify.Ready(); err != nil {
|
||||||
|
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
err = changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
|
||||||
|
if errors.Is(err, errSameConfig) {
|
||||||
|
err = nil // not really an error
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeConfig changes the current config (rawCfg) according to the
|
// changeConfig changes the current config (rawCfg) according to the
|
||||||
// method, traversed via the given path, and uses the given input as
|
// 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).
|
// 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
|
// If the resulting config is the same as the previous, no reload will
|
||||||
// occur unless forceReload is true. This function is safe for
|
// occur unless forceReload is true. If the config is unchanged and not
|
||||||
|
// forcefully reloaded, then errConfigUnchanged This function is safe for
|
||||||
// concurrent use.
|
// concurrent use.
|
||||||
func changeConfig(method, path string, input []byte, forceReload bool) error {
|
// The ifMatchHeader can optionally be given a string of the format:
|
||||||
|
//
|
||||||
|
// "<path> <hash>"
|
||||||
|
//
|
||||||
|
// where <path> is the absolute path in the config and <hash> is the expected hash of
|
||||||
|
// the config at that path. If the hash in the ifMatchHeader doesn't match
|
||||||
|
// the hash of the config, then an APIError with status 412 will be returned.
|
||||||
|
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
|
||||||
switch method {
|
switch method {
|
||||||
case http.MethodGet,
|
case http.MethodGet,
|
||||||
http.MethodHead,
|
http.MethodHead,
|
||||||
@@ -130,8 +162,42 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
|||||||
return fmt.Errorf("method not allowed")
|
return fmt.Errorf("method not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCfgMu.Lock()
|
rawCfgMu.Lock()
|
||||||
defer currentCfgMu.Unlock()
|
defer rawCfgMu.Unlock()
|
||||||
|
|
||||||
|
if ifMatchHeader != "" {
|
||||||
|
// expect the first and last character to be quotes
|
||||||
|
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read out the parts
|
||||||
|
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the current hash of the config
|
||||||
|
// at the given path
|
||||||
|
hash := etagHasher()
|
||||||
|
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusPreconditionFailed,
|
||||||
|
Err: fmt.Errorf("If-Match header did not match current config hash"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := unsyncedConfigAccess(method, path, input, nil)
|
err := unsyncedConfigAccess(method, path, input, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -149,8 +215,8 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
|||||||
|
|
||||||
// if nothing changed, no need to do a whole reload unless the client forces it
|
// if nothing changed, no need to do a whole reload unless the client forces it
|
||||||
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
|
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
|
||||||
Log().Named("admin.api").Info("config is unchanged")
|
Log().Info("config is unchanged")
|
||||||
return nil
|
return errSameConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// find any IDs in this config and index them
|
// find any IDs in this config and index them
|
||||||
@@ -172,7 +238,7 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
|||||||
// with what caddy is still running; we need to
|
// with what caddy is still running; we need to
|
||||||
// unmarshal it again because it's likely that
|
// unmarshal it again because it's likely that
|
||||||
// pointers deep in our rawCfg map were modified
|
// pointers deep in our rawCfg map were modified
|
||||||
var oldCfg interface{}
|
var oldCfg any
|
||||||
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
|
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
||||||
@@ -197,18 +263,18 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
|||||||
// readConfig traverses the current config to path
|
// readConfig traverses the current config to path
|
||||||
// and writes its JSON encoding to out.
|
// and writes its JSON encoding to out.
|
||||||
func readConfig(path string, out io.Writer) error {
|
func readConfig(path string, out io.Writer) error {
|
||||||
currentCfgMu.RLock()
|
rawCfgMu.RLock()
|
||||||
defer currentCfgMu.RUnlock()
|
defer rawCfgMu.RUnlock()
|
||||||
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexConfigObjects recursively searches ptr for object fields named
|
// indexConfigObjects recursively searches ptr for object fields named
|
||||||
// "@id" and maps that ID value to the full configPath in the index.
|
// "@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
|
// This function is NOT safe for concurrent access; obtain a write lock
|
||||||
// on currentCfgMu.
|
// on currentCtxMu.
|
||||||
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
|
func indexConfigObjects(ptr any, configPath string, index map[string]string) error {
|
||||||
switch val := ptr.(type) {
|
switch val := ptr.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
for k, v := range val {
|
for k, v := range val {
|
||||||
if k == idKey {
|
if k == idKey {
|
||||||
switch idVal := v.(type) {
|
switch idVal := v.(type) {
|
||||||
@@ -227,7 +293,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case []interface{}:
|
case []any:
|
||||||
// traverse each element of the array recursively
|
// traverse each element of the array recursively
|
||||||
for i := range val {
|
for i := range val {
|
||||||
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
|
err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
|
||||||
@@ -245,7 +311,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
|
|||||||
// it as the new config, replacing any other current config.
|
// it as the new config, replacing any other current config.
|
||||||
// It does NOT update the raw config state, as this is a
|
// It does NOT update the raw config state, as this is a
|
||||||
// lower-level function; most callers will want to use Load
|
// lower-level function; most callers will want to use Load
|
||||||
// instead. A write lock on currentCfgMu is required! If
|
// instead. A write lock on rawCfgMu is required! If
|
||||||
// allowPersist is false, it will not be persisted to disk,
|
// allowPersist is false, it will not be persisted to disk,
|
||||||
// even if it is configured to.
|
// even if it is configured to.
|
||||||
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||||
@@ -254,7 +320,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
strippedCfgJSON := RemoveMetaFields(cfgJSON)
|
strippedCfgJSON := RemoveMetaFields(cfgJSON)
|
||||||
|
|
||||||
var newCfg *Config
|
var newCfg *Config
|
||||||
err := strictUnmarshalJSON(strippedCfgJSON, &newCfg)
|
err := StrictUnmarshalJSON(strippedCfgJSON, &newCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -269,22 +335,24 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
newCfg.Admin != nil &&
|
newCfg.Admin != nil &&
|
||||||
newCfg.Admin.Config != nil &&
|
newCfg.Admin.Config != nil &&
|
||||||
newCfg.Admin.Config.LoadRaw != nil &&
|
newCfg.Admin.Config.LoadRaw != nil &&
|
||||||
newCfg.Admin.Config.LoadInterval <= 0 {
|
newCfg.Admin.Config.LoadDelay <= 0 {
|
||||||
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval")
|
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the new config and start all its apps
|
// run the new config and start all its apps
|
||||||
err = run(newCfg, true)
|
ctx, err := run(newCfg, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap old config with the new one
|
// swap old context (including its config) with the new one
|
||||||
oldCfg := currentCfg
|
currentCtxMu.Lock()
|
||||||
currentCfg = newCfg
|
oldCtx := currentCtx
|
||||||
|
currentCtx = ctx
|
||||||
|
currentCtxMu.Unlock()
|
||||||
|
|
||||||
// Stop, Cleanup each old app
|
// Stop, Cleanup each old app
|
||||||
unsyncedStop(oldCfg)
|
unsyncedStop(oldCtx)
|
||||||
|
|
||||||
// autosave a non-nil config, if not disabled
|
// autosave a non-nil config, if not disabled
|
||||||
if allowPersist &&
|
if allowPersist &&
|
||||||
@@ -294,13 +362,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
newCfg.Admin.Config.Persist == nil ||
|
newCfg.Admin.Config.Persist == nil ||
|
||||||
*newCfg.Admin.Config.Persist) {
|
*newCfg.Admin.Config.Persist) {
|
||||||
dir := filepath.Dir(ConfigAutosavePath)
|
dir := filepath.Dir(ConfigAutosavePath)
|
||||||
err := os.MkdirAll(dir, 0700)
|
err := os.MkdirAll(dir, 0o700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Log().Error("unable to create folder for config autosave",
|
Log().Error("unable to create folder for config autosave",
|
||||||
zap.String("dir", dir),
|
zap.String("dir", dir),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
} else {
|
} else {
|
||||||
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
||||||
} else {
|
} else {
|
||||||
@@ -328,7 +396,63 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
// This is a low-level function; most callers
|
// This is a low-level function; most callers
|
||||||
// will want to use Run instead, which also
|
// will want to use Run instead, which also
|
||||||
// updates the config's raw state.
|
// updates the config's raw state.
|
||||||
func run(newCfg *Config, start bool) error {
|
func run(newCfg *Config, start bool) (Context, error) {
|
||||||
|
ctx, err := provisionContext(newCfg, start)
|
||||||
|
if err != nil {
|
||||||
|
globalMetrics.configSuccess.Set(0)
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !start {
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision any admin routers which may need to access
|
||||||
|
// some of the other apps at runtime
|
||||||
|
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
||||||
|
if err != nil {
|
||||||
|
globalMetrics.configSuccess.Set(0)
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start
|
||||||
|
err = func() error {
|
||||||
|
started := make([]string, 0, len(ctx.cfg.apps))
|
||||||
|
for name, a := range ctx.cfg.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 := ctx.cfg.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
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
globalMetrics.configSuccess.Set(0)
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
globalMetrics.configSuccess.Set(1)
|
||||||
|
globalMetrics.configSuccessTime.SetToCurrentTime()
|
||||||
|
// now that the user's config is running, finish setting up anything else,
|
||||||
|
// such as remote admin endpoint, config loader, etc.
|
||||||
|
return ctx, finishSettingUp(ctx, ctx.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// provisionContext creates a new context from the given configuration and provisions
|
||||||
|
// storage and apps.
|
||||||
|
// If `newCfg` is nil a new empty configuration will be created.
|
||||||
|
// If `replaceAdminServer` is true any currently active admin server will be replaced
|
||||||
|
// with a new admin server based on the provided configuration.
|
||||||
|
func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) {
|
||||||
// because we will need to roll back any state
|
// because we will need to roll back any state
|
||||||
// modifications if this function errors, we
|
// modifications if this function errors, we
|
||||||
// keep a single error value and scope all
|
// keep a single error value and scope all
|
||||||
@@ -351,6 +475,7 @@ func run(newCfg *Config, start bool) error {
|
|||||||
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
globalMetrics.configSuccess.Set(0)
|
||||||
// if there were any errors during startup,
|
// if there were any errors during startup,
|
||||||
// we should cancel the new context we created
|
// we should cancel the new context we created
|
||||||
// since the associated config won't be used;
|
// since the associated config won't be used;
|
||||||
@@ -359,8 +484,8 @@ func run(newCfg *Config, start bool) error {
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
// also undo any other state changes we made
|
// also undo any other state changes we made
|
||||||
if currentCfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
certmagic.Default.Storage = currentCfg.storage
|
certmagic.Default.Storage = currentCtx.cfg.storage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -372,17 +497,20 @@ func run(newCfg *Config, start bool) error {
|
|||||||
}
|
}
|
||||||
err = newCfg.Logging.openLogs(ctx)
|
err = newCfg.Logging.openLogs(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the admin endpoint (and stop any prior one)
|
// start the admin endpoint (and stop any prior one)
|
||||||
if start {
|
if replaceAdminServer {
|
||||||
err = replaceLocalAdminServer(newCfg)
|
err = replaceLocalAdminServer(newCfg, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting caddy administration endpoint: %v", err)
|
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the new filesystem map
|
||||||
|
newCfg.filesystems = &filesystems.FilesystemMap{}
|
||||||
|
|
||||||
// prepare the new config for use
|
// prepare the new config for use
|
||||||
newCfg.apps = make(map[string]App)
|
newCfg.apps = make(map[string]App)
|
||||||
|
|
||||||
@@ -408,7 +536,7 @@ func run(newCfg *Config, start bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and Provision each app and their submodules
|
// Load and Provision each app and their submodules
|
||||||
@@ -420,42 +548,16 @@ func run(newCfg *Config, start bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
return ctx, err
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !start {
|
// ProvisionContext creates a new context from the configuration and provisions storage
|
||||||
return nil
|
// and app modules.
|
||||||
}
|
// The function is intended for testing and advanced use cases only, typically `Run` should be
|
||||||
|
// use to ensure a fully functional caddy instance.
|
||||||
// Start
|
// EXPERIMENTAL: While this is public the interface and implementation details of this function may change.
|
||||||
err = func() error {
|
func ProvisionContext(newCfg *Config) (Context, error) {
|
||||||
var started []string
|
return provisionContext(newCfg, false)
|
||||||
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
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// now that the user's config is running, finish setting up anything else,
|
|
||||||
// such as remote admin endpoint, config loader, etc.
|
|
||||||
return finishSettingUp(ctx, newCfg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishSettingUp should be run after all apps have successfully started.
|
// finishSettingUp should be run after all apps have successfully started.
|
||||||
@@ -481,49 +583,74 @@ func finishSettingUp(ctx Context, cfg *Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading config loader module: %s", err)
|
return fmt.Errorf("loading config loader module: %s", err)
|
||||||
}
|
}
|
||||||
runLoadedConfig := func(config []byte) {
|
|
||||||
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval)))
|
logger := Log().Named("config_loader").With(
|
||||||
currentCfgMu.Lock()
|
zap.String("module", val.(Module).CaddyModule().ID.Name()),
|
||||||
err := unsyncedDecodeAndRun(config, false)
|
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
|
||||||
currentCfgMu.Unlock()
|
|
||||||
if err == nil {
|
runLoadedConfig := func(config []byte) error {
|
||||||
Log().Info("dynamically-loaded config applied successfully")
|
logger.Info("applying dynamically-loaded config")
|
||||||
} else {
|
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
|
||||||
Log().Error("running dynamically-loaded config failed", zap.Error(err))
|
if errors.Is(err, errSameConfig) {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("successfully applied dynamically-loaded config")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if cfg.Admin.Config.LoadInterval > 0 {
|
|
||||||
|
if cfg.Admin.Config.LoadDelay > 0 {
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
// the loop is here to iterate ONLY if there is an error, a no-op config load,
|
||||||
// if LoadInterval is positive, will wait for the interval and then run with new config
|
// or an unchanged config; in which case we simply wait the delay and try again
|
||||||
case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)):
|
for {
|
||||||
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
|
||||||
if err != nil {
|
select {
|
||||||
Log().Error("loading dynamic config failed", zap.Error(err))
|
case <-timer.C:
|
||||||
return
|
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if loadedConfig == nil {
|
||||||
|
logger.Info("dynamically-loaded config was nil; will retry")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = runLoadedConfig(loadedConfig)
|
||||||
|
if errors.Is(err, errSameConfig) {
|
||||||
|
logger.Info("dynamically-loaded config was unchanged; will retry")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
|
logger.Info("stopping dynamic config loading")
|
||||||
}
|
}
|
||||||
runLoadedConfig(loadedConfig)
|
break
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
// if no LoadInterval is provided, will load config synchronously
|
// if no LoadDelay is provided, will load config synchronously
|
||||||
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
|
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
|
||||||
}
|
}
|
||||||
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
|
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
|
||||||
go runLoadedConfig(loadedConfig)
|
go func() { _ = runLoadedConfig(loadedConfig) }()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigLoader is a type that can load a Caddy config. The
|
// ConfigLoader is a type that can load a Caddy config. If
|
||||||
// returned config must be valid Caddy JSON.
|
// the return value is non-nil, it must be valid Caddy JSON;
|
||||||
|
// if nil or with non-nil error, it is considered to be a
|
||||||
|
// no-op load and may be retried later.
|
||||||
type ConfigLoader interface {
|
type ConfigLoader interface {
|
||||||
LoadConfig(Context) ([]byte, error)
|
LoadConfig(Context) ([]byte, error)
|
||||||
}
|
}
|
||||||
@@ -535,29 +662,42 @@ type ConfigLoader interface {
|
|||||||
// stop the others. Stop should only be called
|
// stop the others. Stop should only be called
|
||||||
// if not replacing with a new config.
|
// if not replacing with a new config.
|
||||||
func Stop() error {
|
func Stop() error {
|
||||||
currentCfgMu.Lock()
|
currentCtxMu.RLock()
|
||||||
defer currentCfgMu.Unlock()
|
ctx := currentCtx
|
||||||
unsyncedStop(currentCfg)
|
currentCtxMu.RUnlock()
|
||||||
currentCfg = nil
|
|
||||||
|
rawCfgMu.Lock()
|
||||||
|
unsyncedStop(ctx)
|
||||||
|
|
||||||
|
currentCtxMu.Lock()
|
||||||
|
currentCtx = Context{}
|
||||||
|
currentCtxMu.Unlock()
|
||||||
|
|
||||||
rawCfgJSON = nil
|
rawCfgJSON = nil
|
||||||
rawCfgIndex = nil
|
rawCfgIndex = nil
|
||||||
rawCfg[rawConfigKey] = nil
|
rawCfg[rawConfigKey] = nil
|
||||||
|
rawCfgMu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsyncedStop stops cfg from running, but has
|
// unsyncedStop stops ctx from running, but has
|
||||||
// no locking around cfg. It is a no-op if cfg is
|
// no locking around ctx. It is a no-op if ctx has a
|
||||||
// nil. If any app returns an error when stopping,
|
// nil cfg. If any app returns an error when stopping,
|
||||||
// it is logged and the function continues stopping
|
// it is logged and the function continues stopping
|
||||||
// the next app. This function assumes all apps in
|
// the next app. This function assumes all apps in
|
||||||
// cfg were successfully started first.
|
// ctx were successfully started first.
|
||||||
func unsyncedStop(cfg *Config) {
|
//
|
||||||
if cfg == nil {
|
// A lock on rawCfgMu is required, even though this
|
||||||
|
// function does not access rawCfg, that lock
|
||||||
|
// synchronizes the stop/start of apps.
|
||||||
|
func unsyncedStop(ctx Context) {
|
||||||
|
if ctx.cfg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop each app
|
// stop each app
|
||||||
for name, a := range cfg.apps {
|
for name, a := range ctx.cfg.apps {
|
||||||
err := a.Stop()
|
err := a.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] stop %s: %v", name, err)
|
log.Printf("[ERROR] stop %s: %v", name, err)
|
||||||
@@ -565,13 +705,13 @@ func unsyncedStop(cfg *Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clean up all modules
|
// clean up all modules
|
||||||
cfg.cancelFunc()
|
ctx.cfg.cancelFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate loads, provisions, and validates
|
// Validate loads, provisions, and validates
|
||||||
// cfg, but does not start running it.
|
// cfg, but does not start running it.
|
||||||
func Validate(cfg *Config) error {
|
func Validate(cfg *Config) error {
|
||||||
err := run(cfg, false)
|
_, err := run(cfg, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cfg.cancelFunc() // call Cleanup on all modules
|
cfg.cancelFunc() // call Cleanup on all modules
|
||||||
}
|
}
|
||||||
@@ -584,13 +724,22 @@ func Validate(cfg *Config) error {
|
|||||||
// PID file, and shuts down admin endpoint(s) in a goroutine.
|
// PID file, and shuts down admin endpoint(s) in a goroutine.
|
||||||
// Errors are logged along the way, and an appropriate exit
|
// Errors are logged along the way, and an appropriate exit
|
||||||
// code is emitted.
|
// code is emitted.
|
||||||
func exitProcess(logger *zap.Logger) {
|
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||||
|
// let the rest of the program know we're quitting
|
||||||
|
atomic.StoreInt32(exiting, 1)
|
||||||
|
|
||||||
|
// give the OS or service/process manager our 2 weeks' notice: we quit
|
||||||
|
if err := notify.Stopping(); err != nil {
|
||||||
|
Log().Error("unable to notify service manager of stopping state", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = Log()
|
logger = Log()
|
||||||
}
|
}
|
||||||
logger.Warn("exiting; byeee!! 👋")
|
logger.Warn("exiting; byeee!! 👋")
|
||||||
|
|
||||||
exitCode := ExitCodeSuccess
|
exitCode := ExitCodeSuccess
|
||||||
|
lastContext := ActiveContext()
|
||||||
|
|
||||||
// stop all apps
|
// stop all apps
|
||||||
if err := Stop(); err != nil {
|
if err := Stop(); err != nil {
|
||||||
@@ -599,7 +748,7 @@ func exitProcess(logger *zap.Logger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clean up certmagic locks
|
// clean up certmagic locks
|
||||||
certmagic.CleanUpOwnLocks(logger)
|
certmagic.CleanUpOwnLocks(ctx, logger)
|
||||||
|
|
||||||
// remove pidfile
|
// remove pidfile
|
||||||
if pidfile != "" {
|
if pidfile != "" {
|
||||||
@@ -612,6 +761,16 @@ func exitProcess(logger *zap.Logger) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute any process-exit callbacks
|
||||||
|
for _, exitFunc := range lastContext.exitFuncs {
|
||||||
|
exitFunc(ctx)
|
||||||
|
}
|
||||||
|
exitFuncsMu.Lock()
|
||||||
|
for _, exitFunc := range exitFuncs {
|
||||||
|
exitFunc(ctx)
|
||||||
|
}
|
||||||
|
exitFuncsMu.Unlock()
|
||||||
|
|
||||||
// shut down admin endpoint(s) in goroutines so that
|
// shut down admin endpoint(s) in goroutines so that
|
||||||
// if this function was called from an admin handler,
|
// if this function was called from an admin handler,
|
||||||
// it has a chance to return gracefully
|
// it has a chance to return gracefully
|
||||||
@@ -644,6 +803,29 @@ func exitProcess(logger *zap.Logger) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var exiting = new(int32) // accessed atomically
|
||||||
|
|
||||||
|
// Exiting returns true if the process is exiting.
|
||||||
|
// EXPERIMENTAL API: subject to change or removal.
|
||||||
|
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
|
||||||
|
|
||||||
|
// OnExit registers a callback to invoke during process exit.
|
||||||
|
// This registration is PROCESS-GLOBAL, meaning that each
|
||||||
|
// function should only be registered once forever, NOT once
|
||||||
|
// per config load (etc).
|
||||||
|
//
|
||||||
|
// EXPERIMENTAL API: subject to change or removal.
|
||||||
|
func OnExit(f func(context.Context)) {
|
||||||
|
exitFuncsMu.Lock()
|
||||||
|
exitFuncs = append(exitFuncs, f)
|
||||||
|
exitFuncsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
exitFuncs []func(context.Context)
|
||||||
|
exitFuncsMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
// Duration can be an integer or a string. An integer is
|
// Duration can be an integer or a string. An integer is
|
||||||
// interpreted as nanoseconds. If a string, it is a Go
|
// interpreted as nanoseconds. If a string, it is a Go
|
||||||
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
|
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
|
||||||
@@ -668,8 +850,12 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
|
|||||||
|
|
||||||
// ParseDuration parses a duration string, adding
|
// ParseDuration parses a duration string, adding
|
||||||
// support for the "d" unit meaning number of days,
|
// support for the "d" unit meaning number of days,
|
||||||
// where a day is assumed to be 24h.
|
// where a day is assumed to be 24h. The maximum
|
||||||
|
// input string length is 1024.
|
||||||
func ParseDuration(s string) (time.Duration, error) {
|
func ParseDuration(s string) (time.Duration, error) {
|
||||||
|
if len(s) > 1024 {
|
||||||
|
return 0, fmt.Errorf("parsing duration: input string too long")
|
||||||
|
}
|
||||||
var inNumber bool
|
var inNumber bool
|
||||||
var numStart int
|
var numStart int
|
||||||
for i := 0; i < len(s); i++ {
|
for i := 0; i < len(s); i++ {
|
||||||
@@ -699,14 +885,19 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||||||
// regardless of storage configuration, since each instance is intended to
|
// regardless of storage configuration, since each instance is intended to
|
||||||
// have its own unique ID.
|
// have its own unique ID.
|
||||||
func InstanceID() (uuid.UUID, error) {
|
func InstanceID() (uuid.UUID, error) {
|
||||||
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
|
appDataDir := AppDataDir()
|
||||||
uuidFileBytes, err := ioutil.ReadFile(uuidFilePath)
|
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
|
||||||
if os.IsNotExist(err) {
|
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return uuid, err
|
return uuid, err
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
|
err = os.MkdirAll(appDataDir, 0o700)
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
|
||||||
return uuid, err
|
return uuid, err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return [16]byte{}, err
|
return [16]byte{}, err
|
||||||
@@ -714,36 +905,144 @@ func InstanceID() (uuid.UUID, error) {
|
|||||||
return uuid.ParseBytes(uuidFileBytes)
|
return uuid.ParseBytes(uuidFileBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoModule returns the build info of this Caddy
|
// CustomVersion is an optional string that overrides Caddy's
|
||||||
// build from debug.BuildInfo (requires Go modules).
|
// reported version. It can be helpful when downstream packagers
|
||||||
// If no version information is available, a non-nil
|
// need to manually set Caddy's version. If no other version
|
||||||
// value will still be returned, but with an
|
// information is available, the short form version (see
|
||||||
// unknown version.
|
// Version()) will be set to CustomVersion, and the full version
|
||||||
func GoModule() *debug.Module {
|
// will include CustomVersion at the beginning.
|
||||||
var mod debug.Module
|
//
|
||||||
return goModule(&mod)
|
// Set this variable during `go build` with `-ldflags`:
|
||||||
}
|
//
|
||||||
|
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
|
||||||
|
//
|
||||||
|
// for example.
|
||||||
|
var CustomVersion string
|
||||||
|
|
||||||
// goModule holds the actual implementation of GoModule.
|
// Version returns the Caddy version in a simple/short form, and
|
||||||
// Allocating debug.Module in GoModule() and passing a
|
// a full version string. The short form will not have spaces and
|
||||||
// reference to goModule enables mid-stack inlining.
|
// is intended for User-Agent strings and similar, but may be
|
||||||
func goModule(mod *debug.Module) *debug.Module {
|
// omitting valuable information. Note that Caddy must be compiled
|
||||||
mod.Version = "unknown"
|
// in a special way to properly embed complete version information.
|
||||||
|
// First this function tries to get the version from the embedded
|
||||||
|
// build info provided by go.mod dependencies; then it tries to
|
||||||
|
// get info from embedded VCS information, which requires having
|
||||||
|
// built Caddy from a git repository. If no version is available,
|
||||||
|
// this function returns "(devel)" because Go uses that, but for
|
||||||
|
// the simple form we change it to "unknown". If still no version
|
||||||
|
// is available (e.g. no VCS repo), then it will use CustomVersion;
|
||||||
|
// CustomVersion is always prepended to the full version string.
|
||||||
|
//
|
||||||
|
// See relevant Go issues: https://github.com/golang/go/issues/29228
|
||||||
|
// and https://github.com/golang/go/issues/50603.
|
||||||
|
//
|
||||||
|
// This function is experimental and subject to change or removal.
|
||||||
|
func Version() (simple, full string) {
|
||||||
|
// the currently-recommended way to build Caddy involves
|
||||||
|
// building it as a dependency so we can extract version
|
||||||
|
// information from go.mod tooling; once the upstream
|
||||||
|
// Go issues are fixed, we should just be able to use
|
||||||
|
// bi.Main... hopefully.
|
||||||
|
var module *debug.Module
|
||||||
bi, ok := debug.ReadBuildInfo()
|
bi, ok := debug.ReadBuildInfo()
|
||||||
if ok {
|
if !ok {
|
||||||
mod.Path = bi.Main.Path
|
if CustomVersion != "" {
|
||||||
// The recommended way to build Caddy involves
|
full = CustomVersion
|
||||||
// creating a separate main module, which
|
simple = CustomVersion
|
||||||
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
return
|
||||||
// once that issue is fixed, we should just be able to use bi.Main... hopefully.
|
}
|
||||||
for _, dep := range bi.Deps {
|
full = "unknown"
|
||||||
if dep.Path == ImportPath {
|
simple = "unknown"
|
||||||
return dep
|
return
|
||||||
|
}
|
||||||
|
// find the Caddy module in the dependency list
|
||||||
|
for _, dep := range bi.Deps {
|
||||||
|
if dep.Path == ImportPath {
|
||||||
|
module = dep
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if module != nil {
|
||||||
|
simple, full = module.Version, module.Version
|
||||||
|
if module.Sum != "" {
|
||||||
|
full += " " + module.Sum
|
||||||
|
}
|
||||||
|
if module.Replace != nil {
|
||||||
|
full += " => " + module.Replace.Path
|
||||||
|
if module.Replace.Version != "" {
|
||||||
|
simple = module.Replace.Version + "_custom"
|
||||||
|
full += "@" + module.Replace.Version
|
||||||
|
}
|
||||||
|
if module.Replace.Sum != "" {
|
||||||
|
full += " " + module.Replace.Sum
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &bi.Main
|
|
||||||
}
|
}
|
||||||
return mod
|
|
||||||
|
if full == "" {
|
||||||
|
var vcsRevision string
|
||||||
|
var vcsTime time.Time
|
||||||
|
var vcsModified bool
|
||||||
|
for _, setting := range bi.Settings {
|
||||||
|
switch setting.Key {
|
||||||
|
case "vcs.revision":
|
||||||
|
vcsRevision = setting.Value
|
||||||
|
case "vcs.time":
|
||||||
|
vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
|
||||||
|
case "vcs.modified":
|
||||||
|
vcsModified, _ = strconv.ParseBool(setting.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if vcsRevision != "" {
|
||||||
|
var modified string
|
||||||
|
if vcsModified {
|
||||||
|
modified = "+modified"
|
||||||
|
}
|
||||||
|
full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
|
||||||
|
simple = vcsRevision
|
||||||
|
|
||||||
|
// use short checksum for simple, if hex-only
|
||||||
|
if _, err := hex.DecodeString(simple); err == nil {
|
||||||
|
simple = simple[:8]
|
||||||
|
}
|
||||||
|
|
||||||
|
// append date to simple since it can be convenient
|
||||||
|
// to know the commit date as part of the version
|
||||||
|
if !vcsTime.IsZero() {
|
||||||
|
simple += "-" + vcsTime.Format("20060102")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if full == "" {
|
||||||
|
if CustomVersion != "" {
|
||||||
|
full = CustomVersion
|
||||||
|
} else {
|
||||||
|
full = "unknown"
|
||||||
|
}
|
||||||
|
} else if CustomVersion != "" {
|
||||||
|
full = CustomVersion + " " + full
|
||||||
|
}
|
||||||
|
|
||||||
|
if simple == "" || simple == "(devel)" {
|
||||||
|
if CustomVersion != "" {
|
||||||
|
simple = CustomVersion
|
||||||
|
} else {
|
||||||
|
simple = "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveContext returns the currently-active context.
|
||||||
|
// This function is experimental and might be changed
|
||||||
|
// or removed in the future.
|
||||||
|
func ActiveContext() Context {
|
||||||
|
currentCtxMu.RLock()
|
||||||
|
defer currentCtxMu.RUnlock()
|
||||||
|
return currentCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
// CtxKey is a value type for use with context.WithValue.
|
// CtxKey is a value type for use with context.WithValue.
|
||||||
@@ -751,18 +1050,19 @@ type CtxKey string
|
|||||||
|
|
||||||
// This group of variables pertains to the current configuration.
|
// This group of variables pertains to the current configuration.
|
||||||
var (
|
var (
|
||||||
// currentCfgMu protects everything in this var block.
|
// currentCtx is the root context for the currently-running
|
||||||
currentCfgMu sync.RWMutex
|
// configuration, which can be accessed through this value.
|
||||||
|
// If the Config contained in this value is not nil, then
|
||||||
// currentCfg is the currently-running configuration.
|
// a config is currently active/running.
|
||||||
currentCfg *Config
|
currentCtx Context
|
||||||
|
currentCtxMu sync.RWMutex
|
||||||
|
|
||||||
// rawCfg is the current, generic-decoded configuration;
|
// rawCfg is the current, generic-decoded configuration;
|
||||||
// we initialize it as a map with one field ("config")
|
// we initialize it as a map with one field ("config")
|
||||||
// to maintain parity with the API endpoint and to avoid
|
// to maintain parity with the API endpoint and to avoid
|
||||||
// the special case of having to access/mutate the variable
|
// the special case of having to access/mutate the variable
|
||||||
// directly without traversing into it.
|
// directly without traversing into it.
|
||||||
rawCfg = map[string]interface{}{
|
rawCfg = map[string]any{
|
||||||
rawConfigKey: nil,
|
rawConfigKey: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +1073,17 @@ var (
|
|||||||
// rawCfgIndex is the map of user-assigned ID to expanded
|
// rawCfgIndex is the map of user-assigned ID to expanded
|
||||||
// path, for converting /id/ paths to /config/ paths.
|
// path, for converting /id/ paths to /config/ paths.
|
||||||
rawCfgIndex map[string]string
|
rawCfgIndex map[string]string
|
||||||
|
|
||||||
|
// rawCfgMu protects all the rawCfg fields and also
|
||||||
|
// essentially synchronizes config changes/reloads.
|
||||||
|
rawCfgMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errSameConfig is returned if the new config is the same
|
||||||
|
// as the old one. This isn't usually an actual, actionable
|
||||||
|
// error; it's mostly a sentinel value.
|
||||||
|
var errSameConfig = errors.New("config is unchanged")
|
||||||
|
|
||||||
// ImportPath is the package import path for Caddy core.
|
// ImportPath is the package import path for Caddy core.
|
||||||
|
// This identifier may be removed in the future.
|
||||||
const ImportPath = "github.com/caddyserver/caddy/v2"
|
const ImportPath = "github.com/caddyserver/caddy/v2"
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ type Adapter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adapt converts the Caddyfile config in body to Caddy JSON.
|
// Adapt converts the Caddyfile config in body to Caddy JSON.
|
||||||
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
|
func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconfig.Warning, error) {
|
||||||
if a.ServerType == nil {
|
if a.ServerType == nil {
|
||||||
return nil, nil, fmt.Errorf("no server type")
|
return nil, nil, fmt.Errorf("no server type")
|
||||||
}
|
}
|
||||||
if options == nil {
|
if options == nil {
|
||||||
options = make(map[string]interface{})
|
options = make(map[string]any)
|
||||||
}
|
}
|
||||||
|
|
||||||
filename, _ := options["filename"].(string)
|
filename, _ := options["filename"].(string)
|
||||||
@@ -52,9 +52,9 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// lint check: see if input was properly formatted; sometimes messy files files parse
|
// lint check: see if input was properly formatted; sometimes messy files parse
|
||||||
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
|
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
|
||||||
if warning, different := formattingDifference(filename, body); different {
|
if warning, different := FormattingDifference(filename, body); different {
|
||||||
warnings = append(warnings, warning)
|
warnings = append(warnings, warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +63,10 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
|
|||||||
return result, warnings, err
|
return result, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// formattingDifference returns a warning and true if the formatted version
|
// FormattingDifference returns a warning and true if the formatted version
|
||||||
// is any different from the input; empty warning and false otherwise.
|
// is any different from the input; empty warning and false otherwise.
|
||||||
// TODO: also perform this check on imported files
|
// TODO: also perform this check on imported files
|
||||||
func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
|
func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
|
||||||
// replace windows-style newlines to normalize comparison
|
// replace windows-style newlines to normalize comparison
|
||||||
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
|
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
|
||||||
|
|
||||||
@@ -88,35 +88,31 @@ func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bo
|
|||||||
return caddyconfig.Warning{
|
return caddyconfig.Warning{
|
||||||
File: filename,
|
File: filename,
|
||||||
Line: line,
|
Line: line,
|
||||||
Message: "input is not formatted with 'caddy fmt'",
|
Message: "Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies",
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshaler is a type that can unmarshal
|
// Unmarshaler is a type that can unmarshal Caddyfile tokens to
|
||||||
// Caddyfile tokens to set itself up for a
|
// set itself up for a JSON encoding. The goal of an unmarshaler
|
||||||
// JSON encoding. The goal of an unmarshaler
|
// is not to set itself up for actual use, but to set itself up for
|
||||||
// is not to set itself up for actual use,
|
// being marshaled into JSON. Caddyfile-unmarshaled values will not
|
||||||
// but to set itself up for being marshaled
|
// be used directly; they will be encoded as JSON and then used from
|
||||||
// into JSON. Caddyfile-unmarshaled values
|
// that. Implementations _may_ be able to support multiple segments
|
||||||
// will not be used directly; they will be
|
// (instances of their directive or batch of tokens); typically this
|
||||||
// encoded as JSON and then used from that.
|
// means wrapping parsing logic in a loop: `for d.Next() { ... }`.
|
||||||
// Implementations must be able to support
|
// More commonly, only a single segment is supported, so a simple
|
||||||
// multiple segments (instances of their
|
// `d.Next()` at the start should be used to consume the module
|
||||||
// directive or batch of tokens); typically
|
// identifier token (directive name, etc).
|
||||||
// this means wrapping all token logic in
|
|
||||||
// a loop: `for d.Next() { ... }`.
|
|
||||||
type Unmarshaler interface {
|
type Unmarshaler interface {
|
||||||
UnmarshalCaddyfile(d *Dispenser) error
|
UnmarshalCaddyfile(d *Dispenser) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
|
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
|
||||||
type ServerType interface {
|
type ServerType interface {
|
||||||
// Setup takes the server blocks which
|
// Setup takes the server blocks which contain tokens,
|
||||||
// contain tokens, as well as options
|
// as well as options (e.g. CLI flags) and creates a
|
||||||
// (e.g. CLI flags) and creates a Caddy
|
// Caddy config, along with any warnings or an error.
|
||||||
// config, along with any warnings or
|
Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error)
|
||||||
// an error.
|
|
||||||
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalModule instantiates a module with the given ID and invokes
|
// UnmarshalModule instantiates a module with the given ID and invokes
|
||||||
|
|||||||
Executable → Regular
+154
-29
@@ -19,6 +19,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ type Dispenser struct {
|
|||||||
tokens []Token
|
tokens []Token
|
||||||
cursor int
|
cursor int
|
||||||
nesting int
|
nesting int
|
||||||
|
|
||||||
|
// A map of arbitrary context data that can be used
|
||||||
|
// to pass through some information to unmarshalers.
|
||||||
|
context map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDispenser returns a Dispenser filled with the given tokens.
|
// NewDispenser returns a Dispenser filled with the given tokens.
|
||||||
@@ -100,12 +105,12 @@ func (d *Dispenser) nextOnSameLine() bool {
|
|||||||
d.cursor++
|
d.cursor++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if d.cursor >= len(d.tokens) {
|
if d.cursor >= len(d.tokens)-1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if d.cursor < len(d.tokens)-1 &&
|
curr := d.tokens[d.cursor]
|
||||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
next := d.tokens[d.cursor+1]
|
||||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
if !isNextOnNewLine(curr, next) {
|
||||||
d.cursor++
|
d.cursor++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -121,12 +126,12 @@ func (d *Dispenser) NextLine() bool {
|
|||||||
d.cursor++
|
d.cursor++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if d.cursor >= len(d.tokens) {
|
if d.cursor >= len(d.tokens)-1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if d.cursor < len(d.tokens)-1 &&
|
curr := d.tokens[d.cursor]
|
||||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
next := d.tokens[d.cursor+1]
|
||||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
if isNextOnNewLine(curr, next) {
|
||||||
d.cursor++
|
d.cursor++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -145,15 +150,15 @@ func (d *Dispenser) NextLine() bool {
|
|||||||
//
|
//
|
||||||
// Proper use of this method looks like this:
|
// Proper use of this method looks like this:
|
||||||
//
|
//
|
||||||
// for nesting := d.Nesting(); d.NextBlock(nesting); {
|
// for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// However, in simple cases where it is known that the
|
// However, in simple cases where it is known that the
|
||||||
// Dispenser is new and has not already traversed state
|
// Dispenser is new and has not already traversed state
|
||||||
// by a loop over NextBlock(), this will do:
|
// by a loop over NextBlock(), this will do:
|
||||||
//
|
//
|
||||||
// for d.NextBlock(0) {
|
// for d.NextBlock(0) {
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// As with other token parsing logic, a loop over
|
// As with other token parsing logic, a loop over
|
||||||
// NextBlock() should be contained within a loop over
|
// NextBlock() should be contained within a loop over
|
||||||
@@ -201,6 +206,46 @@ func (d *Dispenser) Val() string {
|
|||||||
return d.tokens[d.cursor].Text
|
return d.tokens[d.cursor].Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValRaw gets the raw text of the current token (including quotes).
|
||||||
|
// If the token was a heredoc, then the delimiter is not included,
|
||||||
|
// because that is not relevant to any unmarshaling logic at this time.
|
||||||
|
// If there is no token loaded, it returns empty string.
|
||||||
|
func (d *Dispenser) ValRaw() string {
|
||||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
quote := d.tokens[d.cursor].wasQuoted
|
||||||
|
if quote > 0 && quote != '<' {
|
||||||
|
// string literal
|
||||||
|
return string(quote) + d.tokens[d.cursor].Text + string(quote)
|
||||||
|
}
|
||||||
|
return d.tokens[d.cursor].Text
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarVal gets value of the current token, converted to the closest
|
||||||
|
// scalar type. If there is no token loaded, it returns nil.
|
||||||
|
func (d *Dispenser) ScalarVal() any {
|
||||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
quote := d.tokens[d.cursor].wasQuoted
|
||||||
|
text := d.tokens[d.cursor].Text
|
||||||
|
|
||||||
|
if quote > 0 {
|
||||||
|
return text // string literal
|
||||||
|
}
|
||||||
|
if num, err := strconv.Atoi(text); err == nil {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
if num, err := strconv.ParseFloat(text, 64); err == nil {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
if bool, err := strconv.ParseBool(text); err == nil {
|
||||||
|
return bool
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
// Line gets the line number of the current token.
|
// Line gets the line number of the current token.
|
||||||
// If there is no token loaded, it returns 0.
|
// If there is no token loaded, it returns 0.
|
||||||
func (d *Dispenser) Line() int {
|
func (d *Dispenser) Line() int {
|
||||||
@@ -249,6 +294,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountRemainingArgs counts the amount of remaining arguments
|
||||||
|
// (tokens on the same line) without consuming the tokens.
|
||||||
|
func (d *Dispenser) CountRemainingArgs() int {
|
||||||
|
count := 0
|
||||||
|
for d.NextArg() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
d.Prev()
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||||
// into a slice and returns them. Open curly brace tokens also indicate
|
// 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 end of arguments, and the curly brace is not included in
|
||||||
@@ -261,6 +319,18 @@ func (d *Dispenser) RemainingArgs() []string {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
||||||
|
// retaining quotes) 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) RemainingArgsRaw() []string {
|
||||||
|
var args []string
|
||||||
|
for d.NextArg() {
|
||||||
|
args = append(args, d.ValRaw())
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
// NewFromNextSegment returns a new dispenser with a copy of
|
// NewFromNextSegment returns a new dispenser with a copy of
|
||||||
// the tokens from the current token until the end of the
|
// the tokens from the current token until the end of the
|
||||||
// "directive" whether that be to the end of the line or
|
// "directive" whether that be to the end of the line or
|
||||||
@@ -325,33 +395,40 @@ func (d *Dispenser) Reset() {
|
|||||||
// an argument.
|
// an argument.
|
||||||
func (d *Dispenser) ArgErr() error {
|
func (d *Dispenser) ArgErr() error {
|
||||||
if d.Val() == "{" {
|
if d.Val() == "{" {
|
||||||
return d.Err("Unexpected token '{', expecting argument")
|
return d.Err("unexpected token '{', expecting argument")
|
||||||
}
|
}
|
||||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyntaxErr creates a generic syntax error which explains what was
|
// SyntaxErr creates a generic syntax error which explains what was
|
||||||
// found and what was expected.
|
// found and what was expected.
|
||||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
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)
|
msg := fmt.Sprintf("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EOFErr returns an error indicating that the dispenser reached
|
// EOFErr returns an error indicating that the dispenser reached
|
||||||
// the end of the input when searching for the next token.
|
// the end of the input when searching for the next token.
|
||||||
func (d *Dispenser) EOFErr() error {
|
func (d *Dispenser) EOFErr() error {
|
||||||
return d.Errf("Unexpected EOF")
|
return d.Errf("unexpected EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err generates a custom parse-time error with a message of msg.
|
// Err generates a custom parse-time error with a message of msg.
|
||||||
func (d *Dispenser) Err(msg string) error {
|
func (d *Dispenser) Err(msg string) error {
|
||||||
return d.Errf(msg)
|
return d.WrapErr(errors.New(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errf is like Err, but for formatted error messages
|
// Errf is like Err, but for formatted error messages
|
||||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
func (d *Dispenser) Errf(format string, args ...any) error {
|
||||||
err := fmt.Errorf(format, args...)
|
return d.WrapErr(fmt.Errorf(format, args...))
|
||||||
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
|
}
|
||||||
|
|
||||||
|
// WrapErr takes an existing error and adds the Caddyfile file and line number.
|
||||||
|
func (d *Dispenser) WrapErr(err error) error {
|
||||||
|
if len(d.Token().imports) > 0 {
|
||||||
|
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes the current token and returns the updated slice
|
// Delete deletes the current token and returns the updated slice
|
||||||
@@ -371,14 +448,42 @@ func (d *Dispenser) Delete() []Token {
|
|||||||
return d.tokens
|
return d.tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// numLineBreaks counts how many line breaks are in the token
|
// DeleteN is the same as Delete, but can delete many tokens at once.
|
||||||
// value given by the token index tknIdx. It returns 0 if the
|
// If there aren't N tokens available to delete, none are deleted.
|
||||||
// token does not exist or there are no line breaks.
|
func (d *Dispenser) DeleteN(amount int) []Token {
|
||||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
if amount > 0 && d.cursor >= (amount-1) && d.cursor <= len(d.tokens)-1 {
|
||||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
d.tokens = append(d.tokens[:d.cursor-(amount-1)], d.tokens[d.cursor+1:]...)
|
||||||
return 0
|
d.cursor -= amount
|
||||||
}
|
}
|
||||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
return d.tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContext sets a key-value pair in the context map.
|
||||||
|
func (d *Dispenser) SetContext(key string, value any) {
|
||||||
|
if d.context == nil {
|
||||||
|
d.context = make(map[string]any)
|
||||||
|
}
|
||||||
|
d.context[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContext gets the value of a key in the context map.
|
||||||
|
func (d *Dispenser) GetContext(key string) any {
|
||||||
|
if d.context == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.context[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContextString gets the value of a key in the context map
|
||||||
|
// as a string, or an empty string if the key does not exist.
|
||||||
|
func (d *Dispenser) GetContextString(key string) string {
|
||||||
|
if d.context == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if val, ok := d.context[key].(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// isNewLine determines whether the current token is on a different
|
// isNewLine determines whether the current token is on a different
|
||||||
@@ -391,6 +496,26 @@ func (d *Dispenser) isNewLine() bool {
|
|||||||
if d.cursor > len(d.tokens)-1 {
|
if d.cursor > len(d.tokens)-1 {
|
||||||
return false
|
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
|
prev := d.tokens[d.cursor-1]
|
||||||
|
curr := d.tokens[d.cursor]
|
||||||
|
return isNextOnNewLine(prev, curr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNextOnNewLine determines whether the current token is on a different
|
||||||
|
// line (higher line number) than the next token. It handles imported
|
||||||
|
// tokens correctly. If there isn't a next token, it returns true.
|
||||||
|
func (d *Dispenser) isNextOnNewLine() bool {
|
||||||
|
if d.cursor < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if d.cursor >= len(d.tokens)-1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
curr := d.tokens[d.cursor]
|
||||||
|
next := d.tokens[d.cursor+1]
|
||||||
|
return isNextOnNewLine(curr, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatcherNameCtxKey = "matcher_name"
|
||||||
|
|||||||
Executable → Regular
+1
-1
@@ -305,7 +305,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
|||||||
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrBarIsFull = errors.New("bar is full")
|
ErrBarIsFull := errors.New("bar is full")
|
||||||
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
|
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
|
||||||
if !errors.Is(bookingError, ErrBarIsFull) {
|
if !errors.Is(bookingError, ErrBarIsFull) {
|
||||||
t.Errorf("Errf(): should be able to unwrap the error chain")
|
t.Errorf("Errf(): should be able to unwrap the error chain")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package caddyfile
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +32,14 @@ func Format(input []byte) []byte {
|
|||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
rdr := bytes.NewReader(input)
|
rdr := bytes.NewReader(input)
|
||||||
|
|
||||||
|
type heredocState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
heredocClosed heredocState = 0
|
||||||
|
heredocOpening heredocState = 1
|
||||||
|
heredocOpened heredocState = 2
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
last rune // the last character that was written to the result
|
last rune // the last character that was written to the result
|
||||||
|
|
||||||
@@ -47,6 +56,11 @@ func Format(input []byte) []byte {
|
|||||||
quoted bool // whether we're in a quoted segment
|
quoted bool // whether we're in a quoted segment
|
||||||
escaped bool // whether current char is escaped
|
escaped bool // whether current char is escaped
|
||||||
|
|
||||||
|
heredoc heredocState // whether we're in a heredoc
|
||||||
|
heredocEscaped bool // whether heredoc is escaped
|
||||||
|
heredocMarker []rune
|
||||||
|
heredocClosingMarker []rune
|
||||||
|
|
||||||
nesting int // indentation level
|
nesting int // indentation level
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,6 +89,62 @@ func Format(input []byte) []byte {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detect whether we have the start of a heredoc
|
||||||
|
if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
|
||||||
|
space && last == '<' && ch == '<' {
|
||||||
|
write(ch)
|
||||||
|
heredoc = heredocOpening
|
||||||
|
space = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if heredoc == heredocOpening {
|
||||||
|
if ch == '\n' {
|
||||||
|
if len(heredocMarker) > 0 && heredocMarkerRegexp.MatchString(string(heredocMarker)) {
|
||||||
|
heredoc = heredocOpened
|
||||||
|
} else {
|
||||||
|
heredocMarker = nil
|
||||||
|
heredoc = heredocClosed
|
||||||
|
nextLine()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
write(ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if unicode.IsSpace(ch) {
|
||||||
|
// a space means it's just a regular token and not a heredoc
|
||||||
|
heredocMarker = nil
|
||||||
|
heredoc = heredocClosed
|
||||||
|
} else {
|
||||||
|
heredocMarker = append(heredocMarker, ch)
|
||||||
|
write(ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if we're in a heredoc, all characters are read&write as-is
|
||||||
|
if heredoc == heredocOpened {
|
||||||
|
heredocClosingMarker = append(heredocClosingMarker, ch)
|
||||||
|
if len(heredocClosingMarker) > len(heredocMarker)+1 { // We assert that the heredocClosingMarker is followed by a unicode.Space
|
||||||
|
heredocClosingMarker = heredocClosingMarker[1:]
|
||||||
|
}
|
||||||
|
// check if we're done
|
||||||
|
if unicode.IsSpace(ch) && slices.Equal(heredocClosingMarker[:len(heredocClosingMarker)-1], heredocMarker) {
|
||||||
|
heredocMarker = nil
|
||||||
|
heredocClosingMarker = nil
|
||||||
|
heredoc = heredocClosed
|
||||||
|
} else {
|
||||||
|
write(ch)
|
||||||
|
if ch == '\n' {
|
||||||
|
heredocClosingMarker = heredocClosingMarker[:0]
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if last == '<' && space {
|
||||||
|
space = false
|
||||||
|
}
|
||||||
|
|
||||||
if comment {
|
if comment {
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
comment = false
|
comment = false
|
||||||
@@ -98,6 +168,9 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if escaped {
|
if escaped {
|
||||||
|
if ch == '<' {
|
||||||
|
heredocEscaped = true
|
||||||
|
}
|
||||||
write(ch)
|
write(ch)
|
||||||
escaped = false
|
escaped = false
|
||||||
continue
|
continue
|
||||||
@@ -117,6 +190,7 @@ func Format(input []byte) []byte {
|
|||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
space = true
|
space = true
|
||||||
|
heredocEscaped = false
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
newLines++
|
newLines++
|
||||||
}
|
}
|
||||||
@@ -153,7 +227,10 @@ func Format(input []byte) []byte {
|
|||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
nextLine()
|
nextLine()
|
||||||
newLines = 0
|
newLines = 0
|
||||||
nesting++
|
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
||||||
|
if nesting < 10 {
|
||||||
|
nesting++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -202,6 +279,11 @@ func Format(input []byte) []byte {
|
|||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if spacePrior && ch == '<' {
|
||||||
|
space = true
|
||||||
|
}
|
||||||
|
|
||||||
write(ch)
|
write(ch)
|
||||||
|
|
||||||
beginningOfLine = false
|
beginningOfLine = false
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// +build gofuzz
|
//go:build gofuzz
|
||||||
|
|
||||||
package caddyfile
|
package caddyfile
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ d {
|
|||||||
{$F}
|
{$F}
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "env var placeholders with port",
|
||||||
|
input: `:{$PORT}`,
|
||||||
|
expect: `:{$PORT}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "comments",
|
description: "comments",
|
||||||
input: `#a "\n"
|
input: `#a "\n"
|
||||||
@@ -357,6 +362,76 @@ block {
|
|||||||
|
|
||||||
block {
|
block {
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "keep heredoc as-is",
|
||||||
|
input: `block {
|
||||||
|
heredoc <<HEREDOC
|
||||||
|
Here's more than one space Here's more than one space
|
||||||
|
HEREDOC
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: `block {
|
||||||
|
heredoc <<HEREDOC
|
||||||
|
Here's more than one space Here's more than one space
|
||||||
|
HEREDOC
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Mixing heredoc with regular part",
|
||||||
|
input: `block {
|
||||||
|
heredoc <<HEREDOC
|
||||||
|
Here's more than one space Here's more than one space
|
||||||
|
HEREDOC
|
||||||
|
respond "More than one space will be eaten" 200
|
||||||
|
}
|
||||||
|
|
||||||
|
block2 {
|
||||||
|
heredoc <<HEREDOC
|
||||||
|
Here's more than one space Here's more than one space
|
||||||
|
HEREDOC
|
||||||
|
respond "More than one space will be eaten" 200
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: `block {
|
||||||
|
heredoc <<HEREDOC
|
||||||
|
Here's more than one space Here's more than one space
|
||||||
|
HEREDOC
|
||||||
|
respond "More than one space will be eaten" 200
|
||||||
|
}
|
||||||
|
|
||||||
|
block2 {
|
||||||
|
heredoc <<HEREDOC
|
||||||
|
Here's more than one space Here's more than one space
|
||||||
|
HEREDOC
|
||||||
|
respond "More than one space will be eaten" 200
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Heredoc as regular token",
|
||||||
|
input: `block {
|
||||||
|
heredoc <<HEREDOC "More than one space will be eaten"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: `block {
|
||||||
|
heredoc <<HEREDOC "More than one space will be eaten"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Escape heredoc",
|
||||||
|
input: `block {
|
||||||
|
heredoc \<<HEREDOC
|
||||||
|
respond "More than one space will be eaten" 200
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: `block {
|
||||||
|
heredoc \<<HEREDOC
|
||||||
|
respond "More than one space will be eaten" 200
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// 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 (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseVariadic determines if the token is a variadic placeholder,
|
||||||
|
// and if so, determines the index range (start/end) of args to use.
|
||||||
|
// Returns a boolean signaling whether a variadic placeholder was found,
|
||||||
|
// and the start and end indices.
|
||||||
|
func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||||
|
if !strings.HasPrefix(token.Text, "{args[") {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(token.Text, "]}") {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
argRange := strings.TrimSuffix(strings.TrimPrefix(token.Text, "{args["), "]}")
|
||||||
|
if argRange == "" {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Placeholder "+token.Text+" cannot have an empty index",
|
||||||
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end, found := strings.Cut(argRange, ":")
|
||||||
|
|
||||||
|
// If no ":" delimiter is found, this is not a variadic.
|
||||||
|
// The replacer will pick this up.
|
||||||
|
if !found {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valid token may contain several placeholders, and
|
||||||
|
// they may be separated by ":". It's not variadic.
|
||||||
|
// https://github.com/caddyserver/caddy/issues/5716
|
||||||
|
if strings.Contains(start, "}") || strings.Contains(end, "{") {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
startIndex = 0
|
||||||
|
endIndex = argCount
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if start != "" {
|
||||||
|
startIndex, err = strconv.Atoi(start)
|
||||||
|
if err != nil {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Variadic placeholder "+token.Text+" has an invalid start index",
|
||||||
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end != "" {
|
||||||
|
endIndex, err = strconv.Atoi(end)
|
||||||
|
if err != nil {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Variadic placeholder "+token.Text+" has an invalid end index",
|
||||||
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bound check
|
||||||
|
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
|
||||||
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
return true, startIndex, endIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeArgsReplacer prepares a Replacer which can replace
|
||||||
|
// non-variadic args placeholders in imported tokens.
|
||||||
|
func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||||
|
repl := caddy.NewEmptyReplacer()
|
||||||
|
repl.Map(func(key string) (any, bool) {
|
||||||
|
// TODO: Remove the deprecated {args.*} placeholder
|
||||||
|
// support at some point in the future
|
||||||
|
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
|
||||||
|
// What's matched may be a substring of the key
|
||||||
|
if matches[0] != key {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := strconv.Atoi(matches[1])
|
||||||
|
if err != nil {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Placeholder {args." + matches[1] + "} has an invalid index")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if value >= len(args) {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Placeholder {args." + matches[1] + "} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Placeholder {args." + matches[1] + "} deprecated, use {args[" + matches[1] + "]} instead")
|
||||||
|
return args[value], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle args[*] form
|
||||||
|
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
|
||||||
|
// What's matched may be a substring of the key
|
||||||
|
if matches[0] != key {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(matches[1], ":") {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
value, err := strconv.Atoi(matches[1])
|
||||||
|
if err != nil {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Placeholder {args[" + matches[1] + "]} has an invalid index")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if value >= len(args) {
|
||||||
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
|
"Placeholder {args[" + matches[1] + "]} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return args[value], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not an args placeholder, ignore
|
||||||
|
return nil, false
|
||||||
|
})
|
||||||
|
return repl
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
argsRegexpIndexDeprecated = regexp.MustCompile(`args\.(.+)`)
|
||||||
|
argsRegexpIndex = regexp.MustCompile(`args\[(.+)]`)
|
||||||
|
)
|
||||||
@@ -16,24 +16,26 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adjacency map[string][]string
|
type adjacency map[string][]string
|
||||||
|
|
||||||
type importGraph struct {
|
type importGraph struct {
|
||||||
nodes map[string]bool
|
nodes map[string]struct{}
|
||||||
edges adjacency
|
edges adjacency
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) addNode(name string) {
|
func (i *importGraph) addNode(name string) {
|
||||||
if i.nodes == nil {
|
if i.nodes == nil {
|
||||||
i.nodes = make(map[string]bool)
|
i.nodes = make(map[string]struct{})
|
||||||
}
|
}
|
||||||
if _, exists := i.nodes[name]; exists {
|
if _, exists := i.nodes[name]; exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
i.nodes[name] = true
|
i.nodes[name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) addNodes(names []string) {
|
func (i *importGraph) addNodes(names []string) {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
i.addNode(name)
|
i.addNode(name)
|
||||||
@@ -43,6 +45,7 @@ func (i *importGraph) addNodes(names []string) {
|
|||||||
func (i *importGraph) removeNode(name string) {
|
func (i *importGraph) removeNode(name string) {
|
||||||
delete(i.nodes, name)
|
delete(i.nodes, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) removeNodes(names []string) {
|
func (i *importGraph) removeNodes(names []string) {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
i.removeNode(name)
|
i.removeNode(name)
|
||||||
@@ -64,7 +67,7 @@ func (i *importGraph) addEdge(from, to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i.nodes == nil {
|
if i.nodes == nil {
|
||||||
i.nodes = make(map[string]bool)
|
i.nodes = make(map[string]struct{})
|
||||||
}
|
}
|
||||||
if i.edges == nil {
|
if i.edges == nil {
|
||||||
i.edges = make(adjacency)
|
i.edges = make(adjacency)
|
||||||
@@ -73,6 +76,7 @@ func (i *importGraph) addEdge(from, to string) error {
|
|||||||
i.edges[from] = append(i.edges[from], to)
|
i.edges[from] = append(i.edges[from], to)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) addEdges(from string, tos []string) error {
|
func (i *importGraph) addEdges(from string, tos []string) error {
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
err := i.addEdge(from, to)
|
err := i.addEdge(from, to)
|
||||||
@@ -88,12 +92,7 @@ func (i *importGraph) areConnected(from, to string) bool {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, v := range al {
|
return slices.Contains(al, to)
|
||||||
if v == to {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) willCycle(from, to string) bool {
|
func (i *importGraph) willCycle(from, to string) bool {
|
||||||
|
|||||||
Executable → Regular
+241
-33
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -17,7 +17,10 @@ package caddyfile
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,14 +38,41 @@ type (
|
|||||||
|
|
||||||
// Token represents a single parsable unit.
|
// Token represents a single parsable unit.
|
||||||
Token struct {
|
Token struct {
|
||||||
File string
|
File string
|
||||||
Line int
|
imports []string
|
||||||
Text string
|
Line int
|
||||||
inSnippet bool
|
Text string
|
||||||
snippetName string
|
wasQuoted rune // enclosing quote character, if any
|
||||||
|
heredocMarker string
|
||||||
|
snippetName string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tokenize takes bytes as input and lexes it into
|
||||||
|
// a list of tokens that can be parsed as a Caddyfile.
|
||||||
|
// Also takes a filename to fill the token's File as
|
||||||
|
// the source of the tokens, which is important to
|
||||||
|
// determine relative paths for `import` directives.
|
||||||
|
func Tokenize(input []byte, filename string) ([]Token, error) {
|
||||||
|
l := lexer{}
|
||||||
|
if err := l.load(bytes.NewReader(input)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tokens []Token
|
||||||
|
for {
|
||||||
|
found, err := l.next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l.token.File = filename
|
||||||
|
tokens = append(tokens, l.token)
|
||||||
|
}
|
||||||
|
return tokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
// load prepares the lexer to scan an input for tokens.
|
// load prepares the lexer to scan an input for tokens.
|
||||||
// It discards any leading byte order mark.
|
// It discards any leading byte order mark.
|
||||||
func (l *lexer) load(input io.Reader) error {
|
func (l *lexer) load(input io.Reader) error {
|
||||||
@@ -74,27 +104,107 @@ func (l *lexer) load(input io.Reader) error {
|
|||||||
// may be escaped. The rest of the line is skipped
|
// may be escaped. The rest of the line is skipped
|
||||||
// if a "#" character is read in. Returns true if
|
// if a "#" character is read in. Returns true if
|
||||||
// a token was loaded; false otherwise.
|
// a token was loaded; false otherwise.
|
||||||
func (l *lexer) next() bool {
|
func (l *lexer) next() (bool, error) {
|
||||||
var val []rune
|
var val []rune
|
||||||
var comment, quoted, btQuoted, escaped bool
|
var comment, quoted, btQuoted, inHeredoc, heredocEscaped, escaped bool
|
||||||
|
var heredocMarker string
|
||||||
|
|
||||||
makeToken := func() bool {
|
makeToken := func(quoted rune) bool {
|
||||||
l.token.Text = string(val)
|
l.token.Text = string(val)
|
||||||
|
l.token.wasQuoted = quoted
|
||||||
|
l.token.heredocMarker = heredocMarker
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
// Read a character in; if err then if we had
|
||||||
|
// read some characters, make a token. If we
|
||||||
|
// reached EOF, then no more tokens to read.
|
||||||
|
// If no EOF, then we had a problem.
|
||||||
ch, _, err := l.reader.ReadRune()
|
ch, _, err := l.reader.ReadRune()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(val) > 0 {
|
if len(val) > 0 {
|
||||||
return makeToken()
|
if inHeredoc {
|
||||||
|
return false, fmt.Errorf("incomplete heredoc <<%s on line #%d, expected ending marker %s", heredocMarker, l.line+l.skippedLines, heredocMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeToken(0), nil
|
||||||
}
|
}
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
panic(err)
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detect whether we have the start of a heredoc
|
||||||
|
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
|
||||||
|
len(val) > 1 && string(val[:2]) == "<<" {
|
||||||
|
// a space means it's just a regular token and not a heredoc
|
||||||
|
if ch == ' ' {
|
||||||
|
return makeToken(0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip CR, we only care about LF
|
||||||
|
if ch == '\r' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// after hitting a newline, we know that the heredoc marker
|
||||||
|
// is the characters after the two << and the newline.
|
||||||
|
// we reset the val because the heredoc is syntax we don't
|
||||||
|
// want to keep.
|
||||||
|
if ch == '\n' {
|
||||||
|
if len(val) == 2 {
|
||||||
|
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there's too many <
|
||||||
|
if string(val[:3]) == "<<<" {
|
||||||
|
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
heredocMarker = string(val[2:])
|
||||||
|
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
||||||
|
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
inHeredoc = true
|
||||||
|
l.skippedLines++
|
||||||
|
val = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = append(val, ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're in a heredoc, all characters are read as-is
|
||||||
|
if inHeredoc {
|
||||||
|
val = append(val, ch)
|
||||||
|
|
||||||
|
if ch == '\n' {
|
||||||
|
l.skippedLines++
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we're done, i.e. that the last few characters are the marker
|
||||||
|
if len(val) >= len(heredocMarker) && heredocMarker == string(val[len(val)-len(heredocMarker):]) {
|
||||||
|
// set the final value
|
||||||
|
val, err = l.finalizeHeredoc(val, heredocMarker)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the line counter, and make the token
|
||||||
|
l.line += l.skippedLines
|
||||||
|
l.skippedLines = 0
|
||||||
|
return makeToken('<'), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stay in the heredoc until we find the ending marker
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// track whether we found an escape '\' for the next
|
||||||
|
// iteration to be contextually aware
|
||||||
if !escaped && !btQuoted && ch == '\\' {
|
if !escaped && !btQuoted && ch == '\\' {
|
||||||
escaped = true
|
escaped = true
|
||||||
continue
|
continue
|
||||||
@@ -109,26 +219,29 @@ func (l *lexer) next() bool {
|
|||||||
}
|
}
|
||||||
escaped = false
|
escaped = false
|
||||||
} else {
|
} else {
|
||||||
if quoted && ch == '"' {
|
if (quoted && ch == '"') || (btQuoted && ch == '`') {
|
||||||
return makeToken()
|
return makeToken(ch), nil
|
||||||
}
|
|
||||||
if btQuoted && ch == '`' {
|
|
||||||
return makeToken()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// allow quoted text to wrap continue on multiple lines
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
l.line += 1 + l.skippedLines
|
l.line += 1 + l.skippedLines
|
||||||
l.skippedLines = 0
|
l.skippedLines = 0
|
||||||
}
|
}
|
||||||
|
// collect this character as part of the quoted token
|
||||||
val = append(val, ch)
|
val = append(val, ch)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
|
// ignore CR altogether, we only actually care about LF (\n)
|
||||||
if ch == '\r' {
|
if ch == '\r' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// end of the line
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
|
// newlines can be escaped to chain arguments
|
||||||
|
// onto multiple lines; else, increment the line count
|
||||||
if escaped {
|
if escaped {
|
||||||
l.skippedLines++
|
l.skippedLines++
|
||||||
escaped = false
|
escaped = false
|
||||||
@@ -136,14 +249,18 @@ func (l *lexer) next() bool {
|
|||||||
l.line += 1 + l.skippedLines
|
l.line += 1 + l.skippedLines
|
||||||
l.skippedLines = 0
|
l.skippedLines = 0
|
||||||
}
|
}
|
||||||
|
// comments (#) are single-line only
|
||||||
comment = false
|
comment = false
|
||||||
}
|
}
|
||||||
|
// any kind of space means we're at the end of this token
|
||||||
if len(val) > 0 {
|
if len(val) > 0 {
|
||||||
return makeToken()
|
return makeToken(0), nil
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// comments must be at the start of a token,
|
||||||
|
// in other words, preceded by space or newline
|
||||||
if ch == '#' && len(val) == 0 {
|
if ch == '#' && len(val) == 0 {
|
||||||
comment = true
|
comment = true
|
||||||
}
|
}
|
||||||
@@ -164,7 +281,12 @@ func (l *lexer) next() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if escaped {
|
if escaped {
|
||||||
val = append(val, '\\')
|
// allow escaping the first < to skip the heredoc syntax
|
||||||
|
if ch == '<' {
|
||||||
|
heredocEscaped = true
|
||||||
|
} else {
|
||||||
|
val = append(val, '\\')
|
||||||
|
}
|
||||||
escaped = false
|
escaped = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,20 +294,106 @@ func (l *lexer) next() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokenize takes bytes as input and lexes it into
|
// finalizeHeredoc takes the runes read as the heredoc text and the marker,
|
||||||
// a list of tokens that can be parsed as a Caddyfile.
|
// and processes the text to strip leading whitespace, returning the final
|
||||||
// Also takes a filename to fill the token's File as
|
// value without the leading whitespace.
|
||||||
// the source of the tokens, which is important to
|
func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
|
||||||
// determine relative paths for `import` directives.
|
stringVal := string(val)
|
||||||
func Tokenize(input []byte, filename string) ([]Token, error) {
|
|
||||||
l := lexer{}
|
// find the last newline of the heredoc, which is where the contents end
|
||||||
if err := l.load(bytes.NewReader(input)); err != nil {
|
lastNewline := strings.LastIndex(stringVal, "\n")
|
||||||
return nil, err
|
|
||||||
|
// collapse the content, then split into separate lines
|
||||||
|
lines := strings.Split(stringVal[:lastNewline+1], "\n")
|
||||||
|
|
||||||
|
// figure out how much whitespace we need to strip from the front of every line
|
||||||
|
// by getting the string that precedes the marker, on the last line
|
||||||
|
paddingToStrip := stringVal[lastNewline+1 : len(stringVal)-len(marker)]
|
||||||
|
|
||||||
|
// iterate over each line and strip the whitespace from the front
|
||||||
|
var out string
|
||||||
|
for lineNum, lineText := range lines[:len(lines)-1] {
|
||||||
|
if lineText == "" || lineText == "\r" {
|
||||||
|
out += "\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// find an exact match for the padding
|
||||||
|
index := strings.Index(lineText, paddingToStrip)
|
||||||
|
|
||||||
|
// if the padding doesn't match exactly at the start then we can't safely strip
|
||||||
|
if index != 0 {
|
||||||
|
return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, lineText, paddingToStrip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip, then append the line, with the newline, to the output.
|
||||||
|
// also removes all "\r" because Windows.
|
||||||
|
out += strings.ReplaceAll(lineText[len(paddingToStrip):]+"\n", "\r", "")
|
||||||
}
|
}
|
||||||
var tokens []Token
|
|
||||||
for l.next() {
|
// Remove the trailing newline from the loop
|
||||||
l.token.File = filename
|
if len(out) > 0 && out[len(out)-1] == '\n' {
|
||||||
tokens = append(tokens, l.token)
|
out = out[:len(out)-1]
|
||||||
}
|
}
|
||||||
return tokens, nil
|
|
||||||
|
// return the final value
|
||||||
|
return []rune(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quoted returns true if the token was enclosed in quotes
|
||||||
|
// (i.e. double quotes, backticks, or heredoc).
|
||||||
|
func (t Token) Quoted() bool {
|
||||||
|
return t.wasQuoted > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumLineBreaks counts how many line breaks are in the token text.
|
||||||
|
func (t Token) NumLineBreaks() int {
|
||||||
|
lineBreaks := strings.Count(t.Text, "\n")
|
||||||
|
if t.wasQuoted == '<' {
|
||||||
|
// heredocs have an extra linebreak because the opening
|
||||||
|
// delimiter is on its own line and is not included in the
|
||||||
|
// token Text itself, and the trailing newline is removed.
|
||||||
|
lineBreaks += 2
|
||||||
|
}
|
||||||
|
return lineBreaks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a deep copy of the token.
|
||||||
|
func (t Token) Clone() Token {
|
||||||
|
return Token{
|
||||||
|
File: t.File,
|
||||||
|
imports: append([]string{}, t.imports...),
|
||||||
|
Line: t.Line,
|
||||||
|
Text: t.Text,
|
||||||
|
wasQuoted: t.wasQuoted,
|
||||||
|
heredocMarker: t.heredocMarker,
|
||||||
|
snippetName: t.snippetName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
|
||||||
|
|
||||||
|
// isNextOnNewLine tests whether t2 is on a different line from t1
|
||||||
|
func isNextOnNewLine(t1, t2 Token) bool {
|
||||||
|
// If the second token is from a different file,
|
||||||
|
// we can assume it's from a different line
|
||||||
|
if t1.File != t2.File {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the second token is from a different import chain,
|
||||||
|
// we can assume it's from a different line
|
||||||
|
if len(t1.imports) != len(t2.imports) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, im := range t1.imports {
|
||||||
|
if im != t2.imports[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the first token (incl line breaks) ends
|
||||||
|
// on a line earlier than the next token,
|
||||||
|
// then the second token is on a new line
|
||||||
|
return t1.Line+t1.NumLineBreaks() < t2.Line
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// +build gofuzz
|
//go:build gofuzz
|
||||||
|
|
||||||
package caddyfile
|
package caddyfile
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
+272
-11
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -18,13 +18,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type lexerTestCase struct {
|
|
||||||
input []byte
|
|
||||||
expected []Token
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLexer(t *testing.T) {
|
func TestLexer(t *testing.T) {
|
||||||
testCases := []lexerTestCase{
|
testCases := []struct {
|
||||||
|
input []byte
|
||||||
|
expected []Token
|
||||||
|
expectErr bool
|
||||||
|
errorMessage string
|
||||||
|
}{
|
||||||
{
|
{
|
||||||
input: []byte(`host:123`),
|
input: []byte(`host:123`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
@@ -249,12 +249,273 @@ func TestLexer(t *testing.T) {
|
|||||||
{Line: 1, Text: `quotes`},
|
{Line: 1, Text: `quotes`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
content
|
||||||
|
EOF same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: "content"},
|
||||||
|
{Line: 3, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<VERY-LONG-MARKER
|
||||||
|
content
|
||||||
|
VERY-LONG-MARKER same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: "content"},
|
||||||
|
{Line: 3, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
extra-newline
|
||||||
|
|
||||||
|
EOF same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: "extra-newline\n"},
|
||||||
|
{Line: 4, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
EOF
|
||||||
|
HERE same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: ``},
|
||||||
|
{Line: 3, Text: `HERE`},
|
||||||
|
{Line: 3, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
EOF same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: ""},
|
||||||
|
{Line: 2, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
content
|
||||||
|
EOF same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: "content"},
|
||||||
|
{Line: 3, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`prev-line
|
||||||
|
heredoc <<EOF
|
||||||
|
multi
|
||||||
|
line
|
||||||
|
content
|
||||||
|
EOF same-line-arg
|
||||||
|
next-line
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `prev-line`},
|
||||||
|
{Line: 2, Text: `heredoc`},
|
||||||
|
{Line: 2, Text: "\tmulti\n\tline\n\tcontent"},
|
||||||
|
{Line: 6, Text: `same-line-arg`},
|
||||||
|
{Line: 7, Text: `next-line`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`escaped-heredoc \<< >>`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `escaped-heredoc`},
|
||||||
|
{Line: 1, Text: `<<`},
|
||||||
|
{Line: 1, Text: `>>`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc <EOF
|
||||||
|
content
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<EOF`},
|
||||||
|
{Line: 2, Text: `content`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc <<<EOF content`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<<EOF`},
|
||||||
|
{Line: 1, Text: `content`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc "<<" ">>"`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<`},
|
||||||
|
{Line: 1, Text: `>>`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc << >>`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<`},
|
||||||
|
{Line: 1, Text: `>>`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc <<HERE SAME LINE
|
||||||
|
content
|
||||||
|
HERE same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<HERE`},
|
||||||
|
{Line: 1, Text: `SAME`},
|
||||||
|
{Line: 1, Text: `LINE`},
|
||||||
|
{Line: 2, Text: `content`},
|
||||||
|
{Line: 3, Text: `HERE`},
|
||||||
|
{Line: 3, Text: `same-line-arg`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<s
|
||||||
|
�
|
||||||
|
s
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `heredoc`},
|
||||||
|
{Line: 1, Text: "�"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte("\u000Aheredoc \u003C\u003C\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F"),
|
||||||
|
expected: []Token{
|
||||||
|
{
|
||||||
|
Line: 2,
|
||||||
|
Text: "heredoc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Line: 2,
|
||||||
|
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Line: 5,
|
||||||
|
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Line: 6,
|
||||||
|
Text: "\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte("not-a-heredoc <<\n"),
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<<EOF
|
||||||
|
content
|
||||||
|
EOF same-line-arg
|
||||||
|
`),
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "too many '<' for heredoc on line #1; only use two, for example <<END",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
content
|
||||||
|
`),
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "incomplete heredoc <<EOF on line #3, expected ending marker EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
content
|
||||||
|
EOF
|
||||||
|
`),
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [\tcontent], expected whitespace [\t\t] to match the closing marker",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
content
|
||||||
|
EOF
|
||||||
|
`),
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [ content], expected whitespace [\t\t] to match the closing marker",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
The next line is a blank line
|
||||||
|
|
||||||
|
The previous line is a blank line
|
||||||
|
EOF`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: "heredoc"},
|
||||||
|
{Line: 1, Text: "The next line is a blank line\n\nThe previous line is a blank line"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
One tab indented heredoc with blank next line
|
||||||
|
|
||||||
|
One tab indented heredoc with blank previous line
|
||||||
|
EOF`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: "heredoc"},
|
||||||
|
{Line: 1, Text: "One tab indented heredoc with blank next line\n\nOne tab indented heredoc with blank previous line"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
The next line is a blank line with one tab
|
||||||
|
|
||||||
|
The previous line is a blank line with one tab
|
||||||
|
EOF`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: "heredoc"},
|
||||||
|
{Line: 1, Text: "The next line is a blank line with one tab\n\t\nThe previous line is a blank line with one tab"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`heredoc <<EOF
|
||||||
|
The next line is a blank line with one tab less than the correct indentation
|
||||||
|
|
||||||
|
The previous line is a blank line with one tab less than the correct indentation
|
||||||
|
EOF`),
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #3 [\t], expected whitespace [\t\t] to match the closing marker",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
actual, err := Tokenize(testCase.input, "")
|
actual, err := Tokenize(testCase.input, "")
|
||||||
|
if testCase.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got actual: %v", actual)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err.Error() != testCase.errorMessage {
|
||||||
|
t.Fatalf("expected error '%v', got: %v", testCase.errorMessage, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%v", err)
|
t.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
lexerCompare(t, i, testCase.expected, actual)
|
lexerCompare(t, i, testCase.expected, actual)
|
||||||
}
|
}
|
||||||
@@ -262,17 +523,17 @@ func TestLexer(t *testing.T) {
|
|||||||
|
|
||||||
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
||||||
if len(expected) != len(actual) {
|
if len(expected) != len(actual) {
|
||||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
t.Fatalf("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++ {
|
for i := 0; i < len(actual) && i < len(expected); i++ {
|
||||||
if actual[i].Line != expected[i].Line {
|
if actual[i].Line != expected[i].Line {
|
||||||
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
t.Fatalf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
||||||
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
|
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if actual[i].Text != expected[i].Text {
|
if actual[i].Text != expected[i].Text {
|
||||||
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
|
t.Fatalf("Test case %d token %d: expected text '%s' but was '%s'",
|
||||||
n, i, expected[i].Text, actual[i].Text)
|
n, i, expected[i].Text, actual[i].Text)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+266
-70
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -17,13 +17,13 @@ package caddyfile
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,22 +37,36 @@ import (
|
|||||||
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
||||||
// will be replaced before parsing begins.
|
// will be replaced before parsing begins.
|
||||||
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
||||||
tokens, err := allTokens(filename, input)
|
// unfortunately, we must copy the input because parsing must
|
||||||
|
// remain a read-only operation, but we have to expand environment
|
||||||
|
// variables before we parse, which changes the underlying array (#4422)
|
||||||
|
inputCopy := make([]byte, len(input))
|
||||||
|
copy(inputCopy, input)
|
||||||
|
|
||||||
|
tokens, err := allTokens(filename, inputCopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p := parser{
|
p := parser{
|
||||||
Dispenser: NewDispenser(tokens),
|
Dispenser: NewDispenser(tokens),
|
||||||
importGraph: importGraph{
|
importGraph: importGraph{
|
||||||
nodes: make(map[string]bool),
|
nodes: make(map[string]struct{}),
|
||||||
edges: make(adjacency),
|
edges: make(adjacency),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return p.parseAll()
|
return p.parseAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allTokens lexes the entire input, but does not parse it.
|
||||||
|
// It returns all the tokens from the input, unstructured
|
||||||
|
// and in order. It may mutate input as it expands env vars.
|
||||||
|
func allTokens(filename string, input []byte) ([]Token, error) {
|
||||||
|
return Tokenize(replaceEnvVars(input), filename)
|
||||||
|
}
|
||||||
|
|
||||||
// replaceEnvVars replaces all occurrences of environment variables.
|
// replaceEnvVars replaces all occurrences of environment variables.
|
||||||
func replaceEnvVars(input []byte) ([]byte, error) {
|
// It mutates the underlying array and returns the updated slice.
|
||||||
|
func replaceEnvVars(input []byte) []byte {
|
||||||
var offset int
|
var offset int
|
||||||
for {
|
for {
|
||||||
begin := bytes.Index(input[offset:], spanOpen)
|
begin := bytes.Index(input[offset:], spanOpen)
|
||||||
@@ -93,22 +107,7 @@ func replaceEnvVars(input []byte) ([]byte, error) {
|
|||||||
// continue at the end of the replacement
|
// continue at the end of the replacement
|
||||||
offset = begin + len(envVarBytes)
|
offset = begin + len(envVarBytes)
|
||||||
}
|
}
|
||||||
return input, nil
|
return input
|
||||||
}
|
|
||||||
|
|
||||||
// allTokens lexes the entire input, but does not parse it.
|
|
||||||
// It returns all the tokens from the input, unstructured
|
|
||||||
// and in order.
|
|
||||||
func allTokens(filename string, input []byte) ([]Token, error) {
|
|
||||||
input, err := replaceEnvVars(input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tokens, err := Tokenize(input, filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tokens, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
@@ -150,7 +149,6 @@ func (p *parser) begin() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := p.addresses()
|
err := p.addresses()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -161,6 +159,25 @@ func (p *parser) begin() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ok, name := p.isNamedRoute(); ok {
|
||||||
|
// we just need a dummy leading token to ease parsing later
|
||||||
|
nameToken := p.Token()
|
||||||
|
nameToken.Text = name
|
||||||
|
|
||||||
|
// named routes only have one key, the route name
|
||||||
|
p.block.Keys = []Token{nameToken}
|
||||||
|
p.block.IsNamedRoute = true
|
||||||
|
|
||||||
|
// get all the tokens from the block, including the braces
|
||||||
|
tokens, err := p.blockTokens(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tokens = append([]Token{nameToken}, tokens...)
|
||||||
|
p.block.Segments = []Segment{tokens}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if ok, name := p.isSnippet(); ok {
|
if ok, name := p.isSnippet(); ok {
|
||||||
if p.definedSnippets == nil {
|
if p.definedSnippets == nil {
|
||||||
p.definedSnippets = map[string][]Token{}
|
p.definedSnippets = map[string][]Token{}
|
||||||
@@ -169,16 +186,15 @@ func (p *parser) begin() error {
|
|||||||
return p.Errf("redeclaration of previously declared snippet %s", name)
|
return p.Errf("redeclaration of previously declared snippet %s", name)
|
||||||
}
|
}
|
||||||
// consume all tokens til matched close brace
|
// consume all tokens til matched close brace
|
||||||
tokens, err := p.snippetTokens()
|
tokens, err := p.blockTokens(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Just as we need to track which file the token comes from, we need to
|
// Just as we need to track which file the token comes from, we need to
|
||||||
// keep track of which snippets do the tokens come from. This is helpful
|
// keep track of which snippet the token comes from. This is helpful
|
||||||
// in tracking import cycles across files/snippets by namespacing them. Without
|
// in tracking import cycles across files/snippets by namespacing them.
|
||||||
// this we end up with false-positives in cycle-detection.
|
// Without this, we end up with false-positives in cycle-detection.
|
||||||
for k, v := range tokens {
|
for k, v := range tokens {
|
||||||
v.inSnippet = true
|
|
||||||
v.snippetName = name
|
v.snippetName = name
|
||||||
tokens[k] = v
|
tokens[k] = v
|
||||||
}
|
}
|
||||||
@@ -195,11 +211,17 @@ func (p *parser) addresses() error {
|
|||||||
var expectingAnother bool
|
var expectingAnother bool
|
||||||
|
|
||||||
for {
|
for {
|
||||||
tkn := p.Val()
|
value := p.Val()
|
||||||
|
token := p.Token()
|
||||||
|
|
||||||
// special case: import directive replaces tokens during parse-time
|
// Reject request matchers if trying to define them globally
|
||||||
if tkn == "import" && p.isNewLine() {
|
if strings.HasPrefix(value, "@") {
|
||||||
err := p.doImport()
|
return p.Errf("request matchers may not be defined globally, they must be in a site block; found %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: import directive replaces tokens during parse-time
|
||||||
|
if value == "import" && p.isNewLine() {
|
||||||
|
err := p.doImport(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -207,9 +229,9 @@ func (p *parser) addresses() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open brace definitely indicates end of addresses
|
// Open brace definitely indicates end of addresses
|
||||||
if tkn == "{" {
|
if value == "{" {
|
||||||
if expectingAnother {
|
if expectingAnother {
|
||||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
return p.Errf("Expected another address but had '%s' - check for extra comma", value)
|
||||||
}
|
}
|
||||||
// Mark this server block as being defined with braces.
|
// Mark this server block as being defined with braces.
|
||||||
// This is used to provide a better error message when
|
// This is used to provide a better error message when
|
||||||
@@ -221,15 +243,15 @@ func (p *parser) addresses() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Users commonly forget to place a space between the address and the '{'
|
// Users commonly forget to place a space between the address and the '{'
|
||||||
if strings.HasSuffix(tkn, "{") {
|
if strings.HasSuffix(value, "{") {
|
||||||
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn)
|
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tkn != "" { // empty token possible if user typed ""
|
if value != "" { // empty token possible if user typed ""
|
||||||
// Trailing comma indicates another address will follow, which
|
// Trailing comma indicates another address will follow, which
|
||||||
// may possibly be on the next line
|
// may possibly be on the next line
|
||||||
if tkn[len(tkn)-1] == ',' {
|
if value[len(value)-1] == ',' {
|
||||||
tkn = tkn[:len(tkn)-1]
|
value = value[:len(value)-1]
|
||||||
expectingAnother = true
|
expectingAnother = true
|
||||||
} else {
|
} else {
|
||||||
expectingAnother = false // but we may still see another one on this line
|
expectingAnother = false // but we may still see another one on this line
|
||||||
@@ -238,11 +260,17 @@ func (p *parser) addresses() error {
|
|||||||
// If there's a comma here, it's probably because they didn't use a space
|
// If there's a comma here, it's probably because they didn't use a space
|
||||||
// between their two domains, e.g. "foo.com,bar.com", which would not be
|
// between their two domains, e.g. "foo.com,bar.com", which would not be
|
||||||
// parsed as two separate site addresses.
|
// parsed as two separate site addresses.
|
||||||
if strings.Contains(tkn, ",") {
|
if strings.Contains(value, ",") {
|
||||||
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn)
|
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.block.Keys = append(p.block.Keys, tkn)
|
// After the above, a comma surrounded by spaces would result
|
||||||
|
// in an empty token which we should ignore
|
||||||
|
if value != "" {
|
||||||
|
// Add the token as a site address
|
||||||
|
token.Text = value
|
||||||
|
p.block.Keys = append(p.block.Keys, token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance token and possibly break out of loop or return error
|
// Advance token and possibly break out of loop or return error
|
||||||
@@ -299,7 +327,7 @@ func (p *parser) directives() error {
|
|||||||
|
|
||||||
// special case: import directive replaces tokens during parse-time
|
// special case: import directive replaces tokens during parse-time
|
||||||
if p.Val() == "import" {
|
if p.Val() == "import" {
|
||||||
err := p.doImport()
|
err := p.doImport(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -325,7 +353,7 @@ func (p *parser) directives() error {
|
|||||||
// is on the token before where the import directive was. In
|
// is on the token before where the import directive was. In
|
||||||
// other words, call Next() to access the first token that was
|
// other words, call Next() to access the first token that was
|
||||||
// imported.
|
// imported.
|
||||||
func (p *parser) doImport() error {
|
func (p *parser) doImport(nesting int) error {
|
||||||
// syntax checks
|
// syntax checks
|
||||||
if !p.NextArg() {
|
if !p.NextArg() {
|
||||||
return p.ArgErr()
|
return p.ArgErr()
|
||||||
@@ -338,15 +366,48 @@ func (p *parser) doImport() error {
|
|||||||
// grab remaining args as placeholder replacements
|
// grab remaining args as placeholder replacements
|
||||||
args := p.RemainingArgs()
|
args := p.RemainingArgs()
|
||||||
|
|
||||||
// add args to the replacer
|
// set up a replacer for non-variadic args replacement
|
||||||
repl := caddy.NewEmptyReplacer()
|
repl := makeArgsReplacer(args)
|
||||||
for index, arg := range args {
|
|
||||||
repl.Set("args."+strconv.Itoa(index), arg)
|
// grab all the tokens (if it exists) from within a block that follows the import
|
||||||
|
var blockTokens []Token
|
||||||
|
for currentNesting := p.Nesting(); p.NextBlock(currentNesting); {
|
||||||
|
blockTokens = append(blockTokens, p.Token())
|
||||||
|
}
|
||||||
|
// initialize with size 1
|
||||||
|
blockMapping := make(map[string][]Token, 1)
|
||||||
|
if len(blockTokens) > 0 {
|
||||||
|
// use such tokens to create a new dispenser, and then use it to parse each block
|
||||||
|
bd := NewDispenser(blockTokens)
|
||||||
|
for bd.Next() {
|
||||||
|
// see if we can grab a key
|
||||||
|
var currentMappingKey string
|
||||||
|
if bd.Val() == "{" {
|
||||||
|
return p.Err("anonymous blocks are not supported")
|
||||||
|
}
|
||||||
|
currentMappingKey = bd.Val()
|
||||||
|
currentMappingTokens := []Token{}
|
||||||
|
// read all args until end of line / {
|
||||||
|
if bd.NextArg() {
|
||||||
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||||
|
for bd.NextArg() {
|
||||||
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||||
|
}
|
||||||
|
// TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
|
||||||
|
// maybe someone can do that in the future
|
||||||
|
} else {
|
||||||
|
// attempt to enter a block and add tokens to the currentMappingTokens
|
||||||
|
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
|
||||||
|
currentMappingTokens = append(currentMappingTokens, bd.Token())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockMapping[currentMappingKey] = currentMappingTokens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// splice out the import directive and its arguments
|
// splice out the import directive and its arguments
|
||||||
// (2 tokens, plus the length of args)
|
// (2 tokens, plus the length of args)
|
||||||
tokensBefore := p.tokens[:p.cursor-1-len(args)]
|
tokensBefore := p.tokens[:p.cursor-1-len(args)-len(blockTokens)]
|
||||||
tokensAfter := p.tokens[p.cursor+1:]
|
tokensAfter := p.tokens[p.cursor+1:]
|
||||||
var importedTokens []Token
|
var importedTokens []Token
|
||||||
var nodes []string
|
var nodes []string
|
||||||
@@ -380,16 +441,29 @@ func (p *parser) doImport() error {
|
|||||||
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
||||||
}
|
}
|
||||||
matches, err = filepath.Glob(globPattern)
|
matches, err = filepath.Glob(globPattern)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||||
}
|
}
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
if strings.ContainsAny(globPattern, "*?[]") {
|
if strings.ContainsAny(globPattern, "*?[]") {
|
||||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern))
|
||||||
} else {
|
} else {
|
||||||
return p.Errf("File to import not found: %s", importPattern)
|
return p.Errf("File to import not found: %s", importPattern)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// See issue #5295 - should skip any files that start with a . when iterating over them.
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
segGlobPattern := strings.Split(globPattern, sep)
|
||||||
|
if strings.HasPrefix(segGlobPattern[len(segGlobPattern)-1], "*") {
|
||||||
|
var tmpMatches []string
|
||||||
|
for _, m := range matches {
|
||||||
|
seg := strings.Split(m, sep)
|
||||||
|
if !strings.HasPrefix(seg[len(seg)-1], ".") {
|
||||||
|
tmpMatches = append(tmpMatches, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches = tmpMatches
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect all the imported tokens
|
// collect all the imported tokens
|
||||||
@@ -404,7 +478,7 @@ func (p *parser) doImport() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nodeName := p.File()
|
nodeName := p.File()
|
||||||
if p.Token().inSnippet {
|
if p.Token().snippetName != "" {
|
||||||
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
||||||
}
|
}
|
||||||
p.importGraph.addNode(nodeName)
|
p.importGraph.addNode(nodeName)
|
||||||
@@ -415,19 +489,102 @@ func (p *parser) doImport() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copy the tokens so we don't overwrite p.definedSnippets
|
// copy the tokens so we don't overwrite p.definedSnippets
|
||||||
tokensCopy := make([]Token, len(importedTokens))
|
tokensCopy := make([]Token, 0, len(importedTokens))
|
||||||
copy(tokensCopy, importedTokens)
|
|
||||||
|
var (
|
||||||
|
maybeSnippet bool
|
||||||
|
maybeSnippetId bool
|
||||||
|
index int
|
||||||
|
)
|
||||||
|
|
||||||
// run the argument replacer on the tokens
|
// run the argument replacer on the tokens
|
||||||
for index, token := range tokensCopy {
|
// golang for range slice return a copy of value
|
||||||
token.Text = repl.ReplaceKnown(token.Text, "")
|
// similarly, append also copy value
|
||||||
tokensCopy[index] = token
|
for i, token := range importedTokens {
|
||||||
|
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
|
||||||
|
if token.snippetName != "" {
|
||||||
|
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName))
|
||||||
|
} else {
|
||||||
|
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// naive way of determine snippets, as snippets definition can only follow name + block
|
||||||
|
// format, won't check for nesting correctness or any other error, that's what parser does.
|
||||||
|
if !maybeSnippet && nesting == 0 {
|
||||||
|
// first of the line
|
||||||
|
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
|
||||||
|
index = 0
|
||||||
|
} else {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
|
||||||
|
maybeSnippetId = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch token.Text {
|
||||||
|
case "{":
|
||||||
|
nesting++
|
||||||
|
if index == 1 && maybeSnippetId && nesting == 1 {
|
||||||
|
maybeSnippet = true
|
||||||
|
maybeSnippetId = false
|
||||||
|
}
|
||||||
|
case "}":
|
||||||
|
nesting--
|
||||||
|
if nesting == 0 && maybeSnippet {
|
||||||
|
maybeSnippet = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it is {block}, we substitute with all tokens in the block
|
||||||
|
// if it is {blocks.*}, we substitute with the tokens in the mapping for the *
|
||||||
|
var skip bool
|
||||||
|
var tokensToAdd []Token
|
||||||
|
switch {
|
||||||
|
case token.Text == "{block}":
|
||||||
|
tokensToAdd = blockTokens
|
||||||
|
case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
|
||||||
|
// {blocks.foo.bar} will be extracted to key `foo.bar`
|
||||||
|
blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
|
||||||
|
val, ok := blockMapping[blockKey]
|
||||||
|
if ok {
|
||||||
|
tokensToAdd = val
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
skip = true
|
||||||
|
}
|
||||||
|
if !skip {
|
||||||
|
if len(tokensToAdd) == 0 {
|
||||||
|
// if there is no content in the snippet block, don't do any replacement
|
||||||
|
// this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
|
||||||
|
tokensCopy = append(tokensCopy, token)
|
||||||
|
} else {
|
||||||
|
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if maybeSnippet {
|
||||||
|
tokensCopy = append(tokensCopy, token)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
|
||||||
|
if foundVariadic {
|
||||||
|
for _, arg := range args[startIndex:endIndex] {
|
||||||
|
token.Text = arg
|
||||||
|
tokensCopy = append(tokensCopy, token)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token.Text = repl.ReplaceKnown(token.Text, "")
|
||||||
|
tokensCopy = append(tokensCopy, token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// splice the imported tokens in the place of the import statement
|
// splice the imported tokens in the place of the import statement
|
||||||
// and rewind cursor so Next() will land on first imported token
|
// and rewind cursor so Next() will land on first imported token
|
||||||
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
|
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
|
||||||
p.cursor -= len(args) + 1
|
p.cursor -= len(args) + len(blockTokens) + 1
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -447,11 +604,17 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
input, err := ioutil.ReadAll(file)
|
input, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only warning in case of empty files
|
||||||
|
if len(input) == 0 || len(strings.TrimSpace(string(input))) == 0 {
|
||||||
|
caddy.Log().Warn("Import file is empty", zap.String("file", importFile))
|
||||||
|
return []Token{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
importedTokens, err := allTokens(importFile, input)
|
importedTokens, err := allTokens(importFile, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
||||||
@@ -477,7 +640,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
// are loaded into the current server block for later use
|
// are loaded into the current server block for later use
|
||||||
// by directive setup functions.
|
// by directive setup functions.
|
||||||
func (p *parser) directive() error {
|
func (p *parser) directive() error {
|
||||||
|
|
||||||
// a segment is a list of tokens associated with this directive
|
// a segment is a list of tokens associated with this directive
|
||||||
var segment Segment
|
var segment Segment
|
||||||
|
|
||||||
@@ -487,6 +649,16 @@ func (p *parser) directive() error {
|
|||||||
for p.Next() {
|
for p.Next() {
|
||||||
if p.Val() == "{" {
|
if p.Val() == "{" {
|
||||||
p.nesting++
|
p.nesting++
|
||||||
|
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||||
|
return p.Err("Unexpected next token after '{' on same line")
|
||||||
|
}
|
||||||
|
if p.isNewLine() {
|
||||||
|
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?")
|
||||||
|
}
|
||||||
|
} else if p.Val() == "{}" {
|
||||||
|
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||||
|
return p.Err("Unexpected '{}' at end of line")
|
||||||
|
}
|
||||||
} else if p.isNewLine() && p.nesting == 0 {
|
} else if p.isNewLine() && p.nesting == 0 {
|
||||||
p.cursor-- // read too far
|
p.cursor-- // read too far
|
||||||
break
|
break
|
||||||
@@ -495,7 +667,7 @@ func (p *parser) directive() error {
|
|||||||
} else if p.Val() == "}" && p.nesting == 0 {
|
} else if p.Val() == "}" && p.nesting == 0 {
|
||||||
return p.Err("Unexpected '}' because no matching opening brace")
|
return p.Err("Unexpected '}' because no matching opening brace")
|
||||||
} else if p.Val() == "import" && p.isNewLine() {
|
} else if p.Val() == "import" && p.isNewLine() {
|
||||||
if err := p.doImport(); err != nil {
|
if err := p.doImport(1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||||
@@ -536,28 +708,43 @@ func (p *parser) closeCurlyBrace() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *parser) isNamedRoute() (bool, string) {
|
||||||
|
keys := p.block.Keys
|
||||||
|
// A named route block is a single key with parens, prefixed with &.
|
||||||
|
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "&(") && strings.HasSuffix(keys[0].Text, ")") {
|
||||||
|
return true, strings.TrimSuffix(keys[0].Text[2:], ")")
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
func (p *parser) isSnippet() (bool, string) {
|
func (p *parser) isSnippet() (bool, string) {
|
||||||
keys := p.block.Keys
|
keys := p.block.Keys
|
||||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||||
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
|
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "(") && strings.HasSuffix(keys[0].Text, ")") {
|
||||||
return true, strings.TrimSuffix(keys[0][1:], ")")
|
return true, strings.TrimSuffix(keys[0].Text[1:], ")")
|
||||||
}
|
}
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// read and store everything in a block for later replay.
|
// read and store everything in a block for later replay.
|
||||||
func (p *parser) snippetTokens() ([]Token, error) {
|
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
|
||||||
// snippet must have curlies.
|
// block must have curlies.
|
||||||
err := p.openCurlyBrace()
|
err := p.openCurlyBrace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nesting := 1 // count our own nesting in snippets
|
nesting := 1 // count our own nesting
|
||||||
tokens := []Token{}
|
tokens := []Token{}
|
||||||
|
if retainCurlies {
|
||||||
|
tokens = append(tokens, p.Token())
|
||||||
|
}
|
||||||
for p.Next() {
|
for p.Next() {
|
||||||
if p.Val() == "}" {
|
if p.Val() == "}" {
|
||||||
nesting--
|
nesting--
|
||||||
if nesting == 0 {
|
if nesting == 0 {
|
||||||
|
if retainCurlies {
|
||||||
|
tokens = append(tokens, p.Token())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,9 +764,18 @@ func (p *parser) snippetTokens() ([]Token, error) {
|
|||||||
// head of the server block with tokens, which are
|
// head of the server block with tokens, which are
|
||||||
// grouped by segments.
|
// grouped by segments.
|
||||||
type ServerBlock struct {
|
type ServerBlock struct {
|
||||||
HasBraces bool
|
HasBraces bool
|
||||||
Keys []string
|
Keys []Token
|
||||||
Segments []Segment
|
Segments []Segment
|
||||||
|
IsNamedRoute bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sb ServerBlock) GetKeysText() []string {
|
||||||
|
res := []string{}
|
||||||
|
for _, k := range sb.Keys {
|
||||||
|
res = append(res, k.Text)
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// DispenseDirective returns a dispenser that contains
|
// DispenseDirective returns a dispenser that contains
|
||||||
|
|||||||
Executable → Regular
+212
-32
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -16,17 +16,101 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestParseVariadic(t *testing.T) {
|
||||||
|
args := make([]string, 10)
|
||||||
|
for i, tc := range []struct {
|
||||||
|
input string
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[1",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "1]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[:]}aaaaa",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "aaaaa{args[:]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args.}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args.1}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[:]}",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[:]}",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[0:]}",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[:0]}",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[-1:]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[:11]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[10:0]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[0:10]}",
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{args[0]}:{args[1]}:{args[2]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
token := Token{
|
||||||
|
File: "test",
|
||||||
|
Line: 1,
|
||||||
|
Text: tc.input,
|
||||||
|
}
|
||||||
|
if v, _, _ := parseVariadic(token, len(args)); v != tc.result {
|
||||||
|
t.Errorf("Test %d error expectation failed Expected: %t, got %t", i, tc.result, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAllTokens(t *testing.T) {
|
func TestAllTokens(t *testing.T) {
|
||||||
input := []byte("a b c\nd e")
|
input := []byte("a b c\nd e")
|
||||||
expected := []string{"a", "b", "c", "d", "e"}
|
expected := []string{"a", "b", "c", "d", "e"}
|
||||||
tokens, err := allTokens("TestAllTokens", input)
|
tokens, err := allTokens("TestAllTokens", input)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Expected no error, got %v", err)
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -64,10 +148,11 @@ func TestParseOneAndImport(t *testing.T) {
|
|||||||
"localhost",
|
"localhost",
|
||||||
}, []int{1}},
|
}, []int{1}},
|
||||||
|
|
||||||
{`localhost:1234
|
{
|
||||||
|
`localhost:1234
|
||||||
dir1 foo bar`, false, []string{
|
dir1 foo bar`, false, []string{
|
||||||
"localhost:1234",
|
"localhost:1234",
|
||||||
}, []int{3},
|
}, []int{3},
|
||||||
},
|
},
|
||||||
|
|
||||||
{`localhost {
|
{`localhost {
|
||||||
@@ -188,10 +273,49 @@ func TestParseOneAndImport(t *testing.T) {
|
|||||||
|
|
||||||
{`import testdata/not_found.txt`, true, []string{}, []int{}},
|
{`import testdata/not_found.txt`, true, []string{}, []int{}},
|
||||||
|
|
||||||
|
// empty file should just log a warning, and result in no tokens
|
||||||
|
{`import testdata/empty.txt`, false, []string{}, []int{}},
|
||||||
|
|
||||||
|
{`import testdata/only_white_space.txt`, false, []string{}, []int{}},
|
||||||
|
|
||||||
|
// import path/to/dir/* should skip any files that start with a . when iterating over them.
|
||||||
|
{`localhost
|
||||||
|
dir1 arg1
|
||||||
|
import testdata/glob/*`, false, []string{
|
||||||
|
"localhost",
|
||||||
|
}, []int{2, 3, 1}},
|
||||||
|
|
||||||
|
// import path/to/dir/.* should continue to read all dotfiles in a dir.
|
||||||
|
{`import testdata/glob/.*`, false, []string{
|
||||||
|
"host1",
|
||||||
|
}, []int{1, 2}},
|
||||||
|
|
||||||
{`""`, false, []string{}, []int{}},
|
{`""`, false, []string{}, []int{}},
|
||||||
|
|
||||||
{``, false, []string{}, []int{}},
|
{``, false, []string{}, []int{}},
|
||||||
|
|
||||||
|
// Unexpected next token after '{' on same line
|
||||||
|
{`localhost
|
||||||
|
dir1 { a b }`, true, []string{"localhost"}, []int{}},
|
||||||
|
|
||||||
|
// Unexpected '{' on a new line
|
||||||
|
{`localhost
|
||||||
|
dir1
|
||||||
|
{
|
||||||
|
a b
|
||||||
|
}`, true, []string{"localhost"}, []int{}},
|
||||||
|
|
||||||
|
// Workaround with quotes
|
||||||
|
{`localhost
|
||||||
|
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
|
||||||
|
|
||||||
|
// Unexpected '{}' at end of line
|
||||||
|
{`localhost
|
||||||
|
dir1 {}`, true, []string{"localhost"}, []int{}},
|
||||||
|
// Workaround with quotes
|
||||||
|
{`localhost
|
||||||
|
dir1 "{}"`, false, []string{"localhost"}, []int{2}},
|
||||||
|
|
||||||
// import with args
|
// import with args
|
||||||
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
|
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
|
||||||
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
|
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
|
||||||
@@ -223,7 +347,7 @@ func TestParseOneAndImport(t *testing.T) {
|
|||||||
i, len(test.keys), len(result.Keys))
|
i, len(test.keys), len(result.Keys))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for j, addr := range result.Keys {
|
for j, addr := range result.GetKeysText() {
|
||||||
if addr != test.keys[j] {
|
if addr != test.keys[j] {
|
||||||
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
|
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
|
||||||
i, j, test.keys[j], addr)
|
i, j, test.keys[j], addr)
|
||||||
@@ -255,8 +379,9 @@ func TestRecursiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isExpected := func(got ServerBlock) bool {
|
isExpected := func(got ServerBlock) bool {
|
||||||
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
|
textKeys := got.GetKeysText()
|
||||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
if len(textKeys) != 1 || textKeys[0] != "localhost" {
|
||||||
|
t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(got.Segments) != 2 {
|
if len(got.Segments) != 2 {
|
||||||
@@ -280,16 +405,16 @@ func TestRecursiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test relative recursive import
|
// test relative recursive import
|
||||||
err = ioutil.WriteFile(recursiveFile1, []byte(
|
err = os.WriteFile(recursiveFile1, []byte(
|
||||||
`localhost
|
`localhost
|
||||||
dir1
|
dir1
|
||||||
import recursive_import_test2`), 0644)
|
import recursive_import_test2`), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.Remove(recursiveFile1)
|
defer os.Remove(recursiveFile1)
|
||||||
|
|
||||||
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
|
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -314,10 +439,10 @@ func TestRecursiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test absolute recursive import
|
// test absolute recursive import
|
||||||
err = ioutil.WriteFile(recursiveFile1, []byte(
|
err = os.WriteFile(recursiveFile1, []byte(
|
||||||
`localhost
|
`localhost
|
||||||
dir1
|
dir1
|
||||||
import `+recursiveFile2), 0644)
|
import `+recursiveFile2), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -350,8 +475,9 @@ func TestDirectiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isExpected := func(got ServerBlock) bool {
|
isExpected := func(got ServerBlock) bool {
|
||||||
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
|
textKeys := got.GetKeysText()
|
||||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
if len(textKeys) != 1 || textKeys[0] != "localhost" {
|
||||||
|
t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(got.Segments) != 2 {
|
if len(got.Segments) != 2 {
|
||||||
@@ -370,8 +496,8 @@ func TestDirectiveImport(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
|
err = os.WriteFile(directiveFile, []byte(`prop1 1
|
||||||
prop2 2`), 0644)
|
prop2 2`), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -429,6 +555,10 @@ func TestParseAll(t *testing.T) {
|
|||||||
{"localhost:1234", "http://host2"},
|
{"localhost:1234", "http://host2"},
|
||||||
}},
|
}},
|
||||||
|
|
||||||
|
{`foo.example.com , example.com`, false, [][]string{
|
||||||
|
{"foo.example.com", "example.com"},
|
||||||
|
}},
|
||||||
|
|
||||||
{`localhost:1234, http://host2,`, true, [][]string{}},
|
{`localhost:1234, http://host2,`, true, [][]string{}},
|
||||||
|
|
||||||
{`http://host1.com, http://host2.com {
|
{`http://host1.com, http://host2.com {
|
||||||
@@ -488,11 +618,11 @@ func TestParseAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for j, block := range blocks {
|
for j, block := range blocks {
|
||||||
if len(block.Keys) != len(test.keys[j]) {
|
if len(block.Keys) != len(test.keys[j]) {
|
||||||
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
|
t.Errorf("Test %d: Expected %d keys in block %d, got %d: %v",
|
||||||
i, len(test.keys[j]), j, len(block.Keys))
|
i, len(test.keys[j]), j, len(block.Keys), block.Keys)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for k, addr := range block.Keys {
|
for k, addr := range block.GetKeysText() {
|
||||||
if addr != test.keys[j][k] {
|
if addr != test.keys[j][k] {
|
||||||
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
|
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
|
||||||
i, j, k, test.keys[j][k], addr)
|
i, j, k, test.keys[j][k], addr)
|
||||||
@@ -591,16 +721,43 @@ func TestEnvironmentReplacement(t *testing.T) {
|
|||||||
expect: "}{$",
|
expect: "}{$",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
actual, err := replaceEnvVars([]byte(test.input))
|
actual := replaceEnvVars([]byte(test.input))
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(actual, []byte(test.expect)) {
|
if !bytes.Equal(actual, []byte(test.expect)) {
|
||||||
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImportReplacementInJSONWithBrace(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
args []string
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{"123"},
|
||||||
|
input: "{args[0]}",
|
||||||
|
expect: "123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"123"},
|
||||||
|
input: `{"key":"{args[0]}"}`,
|
||||||
|
expect: `{"key":"123"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"123", "123"},
|
||||||
|
input: `{"key":[{args[0]},{args[1]}]}`,
|
||||||
|
expect: `{"key":[123,123]}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
repl := makeArgsReplacer(test.args)
|
||||||
|
actual := repl.ReplaceKnown(test.input, "")
|
||||||
|
if actual != test.expect {
|
||||||
|
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSnippets(t *testing.T) {
|
func TestSnippets(t *testing.T) {
|
||||||
p := testParser(`
|
p := testParser(`
|
||||||
(common) {
|
(common) {
|
||||||
@@ -618,7 +775,7 @@ func TestSnippets(t *testing.T) {
|
|||||||
if len(blocks) != 1 {
|
if len(blocks) != 1 {
|
||||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||||
}
|
}
|
||||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
|
||||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||||
}
|
}
|
||||||
if len(blocks[0].Segments) != 2 {
|
if len(blocks[0].Segments) != 2 {
|
||||||
@@ -633,7 +790,7 @@ func TestSnippets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
||||||
file, err := ioutil.TempFile("", t.Name())
|
file, err := os.CreateTemp("", t.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // get a stack trace so we know where this was called from.
|
panic(err) // get a stack trace so we know where this was called from.
|
||||||
}
|
}
|
||||||
@@ -650,7 +807,7 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
|
|||||||
fileName := writeStringToTempFileOrDie(t, `
|
fileName := writeStringToTempFileOrDie(t, `
|
||||||
http://example.com {
|
http://example.com {
|
||||||
# This isn't an import directive, it's just an arg with value 'import'
|
# This isn't an import directive, it's just an arg with value 'import'
|
||||||
basicauth / import password
|
basic_auth / import password
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
// Parse the root file that imports the other one.
|
// Parse the root file that imports the other one.
|
||||||
@@ -661,12 +818,12 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
|
|||||||
}
|
}
|
||||||
auth := blocks[0].Segments[0]
|
auth := blocks[0].Segments[0]
|
||||||
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
|
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
|
||||||
if line != "basicauth / import password" {
|
if line != "basic_auth / import password" {
|
||||||
// Previously, it would be changed to:
|
// Previously, it would be changed to:
|
||||||
// basicauth / import /path/to/test/dir/password
|
// basic_auth / import /path/to/test/dir/password
|
||||||
// referencing a file that (probably) doesn't exist and changing the
|
// referencing a file that (probably) doesn't exist and changing the
|
||||||
// password!
|
// password!
|
||||||
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
|
t.Errorf("Expected basic_auth tokens to be 'basic_auth / import password' but got %#q", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,7 +850,7 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
|
|||||||
if len(blocks) != 1 {
|
if len(blocks) != 1 {
|
||||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||||
}
|
}
|
||||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
|
||||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||||
}
|
}
|
||||||
if len(blocks[0].Segments) != 1 {
|
if len(blocks[0].Segments) != 1 {
|
||||||
@@ -704,6 +861,29 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRejectsGlobalMatcher(t *testing.T) {
|
||||||
|
p := testParser(`
|
||||||
|
@rejected path /foo
|
||||||
|
|
||||||
|
(common) {
|
||||||
|
gzip foo
|
||||||
|
errors stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
http://example.com {
|
||||||
|
import common
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
_, err := p.parseAll()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error, but got nil")
|
||||||
|
}
|
||||||
|
expected := "request matchers may not be defined globally, they must be in a site block; found @rejected, at Testfile:2"
|
||||||
|
if err.Error() != expected {
|
||||||
|
t.Errorf("Expected error to be '%s' but got '%v'", expected, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testParser(input string) parser {
|
func testParser(input string) parser {
|
||||||
return parser{Dispenser: NewTestDispenser(input)}
|
return parser{Dispenser: NewTestDispenser(input)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
host1 {
|
||||||
|
dir1
|
||||||
|
dir2 arg1
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
dir2 arg1 arg2
|
||||||
|
dir3
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{args.0}
|
{args[0]}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{args.0} {args.1}
|
{args[0]} {args[1]}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
// Adapter is a type which can adapt a configuration to Caddy JSON.
|
// Adapter is a type which can adapt a configuration to Caddy JSON.
|
||||||
// It returns the results and any warnings, or an error.
|
// It returns the results and any warnings, or an error.
|
||||||
type Adapter interface {
|
type Adapter interface {
|
||||||
Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error)
|
Adapt(body []byte, options map[string]any) ([]byte, []Warning, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning represents a warning or notice related to conversion.
|
// Warning represents a warning or notice related to conversion.
|
||||||
@@ -48,7 +48,7 @@ func (w Warning) String() string {
|
|||||||
// are converted to warnings. This is convenient when filling config
|
// are converted to warnings. This is convenient when filling config
|
||||||
// structs that require a json.RawMessage, without having to worry
|
// structs that require a json.RawMessage, without having to worry
|
||||||
// about errors.
|
// about errors.
|
||||||
func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
|
func JSON(val any, warnings *[]Warning) json.RawMessage {
|
||||||
b, err := json.Marshal(val)
|
b, err := json.Marshal(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if warnings != nil {
|
if warnings != nil {
|
||||||
@@ -64,9 +64,9 @@ func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
|
|||||||
// for encoding module values where the module name has to be described within
|
// for encoding module values where the module name has to be described within
|
||||||
// the object by a certain key; for example, `"handler": "file_server"` for a
|
// the object by a certain key; for example, `"handler": "file_server"` for a
|
||||||
// file server HTTP handler (fieldName="handler" and fieldVal="file_server").
|
// file server HTTP handler (fieldName="handler" and fieldVal="file_server").
|
||||||
// The val parameter must encode into a map[string]interface{} (i.e. it must be
|
// The val parameter must encode into a map[string]any (i.e. it must be
|
||||||
// a struct or map). Any errors are converted into warnings.
|
// a struct or map). Any errors are converted into warnings.
|
||||||
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
|
func JSONModuleObject(val any, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
|
||||||
// encode to a JSON object first
|
// encode to a JSON object first
|
||||||
enc, err := json.Marshal(val)
|
enc, err := json.Marshal(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -77,7 +77,7 @@ func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]W
|
|||||||
}
|
}
|
||||||
|
|
||||||
// then decode the object
|
// then decode the object
|
||||||
var tmp map[string]interface{}
|
var tmp map[string]any
|
||||||
err = json.Unmarshal(enc, &tmp)
|
err = json.Unmarshal(enc, &tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if warnings != nil {
|
if warnings != nil {
|
||||||
|
|||||||
@@ -17,16 +17,18 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// mapAddressToServerBlocks returns a map of listener address to list of server
|
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||||
@@ -35,12 +37,12 @@ import (
|
|||||||
// server block that share the same address stay grouped together so the config
|
// server block that share the same address stay grouped together so the config
|
||||||
// isn't repeated unnecessarily. For example, this Caddyfile:
|
// isn't repeated unnecessarily. For example, this Caddyfile:
|
||||||
//
|
//
|
||||||
// example.com {
|
// example.com {
|
||||||
// bind 127.0.0.1
|
// bind 127.0.0.1
|
||||||
// }
|
// }
|
||||||
// www.example.com, example.net/path, localhost:9999 {
|
// www.example.com, example.net/path, localhost:9999 {
|
||||||
// bind 127.0.0.1 1.2.3.4
|
// bind 127.0.0.1 1.2.3.4
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// has two server blocks to start with. But expressed in this Caddyfile are
|
// has two server blocks to start with. But expressed in this Caddyfile are
|
||||||
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
|
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
|
||||||
@@ -75,9 +77,15 @@ import (
|
|||||||
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
||||||
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||||
// (Doing this is essentially a map-reduce technique.)
|
// (Doing this is essentially a map-reduce technique.)
|
||||||
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []serverBlock,
|
||||||
options map[string]interface{}) (map[string][]serverBlock, error) {
|
options map[string]any,
|
||||||
sbmap := make(map[string][]serverBlock)
|
) (map[string]map[string][]serverBlock, error) {
|
||||||
|
addrToProtocolToServerBlocks := map[string]map[string][]serverBlock{}
|
||||||
|
|
||||||
|
type keyWithParsedKey struct {
|
||||||
|
key caddyfile.Token
|
||||||
|
parsedKey Address
|
||||||
|
}
|
||||||
|
|
||||||
for i, sblock := range originalServerBlocks {
|
for i, sblock := range originalServerBlocks {
|
||||||
// within a server block, we need to map all the listener addresses
|
// within a server block, we need to map all the listener addresses
|
||||||
@@ -85,102 +93,193 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
|
|||||||
// will be served by them; this has the effect of treating each
|
// will be served by them; this has the effect of treating each
|
||||||
// key of a server block as its own, but without having to repeat its
|
// key of a server block as its own, but without having to repeat its
|
||||||
// contents in cases where multiple keys really can be served together
|
// contents in cases where multiple keys really can be served together
|
||||||
addrToKeys := make(map[string][]string)
|
addrToProtocolToKeyWithParsedKeys := map[string]map[string][]keyWithParsedKey{}
|
||||||
for j, key := range sblock.block.Keys {
|
for j, key := range sblock.block.Keys {
|
||||||
|
parsedKey, err := ParseAddress(key.Text)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing key: %v", err)
|
||||||
|
}
|
||||||
|
parsedKey = parsedKey.Normalize()
|
||||||
|
|
||||||
// a key can have multiple listener addresses if there are multiple
|
// a key can have multiple listener addresses if there are multiple
|
||||||
// arguments to the 'bind' directive (although they will all have
|
// arguments to the 'bind' directive (although they will all have
|
||||||
// the same port, since the port is defined by the key or is implicit
|
// the same port, since the port is defined by the key or is implicit
|
||||||
// through automatic HTTPS)
|
// through automatic HTTPS)
|
||||||
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key, options)
|
listeners, err := st.listenersForServerBlockAddress(sblock, parsedKey, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err)
|
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// associate this key with each listener address it is served on
|
// associate this key with its protocols and each listener address served with them
|
||||||
for _, addr := range addrs {
|
kwpk := keyWithParsedKey{key, parsedKey}
|
||||||
addrToKeys[addr] = append(addrToKeys[addr], key)
|
for addr, protocols := range listeners {
|
||||||
|
protocolToKeyWithParsedKeys, ok := addrToProtocolToKeyWithParsedKeys[addr]
|
||||||
|
if !ok {
|
||||||
|
protocolToKeyWithParsedKeys = map[string][]keyWithParsedKey{}
|
||||||
|
addrToProtocolToKeyWithParsedKeys[addr] = protocolToKeyWithParsedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// an empty protocol indicates the default, a nil or empty value in the ListenProtocols array
|
||||||
|
if len(protocols) == 0 {
|
||||||
|
protocols[""] = struct{}{}
|
||||||
|
}
|
||||||
|
for prot := range protocols {
|
||||||
|
protocolToKeyWithParsedKeys[prot] = append(
|
||||||
|
protocolToKeyWithParsedKeys[prot],
|
||||||
|
kwpk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make a slice of the map keys so we can iterate in sorted order
|
||||||
|
addrs := make([]string, 0, len(addrToProtocolToKeyWithParsedKeys))
|
||||||
|
for addr := range addrToProtocolToKeyWithParsedKeys {
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
sort.Strings(addrs)
|
||||||
|
|
||||||
// now that we know which addresses serve which keys of this
|
// now that we know which addresses serve which keys of this
|
||||||
// server block, we iterate that mapping and create a list of
|
// server block, we iterate that mapping and create a list of
|
||||||
// new server blocks for each address where the keys of the
|
// new server blocks for each address where the keys of the
|
||||||
// server block are only the ones which use the address; but
|
// server block are only the ones which use the address; but
|
||||||
// the contents (tokens) are of course the same
|
// the contents (tokens) are of course the same
|
||||||
for addr, keys := range addrToKeys {
|
for _, addr := range addrs {
|
||||||
// parse keys so that we only have to do it once
|
protocolToKeyWithParsedKeys := addrToProtocolToKeyWithParsedKeys[addr]
|
||||||
parsedKeys := make([]Address, 0, len(keys))
|
|
||||||
for _, key := range keys {
|
prots := make([]string, 0, len(protocolToKeyWithParsedKeys))
|
||||||
addr, err := ParseAddress(key)
|
for prot := range protocolToKeyWithParsedKeys {
|
||||||
if err != nil {
|
prots = append(prots, prot)
|
||||||
return nil, fmt.Errorf("parsing key '%s': %v", key, err)
|
|
||||||
}
|
|
||||||
parsedKeys = append(parsedKeys, addr.Normalize())
|
|
||||||
}
|
}
|
||||||
sbmap[addr] = append(sbmap[addr], serverBlock{
|
sort.Strings(prots)
|
||||||
block: caddyfile.ServerBlock{
|
|
||||||
Keys: keys,
|
protocolToServerBlocks, ok := addrToProtocolToServerBlocks[addr]
|
||||||
Segments: sblock.block.Segments,
|
if !ok {
|
||||||
},
|
protocolToServerBlocks = map[string][]serverBlock{}
|
||||||
pile: sblock.pile,
|
addrToProtocolToServerBlocks[addr] = protocolToServerBlocks
|
||||||
keys: parsedKeys,
|
}
|
||||||
|
|
||||||
|
for _, prot := range prots {
|
||||||
|
keyWithParsedKeys := protocolToKeyWithParsedKeys[prot]
|
||||||
|
|
||||||
|
keys := make([]caddyfile.Token, len(keyWithParsedKeys))
|
||||||
|
parsedKeys := make([]Address, len(keyWithParsedKeys))
|
||||||
|
|
||||||
|
for k, keyWithParsedKey := range keyWithParsedKeys {
|
||||||
|
keys[k] = keyWithParsedKey.key
|
||||||
|
parsedKeys[k] = keyWithParsedKey.parsedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], serverBlock{
|
||||||
|
block: caddyfile.ServerBlock{
|
||||||
|
Keys: keys,
|
||||||
|
Segments: sblock.block.Segments,
|
||||||
|
},
|
||||||
|
pile: sblock.pile,
|
||||||
|
parsedKeys: parsedKeys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addrToProtocolToServerBlocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
|
||||||
|
// single listener addresses to protocols to lists of server blocks. Since multiple addresses
|
||||||
|
// may serve multiple protocols to identical sites (server block contents), this function turns
|
||||||
|
// a 1:many mapping into a many:many mapping. Server block contents (tokens) must be
|
||||||
|
// exactly identical so that reflect.DeepEqual returns true in order for the addresses to be combined.
|
||||||
|
// Identical entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
|
||||||
|
// association from multiple addresses to multiple server blocks; i.e. each element of
|
||||||
|
// the returned slice) becomes a server definition in the output JSON.
|
||||||
|
func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]serverBlock) []sbAddrAssociation {
|
||||||
|
sbaddrs := make([]sbAddrAssociation, 0, len(addrToProtocolToServerBlocks))
|
||||||
|
|
||||||
|
addrs := make([]string, 0, len(addrToProtocolToServerBlocks))
|
||||||
|
for addr := range addrToProtocolToServerBlocks {
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
sort.Strings(addrs)
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
protocolToServerBlocks := addrToProtocolToServerBlocks[addr]
|
||||||
|
|
||||||
|
prots := make([]string, 0, len(protocolToServerBlocks))
|
||||||
|
for prot := range protocolToServerBlocks {
|
||||||
|
prots = append(prots, prot)
|
||||||
|
}
|
||||||
|
sort.Strings(prots)
|
||||||
|
|
||||||
|
for _, prot := range prots {
|
||||||
|
serverBlocks := protocolToServerBlocks[prot]
|
||||||
|
|
||||||
|
// now find other addresses that map to identical
|
||||||
|
// server blocks and add them to our map of listener
|
||||||
|
// addresses and protocols, while removing them from
|
||||||
|
// the original map
|
||||||
|
listeners := map[string]map[string]struct{}{}
|
||||||
|
|
||||||
|
for otherAddr, otherProtocolToServerBlocks := range addrToProtocolToServerBlocks {
|
||||||
|
for otherProt, otherServerBlocks := range otherProtocolToServerBlocks {
|
||||||
|
if addr == otherAddr && prot == otherProt || reflect.DeepEqual(serverBlocks, otherServerBlocks) {
|
||||||
|
listener, ok := listeners[otherAddr]
|
||||||
|
if !ok {
|
||||||
|
listener = map[string]struct{}{}
|
||||||
|
listeners[otherAddr] = listener
|
||||||
|
}
|
||||||
|
listener[otherProt] = struct{}{}
|
||||||
|
delete(otherProtocolToServerBlocks, otherProt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses := make([]string, 0, len(listeners))
|
||||||
|
for lnAddr := range listeners {
|
||||||
|
addresses = append(addresses, lnAddr)
|
||||||
|
}
|
||||||
|
sort.Strings(addresses)
|
||||||
|
|
||||||
|
addressesWithProtocols := make([]addressWithProtocols, 0, len(listeners))
|
||||||
|
|
||||||
|
for _, lnAddr := range addresses {
|
||||||
|
lnProts := listeners[lnAddr]
|
||||||
|
prots := make([]string, 0, len(lnProts))
|
||||||
|
for prot := range lnProts {
|
||||||
|
prots = append(prots, prot)
|
||||||
|
}
|
||||||
|
sort.Strings(prots)
|
||||||
|
|
||||||
|
addressesWithProtocols = append(addressesWithProtocols, addressWithProtocols{
|
||||||
|
address: lnAddr,
|
||||||
|
protocols: prots,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sbaddrs = append(sbaddrs, sbAddrAssociation{
|
||||||
|
addressesWithProtocols: addressesWithProtocols,
|
||||||
|
serverBlocks: serverBlocks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sbmap, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
|
|
||||||
// single listener addresses to lists of server blocks. Since multiple addresses may serve
|
|
||||||
// identical sites (server block contents), this function turns a 1:many mapping into a
|
|
||||||
// many:many mapping. Server block contents (tokens) must be exactly identical so that
|
|
||||||
// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
|
|
||||||
// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
|
|
||||||
// association from multiple addresses to multiple server blocks; i.e. each element of
|
|
||||||
// the returned slice) becomes a server definition in the output JSON.
|
|
||||||
func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation {
|
|
||||||
sbaddrs := make([]sbAddrAssociation, 0, len(addrToServerBlocks))
|
|
||||||
for addr, sblocks := range addrToServerBlocks {
|
|
||||||
// we start with knowing that at least this address
|
|
||||||
// maps to these server blocks
|
|
||||||
a := sbAddrAssociation{
|
|
||||||
addresses: []string{addr},
|
|
||||||
serverBlocks: sblocks,
|
|
||||||
}
|
|
||||||
|
|
||||||
// now find other addresses that map to identical
|
|
||||||
// server blocks and add them to our list of
|
|
||||||
// addresses, while removing them from the map
|
|
||||||
for otherAddr, otherSblocks := range addrToServerBlocks {
|
|
||||||
if addr == otherAddr {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if reflect.DeepEqual(sblocks, otherSblocks) {
|
|
||||||
a.addresses = append(a.addresses, otherAddr)
|
|
||||||
delete(addrToServerBlocks, otherAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sbaddrs = append(sbaddrs, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort them by their first address (we know there will always be at least one)
|
|
||||||
// to avoid problems with non-deterministic ordering (makes tests flaky)
|
|
||||||
sort.Slice(sbaddrs, func(i, j int) bool {
|
|
||||||
return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
return sbaddrs
|
return sbaddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
// listenersForServerBlockAddress essentially converts the Caddyfile site addresses to a map from
|
||||||
options map[string]interface{}) ([]string, error) {
|
// Caddy listener addresses and the protocols to serve them with to the parsed address for each server block.
|
||||||
addr, err := ParseAddress(key)
|
func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Address,
|
||||||
if err != nil {
|
options map[string]any,
|
||||||
return nil, fmt.Errorf("parsing key: %v", err)
|
) (map[string]map[string]struct{}, error) {
|
||||||
|
switch addr.Scheme {
|
||||||
|
case "wss":
|
||||||
|
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
||||||
|
case "ws":
|
||||||
|
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
|
||||||
|
case "https", "http", "":
|
||||||
|
// Do nothing or handle the valid schemes
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
|
||||||
}
|
}
|
||||||
addr = addr.Normalize()
|
|
||||||
|
|
||||||
// figure out the HTTP and HTTPS ports; either
|
// figure out the HTTP and HTTPS ports; either
|
||||||
// use defaults, or override with user config
|
// use defaults, or override with user config
|
||||||
@@ -204,36 +303,54 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
|||||||
|
|
||||||
// error if scheme and port combination violate convention
|
// error if scheme and port combination violate convention
|
||||||
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
|
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
|
||||||
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
|
return nil, fmt.Errorf("[%s] scheme and port violate convention", addr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// the bind directive specifies hosts, but is optional
|
// the bind directive specifies hosts (and potentially network), and the protocols to serve them with, but is optional
|
||||||
lnHosts := make([]string, 0, len(sblock.pile))
|
lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.pile["bind"]))
|
||||||
for _, cfgVal := range sblock.pile["bind"] {
|
for _, cfgVal := range sblock.pile["bind"] {
|
||||||
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
|
if val, ok := cfgVal.Value.(addressesWithProtocols); ok {
|
||||||
|
lnCfgVals = append(lnCfgVals, val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(lnHosts) == 0 {
|
if len(lnCfgVals) == 0 {
|
||||||
lnHosts = []string{""}
|
if defaultBindValues, ok := options["default_bind"].([]ConfigValue); ok {
|
||||||
}
|
for _, defaultBindValue := range defaultBindValues {
|
||||||
|
lnCfgVals = append(lnCfgVals, defaultBindValue.Value.(addressesWithProtocols))
|
||||||
// use a map to prevent duplication
|
}
|
||||||
listeners := make(map[string]struct{})
|
|
||||||
for _, host := range lnHosts {
|
|
||||||
addr, err := caddy.ParseNetworkAddress(host)
|
|
||||||
if err == nil && addr.IsUnixNetwork() {
|
|
||||||
listeners[host] = struct{}{}
|
|
||||||
} else {
|
} else {
|
||||||
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
|
lnCfgVals = []addressesWithProtocols{{
|
||||||
|
addresses: []string{""},
|
||||||
|
protocols: nil,
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now turn map into list
|
// use a map to prevent duplication
|
||||||
listenersList := make([]string, 0, len(listeners))
|
listeners := map[string]map[string]struct{}{}
|
||||||
for lnStr := range listeners {
|
for _, lnCfgVal := range lnCfgVals {
|
||||||
listenersList = append(listenersList, lnStr)
|
for _, lnHost := range lnCfgVal.addresses {
|
||||||
|
networkAddr, err := caddy.ParseNetworkAddressFromHostPort(lnHost, lnPort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing network address: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := listeners[addr.String()]; !ok {
|
||||||
|
listeners[networkAddr.String()] = map[string]struct{}{}
|
||||||
|
}
|
||||||
|
for _, protocol := range lnCfgVal.protocols {
|
||||||
|
listeners[networkAddr.String()][protocol] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return listenersList, nil
|
return listeners, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addressesWithProtocols associates a list of listen addresses
|
||||||
|
// with a list of protocols to serve them with
|
||||||
|
type addressesWithProtocols struct {
|
||||||
|
addresses []string
|
||||||
|
protocols []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address represents a site address. It contains
|
// Address represents a site address. It contains
|
||||||
@@ -336,8 +453,10 @@ func (a Address) Normalize() Address {
|
|||||||
|
|
||||||
// ensure host is normalized if it's an IP address
|
// ensure host is normalized if it's an IP address
|
||||||
host := strings.TrimSpace(a.Host)
|
host := strings.TrimSpace(a.Host)
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
if ip, err := netip.ParseAddr(host); err == nil {
|
||||||
host = ip.String()
|
if ip.Is6() && !ip.Is4() && !ip.Is4In6() {
|
||||||
|
host = ip.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Address{
|
return Address{
|
||||||
@@ -349,28 +468,6 @@ func (a Address) Normalize() Address {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key returns a string form of a, much like String() does, but this
|
|
||||||
// method doesn't add anything default that wasn't in the original.
|
|
||||||
func (a Address) Key() string {
|
|
||||||
res := ""
|
|
||||||
if a.Scheme != "" {
|
|
||||||
res += a.Scheme + "://"
|
|
||||||
}
|
|
||||||
if a.Host != "" {
|
|
||||||
res += a.Host
|
|
||||||
}
|
|
||||||
// insert port only if the original has its own explicit port
|
|
||||||
if a.Port != "" &&
|
|
||||||
len(a.Original) >= len(res) &&
|
|
||||||
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
|
||||||
res += ":" + a.Port
|
|
||||||
}
|
|
||||||
if a.Path != "" {
|
|
||||||
res += a.Path
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// lowerExceptPlaceholders lowercases s except within
|
// lowerExceptPlaceholders lowercases s except within
|
||||||
// placeholders (substrings in non-escaped '{ }' spans).
|
// placeholders (substrings in non-escaped '{ }' spans).
|
||||||
// See https://github.com/caddyserver/caddy/issues/3264
|
// See https://github.com/caddyserver/caddy/issues/3264
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// +build gofuzz
|
//go:build gofuzz
|
||||||
|
|
||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
|
|||||||
@@ -106,67 +106,128 @@ func TestAddressString(t *testing.T) {
|
|||||||
func TestKeyNormalization(t *testing.T) {
|
func TestKeyNormalization(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
input string
|
input string
|
||||||
expect string
|
expect Address
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
input: "example.com",
|
input: "example.com",
|
||||||
expect: "example.com",
|
expect: Address{
|
||||||
|
Host: "example.com",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "http://host:1234/path",
|
input: "http://host:1234/path",
|
||||||
expect: "http://host:1234/path",
|
expect: Address{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "host",
|
||||||
|
Port: "1234",
|
||||||
|
Path: "/path",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "HTTP://A/ABCDEF",
|
input: "HTTP://A/ABCDEF",
|
||||||
expect: "http://a/ABCDEF",
|
expect: Address{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "a",
|
||||||
|
Path: "/ABCDEF",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "A/ABCDEF",
|
input: "A/ABCDEF",
|
||||||
expect: "a/ABCDEF",
|
expect: Address{
|
||||||
|
Host: "a",
|
||||||
|
Path: "/ABCDEF",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "A:2015/Path",
|
input: "A:2015/Path",
|
||||||
expect: "a:2015/Path",
|
expect: Address{
|
||||||
|
Host: "a",
|
||||||
|
Port: "2015",
|
||||||
|
Path: "/Path",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.{env.MY_DOMAIN}",
|
input: "sub.{env.MY_DOMAIN}",
|
||||||
expect: "sub.{env.MY_DOMAIN}",
|
expect: Address{
|
||||||
|
Host: "sub.{env.MY_DOMAIN}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.ExAmPle",
|
input: "sub.ExAmPle",
|
||||||
expect: "sub.example",
|
expect: Address{
|
||||||
|
Host: "sub.example",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.\\{env.MY_DOMAIN\\}",
|
input: "sub.\\{env.MY_DOMAIN\\}",
|
||||||
expect: "sub.\\{env.my_domain\\}",
|
expect: Address{
|
||||||
|
Host: "sub.\\{env.my_domain\\}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.{env.MY_DOMAIN}.com",
|
input: "sub.{env.MY_DOMAIN}.com",
|
||||||
expect: "sub.{env.MY_DOMAIN}.com",
|
expect: Address{
|
||||||
|
Host: "sub.{env.MY_DOMAIN}.com",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":80",
|
input: ":80",
|
||||||
expect: ":80",
|
expect: Address{
|
||||||
|
Port: "80",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":443",
|
input: ":443",
|
||||||
expect: ":443",
|
expect: Address{
|
||||||
|
Port: "443",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":1234",
|
input: ":1234",
|
||||||
expect: ":1234",
|
expect: Address{
|
||||||
|
Port: "1234",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "",
|
input: "",
|
||||||
expect: "",
|
expect: Address{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":",
|
input: ":",
|
||||||
expect: "",
|
expect: Address{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "[::]",
|
input: "[::]",
|
||||||
expect: "::",
|
expect: Address{
|
||||||
|
Host: "::",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "127.0.0.1",
|
||||||
|
expect: Address{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:1234",
|
||||||
|
expect: Address{
|
||||||
|
Host: "2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
||||||
|
Port: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// IPv4 address in IPv6 form (#4381)
|
||||||
|
input: "[::ffff:cff4:e77d]:1234",
|
||||||
|
expect: Address{
|
||||||
|
Host: "::ffff:cff4:e77d",
|
||||||
|
Port: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "::ffff:cff4:e77d",
|
||||||
|
expect: Address{
|
||||||
|
Host: "::ffff:cff4:e77d",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
@@ -175,9 +236,18 @@ func TestKeyNormalization(t *testing.T) {
|
|||||||
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
|
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if actual := addr.Normalize().Key(); actual != tc.expect {
|
actual := addr.Normalize()
|
||||||
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
|
if actual.Scheme != tc.expect.Scheme {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Scheme='%s' but got Scheme='%s'", i, tc.input, tc.expect.Scheme, actual.Scheme)
|
||||||
|
}
|
||||||
|
if actual.Host != tc.expect.Host {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Host='%s' but got Host='%s'", i, tc.input, tc.expect.Host, actual.Host)
|
||||||
|
}
|
||||||
|
if actual.Port != tc.expect.Port {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Port='%s' but got Port='%s'", i, tc.input, tc.expect.Port, actual.Port)
|
||||||
|
}
|
||||||
|
if actual.Path != tc.expect.Path {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Path='%s' but got Path='%s'", i, tc.input, tc.expect.Path, actual.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
@@ -24,11 +25,12 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
{
|
{
|
||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log {
|
log {
|
||||||
|
core mock
|
||||||
output file foo.log
|
output file foo.log
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -37,8 +39,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
format filter {
|
format filter {
|
||||||
wrap console
|
wrap console
|
||||||
fields {
|
fields {
|
||||||
common_log delete
|
request>remote_ip ip_mask {
|
||||||
request>remote_addr ip_mask {
|
|
||||||
ipv4 24
|
ipv4 24
|
||||||
ipv6 32
|
ipv6 32
|
||||||
}
|
}
|
||||||
@@ -47,17 +48,19 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"request\u003eremote_ip":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log invalid {
|
log name-override {
|
||||||
|
core mock
|
||||||
output file foo.log
|
output file foo.log
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectError: true,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|
||||||
@@ -149,6 +152,27 @@ func TestRedirDirectiveSyntax(t *testing.T) {
|
|||||||
}`,
|
}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// this is now allowed so a Location header
|
||||||
|
// can be written and consumed by JS
|
||||||
|
// in the case of XHR requests
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 401
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 402
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 {http.reverse_proxy.status_code}
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
redir /old.html /new.html htlm
|
redir /old.html /new.html htlm
|
||||||
@@ -161,12 +185,6 @@ func TestRedirDirectiveSyntax(t *testing.T) {
|
|||||||
}`,
|
}`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
input: `:8080 {
|
|
||||||
redir * :8081 400
|
|
||||||
}`,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
redir * :8081 temp
|
redir * :8081 temp
|
||||||
@@ -199,3 +217,139 @@ func TestRedirDirectiveSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImportErrorLine(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
input string
|
||||||
|
errorFunc func(err error) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `(t1) {
|
||||||
|
abort {args[:]}
|
||||||
|
}
|
||||||
|
:8080 {
|
||||||
|
import t1
|
||||||
|
import t1 true
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1)")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `(t1) {
|
||||||
|
abort {args[:]}
|
||||||
|
}
|
||||||
|
:8080 {
|
||||||
|
import t1 true
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1)")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
import testdata/import_variadic_snippet.txt
|
||||||
|
:8080 {
|
||||||
|
import t1 true
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
import testdata/import_variadic_with_import.txt
|
||||||
|
:8080 {
|
||||||
|
import t1 true
|
||||||
|
import t2 true
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
|
if !tc.errorFunc(err) {
|
||||||
|
t.Errorf("Test %d error expectation failed, got %s", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNestedImport(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
input string
|
||||||
|
errorFunc func(err error) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `(t1) {
|
||||||
|
respond {args[0]} {args[1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
(t2) {
|
||||||
|
import t1 {args[0]} 202
|
||||||
|
}
|
||||||
|
|
||||||
|
:8080 {
|
||||||
|
handle {
|
||||||
|
import t2 "foobar"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `(t1) {
|
||||||
|
respond {args[:]}
|
||||||
|
}
|
||||||
|
|
||||||
|
(t2) {
|
||||||
|
import t1 {args[0]} {args[1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8080 {
|
||||||
|
handle {
|
||||||
|
import t2 "foobar" 202
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `(t1) {
|
||||||
|
respond {args[0]} {args[1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
(t2) {
|
||||||
|
import t1 {args[:]}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8080 {
|
||||||
|
handle {
|
||||||
|
import t2 "foobar" 202
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
errorFunc: func(err error) bool {
|
||||||
|
return err == nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
|
if !tc.errorFunc(err) {
|
||||||
|
t.Errorf("Test %d error expectation failed, got %s", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,37 +28,58 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// directiveOrder specifies the order
|
// defaultDirectiveOrder specifies the default order
|
||||||
// to apply directives in HTTP routes.
|
// to apply directives in HTTP routes. This must only
|
||||||
|
// consist of directives that are included in Caddy's
|
||||||
|
// standard distribution.
|
||||||
//
|
//
|
||||||
// The root directive goes first in case rewrites or
|
// e.g. The 'root' directive goes near the start in
|
||||||
// redirects depend on existence of files, i.e. the
|
// case rewrites or redirects depend on existence of
|
||||||
// file matcher, which must know the root first.
|
// files, i.e. the file matcher, which must know the
|
||||||
|
// root first.
|
||||||
//
|
//
|
||||||
// The header directive goes second so that headers
|
// e.g. The 'header' directive goes before 'redir' so
|
||||||
// can be manipulated before doing redirects.
|
// that headers can be manipulated before doing redirects.
|
||||||
var directiveOrder = []string{
|
//
|
||||||
|
// e.g. The 'respond' directive is near the end because it
|
||||||
|
// writes a response and terminates the middleware chain.
|
||||||
|
var defaultDirectiveOrder = []string{
|
||||||
|
"tracing",
|
||||||
|
|
||||||
|
// set variables that may be used by other directives
|
||||||
"map",
|
"map",
|
||||||
|
"vars",
|
||||||
|
"fs",
|
||||||
"root",
|
"root",
|
||||||
|
"log_append",
|
||||||
|
"skip_log", // TODO: deprecated, renamed to log_skip
|
||||||
|
"log_skip",
|
||||||
|
"log_name",
|
||||||
|
|
||||||
"header",
|
"header",
|
||||||
|
"copy_response_headers", // only in reverse_proxy's handle_response
|
||||||
"request_body",
|
"request_body",
|
||||||
|
|
||||||
"redir",
|
"redir",
|
||||||
|
|
||||||
// URI manipulation
|
// incoming request manipulation
|
||||||
|
"method",
|
||||||
"rewrite",
|
"rewrite",
|
||||||
"uri",
|
"uri",
|
||||||
"try_files",
|
"try_files",
|
||||||
|
|
||||||
// middleware handlers; some wrap responses
|
// middleware handlers; some wrap responses
|
||||||
"basicauth",
|
"basicauth", // TODO: deprecated, renamed to basic_auth
|
||||||
|
"basic_auth",
|
||||||
|
"forward_auth",
|
||||||
"request_header",
|
"request_header",
|
||||||
"encode",
|
"encode",
|
||||||
"push",
|
"push",
|
||||||
|
"intercept",
|
||||||
"templates",
|
"templates",
|
||||||
|
|
||||||
// special routing & dispatching directives
|
// special routing & dispatching directives
|
||||||
|
"invoke",
|
||||||
"handle",
|
"handle",
|
||||||
"handle_path",
|
"handle_path",
|
||||||
"route",
|
"route",
|
||||||
@@ -65,6 +87,7 @@ var directiveOrder = []string{
|
|||||||
// handlers that typically respond to requests
|
// handlers that typically respond to requests
|
||||||
"abort",
|
"abort",
|
||||||
"error",
|
"error",
|
||||||
|
"copy_response", // only in reverse_proxy's handle_response
|
||||||
"respond",
|
"respond",
|
||||||
"metrics",
|
"metrics",
|
||||||
"reverse_proxy",
|
"reverse_proxy",
|
||||||
@@ -73,16 +96,10 @@ var directiveOrder = []string{
|
|||||||
"acme_server",
|
"acme_server",
|
||||||
}
|
}
|
||||||
|
|
||||||
// directiveIsOrdered returns true if dir is
|
// directiveOrder specifies the order to apply directives
|
||||||
// a known, ordered (sorted) directive.
|
// in HTTP routes, after being modified by either the
|
||||||
func directiveIsOrdered(dir string) bool {
|
// plugins or by the user via the "order" global option.
|
||||||
for _, d := range directiveOrder {
|
var directiveOrder = defaultDirectiveOrder
|
||||||
if d == dir {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterDirective registers a unique directive dir with an
|
// RegisterDirective registers a unique directive dir with an
|
||||||
// associated unmarshaling (setup) function. When directive dir
|
// associated unmarshaling (setup) function. When directive dir
|
||||||
@@ -119,6 +136,53 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterDirectiveOrder registers the default order for a
|
||||||
|
// directive from a plugin.
|
||||||
|
//
|
||||||
|
// This is useful when a plugin has a well-understood place
|
||||||
|
// it should run in the middleware pipeline, and it allows
|
||||||
|
// users to avoid having to define the order themselves.
|
||||||
|
//
|
||||||
|
// The directive dir may be placed in the position relative
|
||||||
|
// to ('before' or 'after') a directive included in Caddy's
|
||||||
|
// standard distribution. It cannot be relative to another
|
||||||
|
// plugin's directive.
|
||||||
|
//
|
||||||
|
// EXPERIMENTAL: This API may change or be removed.
|
||||||
|
func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
|
||||||
|
// check if directive was already ordered
|
||||||
|
if slices.Contains(directiveOrder, dir) {
|
||||||
|
panic("directive '" + dir + "' already ordered")
|
||||||
|
}
|
||||||
|
|
||||||
|
if position != Before && position != After {
|
||||||
|
panic("the 2nd argument must be either 'before' or 'after', got '" + position + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if directive exists in standard distribution, since
|
||||||
|
// we can't allow plugins to depend on one another; we can't
|
||||||
|
// guarantee the order that plugins are loaded in.
|
||||||
|
foundStandardDir := slices.Contains(defaultDirectiveOrder, standardDir)
|
||||||
|
if !foundStandardDir {
|
||||||
|
panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert directive into proper position
|
||||||
|
newOrder := directiveOrder
|
||||||
|
for i, d := range newOrder {
|
||||||
|
if d != standardDir {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if position == Before {
|
||||||
|
newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...)
|
||||||
|
} else if position == After {
|
||||||
|
newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
directiveOrder = newOrder
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterGlobalOption registers a unique global option opt with
|
// RegisterGlobalOption registers a unique global option opt with
|
||||||
// an associated unmarshaling (setup) function. When the global
|
// an associated unmarshaling (setup) function. When the global
|
||||||
// option opt is encountered in a Caddyfile, setupFunc will be
|
// option opt is encountered in a Caddyfile, setupFunc will be
|
||||||
@@ -135,8 +199,8 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
|
|||||||
type Helper struct {
|
type Helper struct {
|
||||||
*caddyfile.Dispenser
|
*caddyfile.Dispenser
|
||||||
// State stores intermediate variables during caddyfile adaptation.
|
// State stores intermediate variables during caddyfile adaptation.
|
||||||
State map[string]interface{}
|
State map[string]any
|
||||||
options map[string]interface{}
|
options map[string]any
|
||||||
warnings *[]caddyconfig.Warning
|
warnings *[]caddyconfig.Warning
|
||||||
matcherDefs map[string]caddy.ModuleMap
|
matcherDefs map[string]caddy.ModuleMap
|
||||||
parentBlock caddyfile.ServerBlock
|
parentBlock caddyfile.ServerBlock
|
||||||
@@ -144,7 +208,7 @@ type Helper struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Option gets the option keyed by name.
|
// Option gets the option keyed by name.
|
||||||
func (h Helper) Option(name string) interface{} {
|
func (h Helper) Option(name string) any {
|
||||||
return h.options[name]
|
return h.options[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +228,12 @@ func (h Helper) Caddyfiles() []string {
|
|||||||
for file := range files {
|
for file := range files {
|
||||||
filesSlice = append(filesSlice, file)
|
filesSlice = append(filesSlice, file)
|
||||||
}
|
}
|
||||||
|
sort.Strings(filesSlice)
|
||||||
return filesSlice
|
return filesSlice
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON converts val into JSON. Any errors are added to warnings.
|
// JSON converts val into JSON. Any errors are added to warnings.
|
||||||
func (h Helper) JSON(val interface{}) json.RawMessage {
|
func (h Helper) JSON(val any) json.RawMessage {
|
||||||
return caddyconfig.JSON(val, h.warnings)
|
return caddyconfig.JSON(val, h.warnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +272,8 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
|
|||||||
|
|
||||||
// NewRoute returns config values relevant to creating a new HTTP route.
|
// NewRoute returns config values relevant to creating a new HTTP route.
|
||||||
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
||||||
handler caddyhttp.MiddlewareHandler) []ConfigValue {
|
handler caddyhttp.MiddlewareHandler,
|
||||||
|
) []ConfigValue {
|
||||||
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
||||||
@@ -259,12 +325,6 @@ func (h Helper) GroupRoutes(vals []ConfigValue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBindAddresses returns config values relevant to adding
|
|
||||||
// listener bind addresses to the config.
|
|
||||||
func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
|
|
||||||
return []ConfigValue{{Class: "bind", Value: addrs}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDispenser returns a new instance based on d. All others Helper
|
// WithDispenser returns a new instance based on d. All others Helper
|
||||||
// fields are copied, so typically maps are shared with this new instance.
|
// fields are copied, so typically maps are shared with this new instance.
|
||||||
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
|
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
|
||||||
@@ -281,7 +341,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildSubroute(allResults, h.groupCounter)
|
return buildSubroute(allResults, h.groupCounter, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSegmentAsConfig parses the segment such that its subdirectives
|
// parseSegmentAsConfig parses the segment such that its subdirectives
|
||||||
@@ -340,6 +400,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dir = normalizeDirectiveName(dir)
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
result.directive = dir
|
result.directive = dir
|
||||||
allResults = append(allResults, result)
|
allResults = append(allResults, result)
|
||||||
@@ -365,7 +428,7 @@ type ConfigValue struct {
|
|||||||
// The value to be used when building the config.
|
// The value to be used when building the config.
|
||||||
// Generally its type is associated with the
|
// Generally its type is associated with the
|
||||||
// name of the Class.
|
// name of the Class.
|
||||||
Value interface{}
|
Value any
|
||||||
|
|
||||||
directive string
|
directive string
|
||||||
}
|
}
|
||||||
@@ -396,7 +459,7 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode the path matchers, if there is just one of them
|
// decode the path matchers if there is just one matcher set
|
||||||
var iPM, jPM caddyhttp.MatchPath
|
var iPM, jPM caddyhttp.MatchPath
|
||||||
if len(iRoute.MatcherSetsRaw) == 1 {
|
if len(iRoute.MatcherSetsRaw) == 1 {
|
||||||
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
|
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
|
||||||
@@ -405,24 +468,47 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
|
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort by longer path (more specific) first; missing path
|
// if there is only one path in the path matcher, sort by longer path
|
||||||
// matchers or multi-matchers are treated as zero-length paths
|
// (more specific) first; missing path matchers or multi-matchers are
|
||||||
|
// treated as zero-length paths
|
||||||
var iPathLen, jPathLen int
|
var iPathLen, jPathLen int
|
||||||
if len(iPM) > 0 {
|
if len(iPM) == 1 {
|
||||||
iPathLen = len(iPM[0])
|
iPathLen = len(iPM[0])
|
||||||
}
|
}
|
||||||
if len(jPM) > 0 {
|
if len(jPM) == 1 {
|
||||||
jPathLen = len(jPM[0])
|
jPathLen = len(jPM[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// if both directives have no path matcher, use whichever one
|
sortByPath := func() bool {
|
||||||
// has any kind of matcher defined first.
|
// we can only confidently compare path lengths if both
|
||||||
if iPathLen == 0 && jPathLen == 0 {
|
// directives have a single path to match (issue #5037)
|
||||||
|
if iPathLen > 0 && jPathLen > 0 {
|
||||||
|
// if both paths are the same except for a trailing wildcard,
|
||||||
|
// sort by the shorter path first (which is more specific)
|
||||||
|
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") {
|
||||||
|
return iPathLen < jPathLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort most-specific (longest) path first
|
||||||
|
return iPathLen > jPathLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// if both directives don't have a single path to compare,
|
||||||
|
// sort whichever one has a matcher first; if both have
|
||||||
|
// a matcher, sort equally (stable sort preserves order)
|
||||||
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
||||||
|
}()
|
||||||
|
|
||||||
|
// some directives involve setting values which can overwrite
|
||||||
|
// each other, so it makes most sense to reverse the order so
|
||||||
|
// that the least-specific matcher is first, allowing the last
|
||||||
|
// matching one to win
|
||||||
|
if iDir == "vars" {
|
||||||
|
return !sortByPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort with the most-specific (longest) path first
|
// everything else is most-specific matcher first
|
||||||
return iPathLen > jPathLen
|
return sortByPath
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,9 +516,9 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
// a "pile" of config values, keyed by class name,
|
// a "pile" of config values, keyed by class name,
|
||||||
// as well as its parsed keys for convenience.
|
// as well as its parsed keys for convenience.
|
||||||
type serverBlock struct {
|
type serverBlock struct {
|
||||||
block caddyfile.ServerBlock
|
block caddyfile.ServerBlock
|
||||||
pile map[string][]ConfigValue // config values obtained from directives
|
pile map[string][]ConfigValue // config values obtained from directives
|
||||||
keys []Address
|
parsedKeys []Address
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostsFromKeys returns a list of all the non-empty hostnames found in
|
// hostsFromKeys returns a list of all the non-empty hostnames found in
|
||||||
@@ -449,7 +535,7 @@ type serverBlock struct {
|
|||||||
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
||||||
// ensure each entry in our list is unique
|
// ensure each entry in our list is unique
|
||||||
hostMap := make(map[string]struct{})
|
hostMap := make(map[string]struct{})
|
||||||
for _, addr := range sb.keys {
|
for _, addr := range sb.parsedKeys {
|
||||||
if addr.Host == "" {
|
if addr.Host == "" {
|
||||||
if !loggerMode {
|
if !loggerMode {
|
||||||
// server block contains a key like ":443", i.e. the host portion
|
// server block contains a key like ":443", i.e. the host portion
|
||||||
@@ -481,7 +567,7 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
|||||||
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
||||||
// ensure each entry in our list is unique
|
// ensure each entry in our list is unique
|
||||||
hostMap := make(map[string]struct{})
|
hostMap := make(map[string]struct{})
|
||||||
for _, addr := range sb.keys {
|
for _, addr := range sb.parsedKeys {
|
||||||
if addr.Host == "" {
|
if addr.Host == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -502,14 +588,29 @@ func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
|||||||
// hasHostCatchAllKey returns true if sb has a key that
|
// hasHostCatchAllKey returns true if sb has a key that
|
||||||
// omits a host portion, i.e. it "catches all" hosts.
|
// omits a host portion, i.e. it "catches all" hosts.
|
||||||
func (sb serverBlock) hasHostCatchAllKey() bool {
|
func (sb serverBlock) hasHostCatchAllKey() bool {
|
||||||
for _, addr := range sb.keys {
|
return slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
|
||||||
if addr.Host == "" {
|
return addr.Host == ""
|
||||||
return true
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAllHTTP returns true if all sb keys explicitly specify
|
||||||
|
// the http:// scheme
|
||||||
|
func (sb serverBlock) isAllHTTP() bool {
|
||||||
|
return !slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
|
||||||
|
return addr.Scheme != "http"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positional are the supported modes for ordering directives.
|
||||||
|
type Positional string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Before Positional = "before"
|
||||||
|
After Positional = "after"
|
||||||
|
First Positional = "first"
|
||||||
|
Last Positional = "last"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// UnmarshalFunc is a function which can unmarshal Caddyfile
|
// UnmarshalFunc is a function which can unmarshal Caddyfile
|
||||||
// tokens into zero or more config values using a Helper type.
|
// tokens into zero or more config values using a Helper type.
|
||||||
@@ -531,7 +632,7 @@ type (
|
|||||||
// tokens from a global option. It is passed the tokens to parse and
|
// tokens from a global option. It is passed the tokens to parse and
|
||||||
// existing value from the previous instance of this global option
|
// existing value from the previous instance of this global option
|
||||||
// (if any). It returns the value to associate with this global option.
|
// (if any). It returns the value to associate with this global option.
|
||||||
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error)
|
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal any) (any, error)
|
||||||
)
|
)
|
||||||
|
|
||||||
var registeredDirectives = make(map[string]UnmarshalFunc)
|
var registeredDirectives = make(map[string]UnmarshalFunc)
|
||||||
|
|||||||
@@ -31,20 +31,23 @@ func TestHostsFromKeys(t *testing.T) {
|
|||||||
[]Address{
|
[]Address{
|
||||||
{Original: ":2015", Port: "2015"},
|
{Original: ":2015", Port: "2015"},
|
||||||
},
|
},
|
||||||
[]string{}, []string{},
|
[]string{},
|
||||||
|
[]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[]Address{
|
[]Address{
|
||||||
{Original: ":443", Port: "443"},
|
{Original: ":443", Port: "443"},
|
||||||
},
|
},
|
||||||
[]string{}, []string{},
|
[]string{},
|
||||||
|
[]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[]Address{
|
[]Address{
|
||||||
{Original: "foo", Host: "foo"},
|
{Original: "foo", Host: "foo"},
|
||||||
{Original: ":2015", Port: "2015"},
|
{Original: ":2015", Port: "2015"},
|
||||||
},
|
},
|
||||||
[]string{}, []string{"foo"},
|
[]string{},
|
||||||
|
[]string{"foo"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[]Address{
|
[]Address{
|
||||||
@@ -75,7 +78,7 @@ func TestHostsFromKeys(t *testing.T) {
|
|||||||
[]string{"example.com:2015"},
|
[]string{"example.com:2015"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
sb := serverBlock{keys: tc.keys}
|
sb := serverBlock{parsedKeys: tc.keys}
|
||||||
|
|
||||||
// test in normal mode
|
// test in normal mode
|
||||||
actual := sb.hostsFromKeys(false)
|
actual := sb.hostsFromKeys(false)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,25 +15,32 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterGlobalOption("debug", parseOptTrue)
|
RegisterGlobalOption("debug", parseOptTrue)
|
||||||
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
||||||
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
||||||
|
RegisterGlobalOption("default_bind", parseOptDefaultBind)
|
||||||
RegisterGlobalOption("grace_period", parseOptDuration)
|
RegisterGlobalOption("grace_period", parseOptDuration)
|
||||||
|
RegisterGlobalOption("shutdown_delay", parseOptDuration)
|
||||||
RegisterGlobalOption("default_sni", parseOptSingleString)
|
RegisterGlobalOption("default_sni", parseOptSingleString)
|
||||||
|
RegisterGlobalOption("fallback_sni", parseOptSingleString)
|
||||||
RegisterGlobalOption("order", parseOptOrder)
|
RegisterGlobalOption("order", parseOptOrder)
|
||||||
RegisterGlobalOption("storage", parseOptStorage)
|
RegisterGlobalOption("storage", parseOptStorage)
|
||||||
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
|
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
|
||||||
|
RegisterGlobalOption("renew_interval", parseOptDuration)
|
||||||
|
RegisterGlobalOption("ocsp_interval", parseOptDuration)
|
||||||
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
||||||
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
||||||
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
|
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
|
||||||
@@ -48,121 +55,120 @@ func init() {
|
|||||||
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
||||||
RegisterGlobalOption("servers", parseServerOptions)
|
RegisterGlobalOption("servers", parseServerOptions)
|
||||||
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
||||||
|
RegisterGlobalOption("cert_lifetime", parseOptDuration)
|
||||||
RegisterGlobalOption("log", parseLogOptions)
|
RegisterGlobalOption("log", parseLogOptions)
|
||||||
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
||||||
|
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
|
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
|
||||||
|
|
||||||
func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
var httpPort int
|
var httpPort int
|
||||||
for d.Next() {
|
var httpPortStr string
|
||||||
var httpPortStr string
|
if !d.AllArgs(&httpPortStr) {
|
||||||
if !d.AllArgs(&httpPortStr) {
|
return 0, d.ArgErr()
|
||||||
return 0, d.ArgErr()
|
}
|
||||||
}
|
var err error
|
||||||
var err error
|
httpPort, err = strconv.Atoi(httpPortStr)
|
||||||
httpPort, err = strconv.Atoi(httpPortStr)
|
if err != nil {
|
||||||
if err != nil {
|
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
|
||||||
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return httpPort, nil
|
return httpPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
var httpsPort int
|
var httpsPort int
|
||||||
for d.Next() {
|
var httpsPortStr string
|
||||||
var httpsPortStr string
|
if !d.AllArgs(&httpsPortStr) {
|
||||||
if !d.AllArgs(&httpsPortStr) {
|
return 0, d.ArgErr()
|
||||||
return 0, d.ArgErr()
|
}
|
||||||
}
|
var err error
|
||||||
var err error
|
httpsPort, err = strconv.Atoi(httpsPortStr)
|
||||||
httpsPort, err = strconv.Atoi(httpsPortStr)
|
if err != nil {
|
||||||
if err != nil {
|
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
|
||||||
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return httpsPort, nil
|
return httpsPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
newOrder := directiveOrder
|
d.Next() // consume option name
|
||||||
|
|
||||||
for d.Next() {
|
// get directive name
|
||||||
// get directive name
|
if !d.Next() {
|
||||||
if !d.Next() {
|
return nil, d.ArgErr()
|
||||||
return nil, d.ArgErr()
|
}
|
||||||
}
|
dirName := d.Val()
|
||||||
dirName := d.Val()
|
if _, ok := registeredDirectives[dirName]; !ok {
|
||||||
if _, ok := registeredDirectives[dirName]; !ok {
|
return nil, d.Errf("%s is not a registered directive", dirName)
|
||||||
return nil, d.Errf("%s is not a registered directive", dirName)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// get positional token
|
// get positional token
|
||||||
if !d.Next() {
|
if !d.Next() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
pos := d.Val()
|
pos := Positional(d.Val())
|
||||||
|
|
||||||
// if directive exists, first remove it
|
// if directive already had an order, drop it
|
||||||
for i, d := range newOrder {
|
newOrder := slices.DeleteFunc(directiveOrder, func(d string) bool {
|
||||||
if d == dirName {
|
return d == dirName
|
||||||
newOrder = append(newOrder[:i], newOrder[i+1:]...)
|
})
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// act on the positional
|
// act on the positional; if it's First or Last, we're done right away
|
||||||
switch pos {
|
switch pos {
|
||||||
case "first":
|
case First:
|
||||||
newOrder = append([]string{dirName}, newOrder...)
|
newOrder = append([]string{dirName}, newOrder...)
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
directiveOrder = newOrder
|
|
||||||
return newOrder, nil
|
|
||||||
case "last":
|
|
||||||
newOrder = append(newOrder, dirName)
|
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
directiveOrder = newOrder
|
|
||||||
return newOrder, nil
|
|
||||||
case "before":
|
|
||||||
case "after":
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unknown positional '%s'", pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get name of other directive
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
otherDir := d.Val()
|
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
|
directiveOrder = newOrder
|
||||||
|
return newOrder, nil
|
||||||
|
|
||||||
// insert directive into proper position
|
case Last:
|
||||||
for i, d := range newOrder {
|
newOrder = append(newOrder, dirName)
|
||||||
if d == otherDir {
|
if d.NextArg() {
|
||||||
if pos == "before" {
|
return nil, d.ArgErr()
|
||||||
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
|
|
||||||
} else if pos == "after" {
|
|
||||||
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
directiveOrder = newOrder
|
||||||
|
return newOrder, nil
|
||||||
|
|
||||||
|
// if it's Before or After, continue
|
||||||
|
case Before:
|
||||||
|
case After:
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unknown positional '%s'", pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get name of other directive
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
otherDir := d.Val()
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the position of the target directive
|
||||||
|
targetIndex := slices.Index(newOrder, otherDir)
|
||||||
|
if targetIndex == -1 {
|
||||||
|
return nil, d.Errf("directive '%s' not found", otherDir)
|
||||||
|
}
|
||||||
|
// if we're inserting after, we need to increment the index to go after
|
||||||
|
if pos == After {
|
||||||
|
targetIndex++
|
||||||
|
}
|
||||||
|
// insert the directive into the new order
|
||||||
|
newOrder = slices.Insert(newOrder, targetIndex, dirName)
|
||||||
|
|
||||||
directiveOrder = newOrder
|
directiveOrder = newOrder
|
||||||
|
|
||||||
return newOrder, nil
|
return newOrder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
if !d.Next() { // consume option name
|
if !d.Next() { // consume option name
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -181,7 +187,7 @@ func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
|
|||||||
return storage, nil
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
if !d.Next() { // consume option name
|
if !d.Next() { // consume option name
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -195,7 +201,7 @@ func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
|
|||||||
return caddy.Duration(dur), nil
|
return caddy.Duration(dur), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
if !d.Next() { // consume option name
|
if !d.Next() { // consume option name
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -207,66 +213,67 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
prov, ok := unm.(certmagic.ACMEDNSProvider)
|
prov, ok := unm.(certmagic.DNSProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
|
return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm)
|
||||||
}
|
}
|
||||||
return prov, nil
|
return prov, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
eab := new(acme.EAB)
|
eab := new(acme.EAB)
|
||||||
for d.Next() {
|
d.Next() // consume option name
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
for d.NextBlock(0) {
|
||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
case "key_id":
|
case "key_id":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
|
||||||
eab.KeyID = d.Val()
|
|
||||||
|
|
||||||
case "mac_key":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
eab.MACKey = d.Val()
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
|
||||||
}
|
}
|
||||||
|
eab.KeyID = d.Val()
|
||||||
|
|
||||||
|
case "mac_key":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
eab.MACKey = d.Val()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return eab, nil
|
return eab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
|
func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
|
||||||
var issuers []certmagic.Issuer
|
var issuers []certmagic.Issuer
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
issuers = existing.([]certmagic.Issuer)
|
issuers = existing.([]certmagic.Issuer)
|
||||||
}
|
}
|
||||||
for d.Next() { // consume option name
|
|
||||||
if !d.Next() { // get issuer module name
|
// get issuer module name
|
||||||
return nil, d.ArgErr()
|
if !d.Next() {
|
||||||
}
|
return nil, d.ArgErr()
|
||||||
modID := "tls.issuance." + d.Val()
|
|
||||||
unm, err := caddyfile.UnmarshalModule(d, modID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
iss, ok := unm.(certmagic.Issuer)
|
|
||||||
if !ok {
|
|
||||||
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
|
|
||||||
}
|
|
||||||
issuers = append(issuers, iss)
|
|
||||||
}
|
}
|
||||||
|
modID := "tls.issuance." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iss, ok := unm.(certmagic.Issuer)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
|
||||||
|
}
|
||||||
|
issuers = append(issuers, iss)
|
||||||
return issuers, nil
|
return issuers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume parameter name
|
d.Next() // consume option name
|
||||||
if !d.Next() {
|
if !d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -277,34 +284,62 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, e
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
|
||||||
|
var addresses, protocols []string
|
||||||
|
addresses = d.RemainingArgs()
|
||||||
|
|
||||||
|
if len(addresses) == 0 {
|
||||||
|
addresses = append(addresses, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
for d.NextBlock(0) {
|
||||||
|
switch d.Val() {
|
||||||
|
case "protocols":
|
||||||
|
protocols = d.RemainingArgs()
|
||||||
|
if len(protocols) == 0 {
|
||||||
|
return nil, d.Errf("protocols requires one or more arguments")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unknown subdirective: %s", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
|
||||||
|
addresses: addresses,
|
||||||
|
protocols: protocols,
|
||||||
|
}}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
|
||||||
adminCfg := new(caddy.AdminConfig)
|
adminCfg := new(caddy.AdminConfig)
|
||||||
for d.Next() {
|
if d.NextArg() {
|
||||||
if d.NextArg() {
|
listenAddress := d.Val()
|
||||||
listenAddress := d.Val()
|
if listenAddress == "off" {
|
||||||
if listenAddress == "off" {
|
adminCfg.Disabled = true
|
||||||
adminCfg.Disabled = true
|
if d.Next() { // Do not accept any remaining options including block
|
||||||
if d.Next() { // Do not accept any remaining options including block
|
return nil, d.Err("No more option is allowed after turning off admin config")
|
||||||
return nil, d.Err("No more option is allowed after turning off admin config")
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
adminCfg.Listen = listenAddress
|
||||||
adminCfg.Listen = listenAddress
|
if d.NextArg() { // At most 1 arg is allowed
|
||||||
if d.NextArg() { // At most 1 arg is allowed
|
return nil, d.ArgErr()
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
}
|
||||||
switch d.Val() {
|
for d.NextBlock(0) {
|
||||||
case "enforce_origin":
|
switch d.Val() {
|
||||||
adminCfg.EnforceOrigin = true
|
case "enforce_origin":
|
||||||
|
adminCfg.EnforceOrigin = true
|
||||||
|
|
||||||
case "origins":
|
case "origins":
|
||||||
adminCfg.Origins = d.RemainingArgs()
|
adminCfg.Origins = d.RemainingArgs()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if adminCfg.Listen == "" && !adminCfg.Disabled {
|
if adminCfg.Listen == "" && !adminCfg.Disabled {
|
||||||
@@ -313,58 +348,59 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
|||||||
return adminCfg, nil
|
return adminCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
var ond *caddytls.OnDemandConfig
|
var ond *caddytls.OnDemandConfig
|
||||||
for d.Next() {
|
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
switch d.Val() {
|
|
||||||
case "ask":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
if ond == nil {
|
|
||||||
ond = new(caddytls.OnDemandConfig)
|
|
||||||
}
|
|
||||||
ond.Ask = d.Val()
|
|
||||||
|
|
||||||
case "interval":
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
if !d.NextArg() {
|
switch d.Val() {
|
||||||
return nil, d.ArgErr()
|
case "ask":
|
||||||
}
|
if !d.NextArg() {
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
return nil, d.ArgErr()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ond == nil {
|
|
||||||
ond = new(caddytls.OnDemandConfig)
|
|
||||||
}
|
|
||||||
if ond.RateLimit == nil {
|
|
||||||
ond.RateLimit = new(caddytls.RateLimit)
|
|
||||||
}
|
|
||||||
ond.RateLimit.Interval = caddy.Duration(dur)
|
|
||||||
|
|
||||||
case "burst":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
burst, err := strconv.Atoi(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ond == nil {
|
|
||||||
ond = new(caddytls.OnDemandConfig)
|
|
||||||
}
|
|
||||||
if ond.RateLimit == nil {
|
|
||||||
ond.RateLimit = new(caddytls.RateLimit)
|
|
||||||
}
|
|
||||||
ond.RateLimit.Burst = burst
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
|
||||||
}
|
}
|
||||||
|
if ond == nil {
|
||||||
|
ond = new(caddytls.OnDemandConfig)
|
||||||
|
}
|
||||||
|
if ond.PermissionRaw != nil {
|
||||||
|
return nil, d.Err("on-demand TLS permission module (or 'ask') already specified")
|
||||||
|
}
|
||||||
|
perm := caddytls.PermissionByHTTP{Endpoint: d.Val()}
|
||||||
|
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", "http", nil)
|
||||||
|
|
||||||
|
case "permission":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
if ond == nil {
|
||||||
|
ond = new(caddytls.OnDemandConfig)
|
||||||
|
}
|
||||||
|
if ond.PermissionRaw != nil {
|
||||||
|
return nil, d.Err("on-demand TLS permission module (or 'ask') already specified")
|
||||||
|
}
|
||||||
|
modName := d.Val()
|
||||||
|
modID := "tls.permission." + modName
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
perm, ok := unm.(caddytls.OnDemandPermission)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("module %s (%T) is not an on-demand TLS permission module", modID, unm)
|
||||||
|
}
|
||||||
|
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", modName, nil)
|
||||||
|
|
||||||
|
case "interval":
|
||||||
|
return nil, d.Errf("the on_demand_tls 'interval' option is no longer supported, remove it from your config")
|
||||||
|
|
||||||
|
case "burst":
|
||||||
|
return nil, d.Errf("the on_demand_tls 'burst' option is no longer supported, remove it from your config")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ond == nil {
|
if ond == nil {
|
||||||
@@ -373,8 +409,8 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error
|
|||||||
return ond, nil
|
return ond, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume parameter name
|
d.Next() // consume option name
|
||||||
if !d.Next() {
|
if !d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
}
|
}
|
||||||
@@ -382,17 +418,39 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
|
|||||||
if d.Next() {
|
if d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
}
|
}
|
||||||
if val != "off" && val != "disable_redirects" && val != "ignore_loaded_certs" {
|
if val != "off" {
|
||||||
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects' or 'ignore_loaded_certs'")
|
return "", d.Errf("persist_config must be 'off'")
|
||||||
}
|
}
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
val := d.RemainingArgs()
|
||||||
|
if len(val) == 0 {
|
||||||
|
return "", d.ArgErr()
|
||||||
|
}
|
||||||
|
for _, v := range val {
|
||||||
|
switch v {
|
||||||
|
case "off":
|
||||||
|
case "disable_redirects":
|
||||||
|
case "disable_certs":
|
||||||
|
case "ignore_loaded_certs":
|
||||||
|
case "prefer_wildcard":
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
return unmarshalCaddyfileServerOptions(d)
|
return unmarshalCaddyfileServerOptions(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
var val string
|
var val string
|
||||||
if !d.AllArgs(&val) {
|
if !d.AllArgs(&val) {
|
||||||
@@ -408,18 +466,17 @@ func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{
|
|||||||
|
|
||||||
// parseLogOptions parses the global log option. Syntax:
|
// parseLogOptions parses the global log option. Syntax:
|
||||||
//
|
//
|
||||||
// log [name] {
|
// log [name] {
|
||||||
// output <writer_module> ...
|
// output <writer_module> ...
|
||||||
// format <encoder_module> ...
|
// format <encoder_module> ...
|
||||||
// level <level>
|
// level <level>
|
||||||
// include <namespaces...>
|
// include <namespaces...>
|
||||||
// exclude <namespaces...>
|
// exclude <namespaces...>
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// When the name argument is unspecified, this directive modifies the default
|
// When the name argument is unspecified, this directive modifies the default
|
||||||
// logger.
|
// logger.
|
||||||
//
|
func parseLogOptions(d *caddyfile.Dispenser, existingVal any) (any, error) {
|
||||||
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
|
|
||||||
currentNames := make(map[string]struct{})
|
currentNames := make(map[string]struct{})
|
||||||
if existingVal != nil {
|
if existingVal != nil {
|
||||||
innerVals, ok := existingVal.([]ConfigValue)
|
innerVals, ok := existingVal.([]ConfigValue)
|
||||||
@@ -454,7 +511,7 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface
|
|||||||
return configValues, nil
|
return configValues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next()
|
d.Next()
|
||||||
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
|
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,24 +15,187 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterGlobalOption("pki", parsePKIApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePKIApp parses the global log option. Syntax:
|
||||||
|
//
|
||||||
|
// pki {
|
||||||
|
// ca [<id>] {
|
||||||
|
// name <name>
|
||||||
|
// root_cn <name>
|
||||||
|
// intermediate_cn <name>
|
||||||
|
// intermediate_lifetime <duration>
|
||||||
|
// root {
|
||||||
|
// cert <path>
|
||||||
|
// key <path>
|
||||||
|
// format <format>
|
||||||
|
// }
|
||||||
|
// intermediate {
|
||||||
|
// cert <path>
|
||||||
|
// key <path>
|
||||||
|
// format <format>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// When the CA ID is unspecified, 'local' is assumed.
|
||||||
|
func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
|
||||||
|
d.Next() // consume app name
|
||||||
|
|
||||||
|
pki := &caddypki.PKI{
|
||||||
|
CAs: make(map[string]*caddypki.CA),
|
||||||
|
}
|
||||||
|
for d.NextBlock(0) {
|
||||||
|
switch d.Val() {
|
||||||
|
case "ca":
|
||||||
|
pkiCa := new(caddypki.CA)
|
||||||
|
if d.NextArg() {
|
||||||
|
pkiCa.ID = d.Val()
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pkiCa.ID == "" {
|
||||||
|
pkiCa.ID = caddypki.DefaultCAID
|
||||||
|
}
|
||||||
|
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "name":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Name = d.Val()
|
||||||
|
|
||||||
|
case "root_cn":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.RootCommonName = d.Val()
|
||||||
|
|
||||||
|
case "intermediate_cn":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.IntermediateCommonName = d.Val()
|
||||||
|
|
||||||
|
case "intermediate_lifetime":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pkiCa.IntermediateLifetime = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "root":
|
||||||
|
if pkiCa.Root == nil {
|
||||||
|
pkiCa.Root = new(caddypki.KeyPair)
|
||||||
|
}
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "cert":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Root.Certificate = d.Val()
|
||||||
|
|
||||||
|
case "key":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Root.PrivateKey = d.Val()
|
||||||
|
|
||||||
|
case "format":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Root.Format = d.Val()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "intermediate":
|
||||||
|
if pkiCa.Intermediate == nil {
|
||||||
|
pkiCa.Intermediate = new(caddypki.KeyPair)
|
||||||
|
}
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "cert":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Intermediate.Certificate = d.Val()
|
||||||
|
|
||||||
|
case "key":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Intermediate.PrivateKey = d.Val()
|
||||||
|
|
||||||
|
case "format":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Intermediate.Format = d.Val()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pki.CAs[pkiCa.ID] = pkiCa
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pki, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (st ServerType) buildPKIApp(
|
func (st ServerType) buildPKIApp(
|
||||||
pairings []sbAddrAssociation,
|
pairings []sbAddrAssociation,
|
||||||
options map[string]interface{},
|
options map[string]any,
|
||||||
warnings []caddyconfig.Warning,
|
warnings []caddyconfig.Warning,
|
||||||
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||||
|
|
||||||
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
|
||||||
|
|
||||||
skipInstallTrust := false
|
skipInstallTrust := false
|
||||||
if _, ok := options["skip_install_trust"]; ok {
|
if _, ok := options["skip_install_trust"]; ok {
|
||||||
skipInstallTrust = true
|
skipInstallTrust = true
|
||||||
}
|
}
|
||||||
falseBool := false
|
falseBool := false
|
||||||
|
|
||||||
|
// Load the PKI app configured via global options
|
||||||
|
var pkiApp *caddypki.PKI
|
||||||
|
unwrappedPki, ok := options["pki"].(*caddypki.PKI)
|
||||||
|
if ok {
|
||||||
|
pkiApp = unwrappedPki
|
||||||
|
} else {
|
||||||
|
pkiApp = &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||||
|
}
|
||||||
|
for _, ca := range pkiApp.CAs {
|
||||||
|
if skipInstallTrust {
|
||||||
|
ca.InstallTrust = &falseBool
|
||||||
|
}
|
||||||
|
pkiApp.CAs[ca.ID] = ca
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in the CAs configured via directives
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
for _, sblock := range p.serverBlocks {
|
for _, sblock := range p.serverBlocks {
|
||||||
// find all the CAs that were defined and add them to the app config
|
// find all the CAs that were defined and add them to the app config
|
||||||
@@ -42,7 +205,12 @@ func (st ServerType) buildPKIApp(
|
|||||||
if skipInstallTrust {
|
if skipInstallTrust {
|
||||||
ca.InstallTrust = &falseBool
|
ca.InstallTrust = &falseBool
|
||||||
}
|
}
|
||||||
pkiApp.CAs[ca.ID] = ca
|
|
||||||
|
// the CA might already exist from global options, so
|
||||||
|
// don't overwrite it in that case
|
||||||
|
if _, ok := pkiApp.CAs[ca.ID]; !ok {
|
||||||
|
pkiApp.CAs[ca.ID] = ca
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// serverOptions collects server config overrides parsed from Caddyfile global options
|
// serverOptions collects server config overrides parsed from Caddyfile global options
|
||||||
@@ -33,137 +35,227 @@ type serverOptions struct {
|
|||||||
ListenerAddress string
|
ListenerAddress string
|
||||||
|
|
||||||
// These will all map 1:1 to the caddyhttp.Server struct
|
// These will all map 1:1 to the caddyhttp.Server struct
|
||||||
ListenerWrappersRaw []json.RawMessage
|
Name string
|
||||||
ReadTimeout caddy.Duration
|
ListenerWrappersRaw []json.RawMessage
|
||||||
ReadHeaderTimeout caddy.Duration
|
ReadTimeout caddy.Duration
|
||||||
WriteTimeout caddy.Duration
|
ReadHeaderTimeout caddy.Duration
|
||||||
IdleTimeout caddy.Duration
|
WriteTimeout caddy.Duration
|
||||||
MaxHeaderBytes int
|
IdleTimeout caddy.Duration
|
||||||
AllowH2C bool
|
KeepAliveInterval caddy.Duration
|
||||||
ExperimentalHTTP3 bool
|
MaxHeaderBytes int
|
||||||
StrictSNIHost *bool
|
EnableFullDuplex bool
|
||||||
|
Protocols []string
|
||||||
|
StrictSNIHost *bool
|
||||||
|
TrustedProxiesRaw json.RawMessage
|
||||||
|
TrustedProxiesStrict int
|
||||||
|
ClientIPHeaders []string
|
||||||
|
ShouldLogCredentials bool
|
||||||
|
Metrics *caddyhttp.Metrics
|
||||||
|
Trace bool // TODO: EXPERIMENTAL
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
|
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
|
||||||
serverOpts := serverOptions{}
|
serverOpts := serverOptions{}
|
||||||
for d.Next() {
|
if d.NextArg() {
|
||||||
|
serverOpts.ListenerAddress = d.Val()
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
serverOpts.ListenerAddress = d.Val()
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for d.NextBlock(0) {
|
||||||
|
switch d.Val() {
|
||||||
|
case "name":
|
||||||
|
if serverOpts.ListenerAddress == "" {
|
||||||
|
return nil, d.Errf("cannot set a name for a server without a listener address")
|
||||||
|
}
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.Name = d.Val()
|
||||||
|
|
||||||
|
case "listener_wrappers":
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
modID := "caddy.listeners." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
|
||||||
|
}
|
||||||
|
jsonListenerWrapper := caddyconfig.JSONModuleObject(
|
||||||
|
listenerWrapper,
|
||||||
|
"wrapper",
|
||||||
|
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "timeouts":
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "read_body":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing read_body timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.ReadTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "read_header":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing read_header timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "write":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing write timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.WriteTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "idle":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing idle timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.IdleTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "keepalive_interval":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing keepalive interval duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.KeepAliveInterval = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "max_header_size":
|
||||||
|
var sizeStr string
|
||||||
|
if !d.AllArgs(&sizeStr) {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
size, err := humanize.ParseBytes(sizeStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing max_header_size: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.MaxHeaderBytes = int(size)
|
||||||
|
|
||||||
|
case "enable_full_duplex":
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
}
|
serverOpts.EnableFullDuplex = true
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
switch d.Val() {
|
|
||||||
case "listener_wrappers":
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
modID := "caddy.listeners." + d.Val()
|
|
||||||
unm, err := caddyfile.UnmarshalModule(d, modID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
|
|
||||||
}
|
|
||||||
jsonListenerWrapper := caddyconfig.JSONModuleObject(
|
|
||||||
listenerWrapper,
|
|
||||||
"wrapper",
|
|
||||||
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "timeouts":
|
case "log_credentials":
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
if d.NextArg() {
|
||||||
switch d.Val() {
|
return nil, d.ArgErr()
|
||||||
case "read_body":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing read_body timeout duration: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.ReadTimeout = caddy.Duration(dur)
|
|
||||||
|
|
||||||
case "read_header":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing read_header timeout duration: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
|
|
||||||
|
|
||||||
case "write":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing write timeout duration: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.WriteTimeout = caddy.Duration(dur)
|
|
||||||
|
|
||||||
case "idle":
|
|
||||||
if !d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
dur, err := caddy.ParseDuration(d.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing idle timeout duration: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.IdleTimeout = caddy.Duration(dur)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "max_header_size":
|
|
||||||
var sizeStr string
|
|
||||||
if !d.AllArgs(&sizeStr) {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
size, err := humanize.ParseBytes(sizeStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("parsing max_header_size: %v", err)
|
|
||||||
}
|
|
||||||
serverOpts.MaxHeaderBytes = int(size)
|
|
||||||
|
|
||||||
case "protocol":
|
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
||||||
switch d.Val() {
|
|
||||||
case "allow_h2c":
|
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
serverOpts.AllowH2C = true
|
|
||||||
|
|
||||||
case "experimental_http3":
|
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
serverOpts.ExperimentalHTTP3 = true
|
|
||||||
|
|
||||||
case "strict_sni_host":
|
|
||||||
if d.NextArg() {
|
|
||||||
return nil, d.ArgErr()
|
|
||||||
}
|
|
||||||
trueBool := true
|
|
||||||
serverOpts.StrictSNIHost = &trueBool
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
|
||||||
}
|
}
|
||||||
|
serverOpts.ShouldLogCredentials = true
|
||||||
|
|
||||||
|
case "protocols":
|
||||||
|
protos := d.RemainingArgs()
|
||||||
|
for _, proto := range protos {
|
||||||
|
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
|
||||||
|
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
|
||||||
|
}
|
||||||
|
if slices.Contains(serverOpts.Protocols, proto) {
|
||||||
|
return nil, d.Errf("protocol %s specified more than once", proto)
|
||||||
|
}
|
||||||
|
serverOpts.Protocols = append(serverOpts.Protocols, proto)
|
||||||
|
}
|
||||||
|
if nesting := d.Nesting(); d.NextBlock(nesting) {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "strict_sni_host":
|
||||||
|
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
|
||||||
|
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
|
||||||
|
}
|
||||||
|
boolVal := true
|
||||||
|
if d.Val() == "insecure_off" {
|
||||||
|
boolVal = false
|
||||||
|
}
|
||||||
|
serverOpts.StrictSNIHost = &boolVal
|
||||||
|
|
||||||
|
case "trusted_proxies":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
|
||||||
|
}
|
||||||
|
modID := "http.ip_sources." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
source, ok := unm.(caddyhttp.IPRangeSource)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
|
||||||
|
}
|
||||||
|
jsonSource := caddyconfig.JSONModuleObject(
|
||||||
|
source,
|
||||||
|
"source",
|
||||||
|
source.(caddy.Module).CaddyModule().ID.Name(),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
serverOpts.TrustedProxiesRaw = jsonSource
|
||||||
|
|
||||||
|
case "trusted_proxies_strict":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.TrustedProxiesStrict = 1
|
||||||
|
|
||||||
|
case "client_ip_headers":
|
||||||
|
headers := d.RemainingArgs()
|
||||||
|
for _, header := range headers {
|
||||||
|
if slices.Contains(serverOpts.ClientIPHeaders, header) {
|
||||||
|
return nil, d.Errf("client IP header %s specified more than once", header)
|
||||||
|
}
|
||||||
|
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
|
||||||
|
}
|
||||||
|
if nesting := d.Nesting(); d.NextBlock(nesting) {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "metrics":
|
||||||
|
serverOpts.Metrics = new(caddyhttp.Metrics)
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "per_host":
|
||||||
|
serverOpts.Metrics.PerHost = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "trace":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.Trace = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return serverOpts, nil
|
return serverOpts, nil
|
||||||
@@ -172,45 +264,40 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
|
|||||||
// applyServerOptions sets the server options on the appropriate servers
|
// applyServerOptions sets the server options on the appropriate servers
|
||||||
func applyServerOptions(
|
func applyServerOptions(
|
||||||
servers map[string]*caddyhttp.Server,
|
servers map[string]*caddyhttp.Server,
|
||||||
options map[string]interface{},
|
options map[string]any,
|
||||||
warnings *[]caddyconfig.Warning,
|
_ *[]caddyconfig.Warning,
|
||||||
) error {
|
) error {
|
||||||
// If experimental HTTP/3 is enabled, enable it on each server.
|
|
||||||
// We already know there won't be a conflict with serverOptions because
|
|
||||||
// we validated earlier that "experimental_http3" cannot be set at the same
|
|
||||||
// time as "servers"
|
|
||||||
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
|
|
||||||
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
|
|
||||||
for _, srv := range servers {
|
|
||||||
srv.ExperimentalHTTP3 = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverOpts, ok := options["servers"].([]serverOptions)
|
serverOpts, ok := options["servers"].([]serverOptions)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, server := range servers {
|
// check for duplicate names, which would clobber the config
|
||||||
// find the options that apply to this server
|
existingNames := map[string]bool{}
|
||||||
opts := func() *serverOptions {
|
for _, opts := range serverOpts {
|
||||||
for _, entry := range serverOpts {
|
if opts.Name == "" {
|
||||||
if entry.ListenerAddress == "" {
|
|
||||||
return &entry
|
|
||||||
}
|
|
||||||
for _, listener := range server.Listen {
|
|
||||||
if entry.ListenerAddress == listener {
|
|
||||||
return &entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
// if none apply, then move to the next server
|
|
||||||
if opts == nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if existingNames[opts.Name] {
|
||||||
|
return fmt.Errorf("cannot use duplicate server name '%s'", opts.Name)
|
||||||
|
}
|
||||||
|
existingNames[opts.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect the server name overrides
|
||||||
|
nameReplacements := map[string]string{}
|
||||||
|
|
||||||
|
for key, server := range servers {
|
||||||
|
// find the options that apply to this server
|
||||||
|
optsIndex := slices.IndexFunc(serverOpts, func(s serverOptions) bool {
|
||||||
|
return s.ListenerAddress == "" || slices.Contains(server.Listen, s.ListenerAddress)
|
||||||
|
})
|
||||||
|
|
||||||
|
// if none apply, then move to the next server
|
||||||
|
if optsIndex == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opts := serverOpts[optsIndex]
|
||||||
|
|
||||||
// set all the options
|
// set all the options
|
||||||
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
||||||
@@ -218,10 +305,38 @@ func applyServerOptions(
|
|||||||
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
||||||
server.WriteTimeout = opts.WriteTimeout
|
server.WriteTimeout = opts.WriteTimeout
|
||||||
server.IdleTimeout = opts.IdleTimeout
|
server.IdleTimeout = opts.IdleTimeout
|
||||||
|
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||||
server.AllowH2C = opts.AllowH2C
|
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||||
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
|
server.Protocols = opts.Protocols
|
||||||
server.StrictSNIHost = opts.StrictSNIHost
|
server.StrictSNIHost = opts.StrictSNIHost
|
||||||
|
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||||
|
server.ClientIPHeaders = opts.ClientIPHeaders
|
||||||
|
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
|
||||||
|
server.Metrics = opts.Metrics
|
||||||
|
if opts.ShouldLogCredentials {
|
||||||
|
if server.Logs == nil {
|
||||||
|
server.Logs = new(caddyhttp.ServerLogConfig)
|
||||||
|
}
|
||||||
|
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
|
||||||
|
}
|
||||||
|
if opts.Trace {
|
||||||
|
// TODO: THIS IS EXPERIMENTAL (MAY 2024)
|
||||||
|
if server.Logs == nil {
|
||||||
|
server.Logs = new(caddyhttp.ServerLogConfig)
|
||||||
|
}
|
||||||
|
server.Logs.Trace = opts.Trace
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Name != "" {
|
||||||
|
nameReplacements[key] = opts.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename the servers if marked to do so
|
||||||
|
for old, new := range nameReplacements {
|
||||||
|
servers[new] = servers[old]
|
||||||
|
delete(servers, old)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package httpcaddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ComplexShorthandReplacer struct {
|
||||||
|
search *regexp.Regexp
|
||||||
|
replace string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShorthandReplacer struct {
|
||||||
|
complex []ComplexShorthandReplacer
|
||||||
|
simple *strings.Replacer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShorthandReplacer() ShorthandReplacer {
|
||||||
|
// replace shorthand placeholders (which are convenient
|
||||||
|
// when writing a Caddyfile) with their actual placeholder
|
||||||
|
// identifiers or variable names
|
||||||
|
replacer := strings.NewReplacer(placeholderShorthands()...)
|
||||||
|
|
||||||
|
// these are placeholders that allow a user-defined final
|
||||||
|
// parameters, but we still want to provide a shorthand
|
||||||
|
// for those, so we use a regexp to replace
|
||||||
|
regexpReplacements := []ComplexShorthandReplacer{
|
||||||
|
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
||||||
|
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
|
||||||
|
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
|
||||||
|
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||||
|
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
|
||||||
|
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
|
||||||
|
{regexp.MustCompile(`{re\.([\w-\.]*)}`), "{http.regexp.$1}"},
|
||||||
|
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||||
|
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
||||||
|
{regexp.MustCompile(`{resp\.([\w-\.]*)}`), "{http.intercept.$1}"},
|
||||||
|
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
|
||||||
|
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShorthandReplacer{
|
||||||
|
complex: regexpReplacements,
|
||||||
|
simple: replacer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeholderShorthands returns a slice of old-new string pairs,
|
||||||
|
// where the left of the pair is a placeholder shorthand that may
|
||||||
|
// be used in the Caddyfile, and the right is the replacement.
|
||||||
|
func placeholderShorthands() []string {
|
||||||
|
return []string{
|
||||||
|
"{dir}", "{http.request.uri.path.dir}",
|
||||||
|
"{file}", "{http.request.uri.path.file}",
|
||||||
|
"{host}", "{http.request.host}",
|
||||||
|
"{hostport}", "{http.request.hostport}",
|
||||||
|
"{port}", "{http.request.port}",
|
||||||
|
"{method}", "{http.request.method}",
|
||||||
|
"{path}", "{http.request.uri.path}",
|
||||||
|
"{query}", "{http.request.uri.query}",
|
||||||
|
"{remote}", "{http.request.remote}",
|
||||||
|
"{remote_host}", "{http.request.remote.host}",
|
||||||
|
"{remote_port}", "{http.request.remote.port}",
|
||||||
|
"{scheme}", "{http.request.scheme}",
|
||||||
|
"{uri}", "{http.request.uri}",
|
||||||
|
"{uuid}", "{http.request.uuid}",
|
||||||
|
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||||
|
"{tls_version}", "{http.request.tls.version}",
|
||||||
|
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||||
|
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||||
|
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||||
|
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||||
|
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||||
|
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||||
|
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||||
|
"{client_ip}", "{http.vars.client_ip}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy.
|
||||||
|
func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) {
|
||||||
|
if segment != nil {
|
||||||
|
for i := 0; i < len(*segment); i++ {
|
||||||
|
// simple string replacements
|
||||||
|
(*segment)[i].Text = s.simple.Replace((*segment)[i].Text)
|
||||||
|
// complex regexp replacements
|
||||||
|
for _, r := range s.complex {
|
||||||
|
(*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
(t2) {
|
||||||
|
respond 200 {
|
||||||
|
body {args[:]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8082 {
|
||||||
|
import t2 false
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
(t1) {
|
||||||
|
respond 200 {
|
||||||
|
body {args[:]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8081 {
|
||||||
|
import t1 false
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
(t1) {
|
||||||
|
respond 200 {
|
||||||
|
body {args[:]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8081 {
|
||||||
|
import t1 false
|
||||||
|
}
|
||||||
|
|
||||||
|
import import_variadic.txt
|
||||||
|
|
||||||
|
:8083 {
|
||||||
|
import t2 true
|
||||||
|
}
|
||||||
@@ -19,24 +19,25 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (st ServerType) buildTLSApp(
|
func (st ServerType) buildTLSApp(
|
||||||
pairings []sbAddrAssociation,
|
pairings []sbAddrAssociation,
|
||||||
options map[string]interface{},
|
options map[string]any,
|
||||||
warnings []caddyconfig.Warning,
|
warnings []caddyconfig.Warning,
|
||||||
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
||||||
|
|
||||||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||||
var certLoaders []caddytls.CertificateLoader
|
var certLoaders []caddytls.CertificateLoader
|
||||||
|
|
||||||
@@ -44,29 +45,26 @@ func (st ServerType) buildTLSApp(
|
|||||||
if hp, ok := options["http_port"].(int); ok {
|
if hp, ok := options["http_port"].(int); ok {
|
||||||
httpPort = strconv.Itoa(hp)
|
httpPort = strconv.Itoa(hp)
|
||||||
}
|
}
|
||||||
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
autoHTTPS := []string{}
|
||||||
if hsp, ok := options["https_port"].(int); ok {
|
if ah, ok := options["auto_https"].([]string); ok {
|
||||||
httpsPort = strconv.Itoa(hsp)
|
autoHTTPS = ah
|
||||||
}
|
}
|
||||||
|
|
||||||
// count how many server blocks have a TLS-enabled key with
|
// find all hosts that share a server block with a hostless
|
||||||
// no host, and find all hosts that share a server block with
|
// key, so that they don't get forgotten/omitted by auto-HTTPS
|
||||||
// a hostless key, so that they don't get forgotten/omitted
|
// (since they won't appear in route matchers)
|
||||||
// by auto-HTTPS (since they won't appear in route matchers)
|
|
||||||
var serverBlocksWithTLSHostlessKey int
|
|
||||||
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
|
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
|
||||||
for _, pair := range pairings {
|
if !slices.Contains(autoHTTPS, "off") {
|
||||||
for _, sb := range pair.serverBlocks {
|
for _, pair := range pairings {
|
||||||
for _, addr := range sb.keys {
|
for _, sb := range pair.serverBlocks {
|
||||||
if addr.Host == "" {
|
for _, addr := range sb.parsedKeys {
|
||||||
// this address has no hostname, but if it's explicitly set
|
if addr.Host != "" {
|
||||||
// to HTTPS, then we need to count it as being TLS-enabled
|
continue
|
||||||
if addr.Scheme == "https" || addr.Port == httpsPort {
|
|
||||||
serverBlocksWithTLSHostlessKey++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this server block has a hostless key, now
|
// this server block has a hostless key, now
|
||||||
// go through and add all the hosts to the set
|
// go through and add all the hosts to the set
|
||||||
for _, otherAddr := range sb.keys {
|
for _, otherAddr := range sb.parsedKeys {
|
||||||
if otherAddr.Original == addr.Original {
|
if otherAddr.Original == addr.Original {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -96,11 +94,21 @@ func (st ServerType) buildTLSApp(
|
|||||||
|
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
// avoid setting up TLS automation policies for a server that is HTTP-only
|
// avoid setting up TLS automation policies for a server that is HTTP-only
|
||||||
if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
|
var addresses []string
|
||||||
|
for _, addressWithProtocols := range p.addressesWithProtocols {
|
||||||
|
addresses = append(addresses, addressWithProtocols.address)
|
||||||
|
}
|
||||||
|
if !listenersUseAnyPortOtherThan(addresses, httpPort) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sblock := range p.serverBlocks {
|
for _, sblock := range p.serverBlocks {
|
||||||
|
// check the scheme of all the site addresses,
|
||||||
|
// skip building AP if they all had http://
|
||||||
|
if sblock.isAllHTTP() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// get values that populate an automation policy for this block
|
// get values that populate an automation policy for this block
|
||||||
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,6 +125,11 @@ func (st ServerType) buildTLSApp(
|
|||||||
ap.OnDemand = true
|
ap.OnDemand = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reuse private keys tls
|
||||||
|
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
|
||||||
|
ap.ReusePrivateKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
|
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
|
||||||
ap.KeyType = keyTypeVals[0].Value.(string)
|
ap.KeyType = keyTypeVals[0].Value.(string)
|
||||||
}
|
}
|
||||||
@@ -128,11 +141,31 @@ func (st ServerType) buildTLSApp(
|
|||||||
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
|
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
|
||||||
}
|
}
|
||||||
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
|
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
|
||||||
|
// this more correctly implements an error check that was removed
|
||||||
|
// below; try it with this config:
|
||||||
|
//
|
||||||
|
// :443 {
|
||||||
|
// bind 127.0.0.1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// :443 {
|
||||||
|
// bind ::1
|
||||||
|
// tls {
|
||||||
|
// issuer acme
|
||||||
|
// }
|
||||||
|
// }
|
||||||
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
|
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
|
||||||
}
|
}
|
||||||
ap.Issuers = issuers
|
ap.Issuers = issuers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// certificate managers
|
||||||
|
if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
|
||||||
|
for _, certManager := range certManagerVals {
|
||||||
|
certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
|
||||||
|
ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
|
||||||
|
}
|
||||||
|
}
|
||||||
// custom bind host
|
// custom bind host
|
||||||
for _, cfgVal := range sblock.pile["bind"] {
|
for _, cfgVal := range sblock.pile["bind"] {
|
||||||
for _, iss := range ap.Issuers {
|
for _, iss := range ap.Issuers {
|
||||||
@@ -155,42 +188,38 @@ func (st ServerType) buildTLSApp(
|
|||||||
if acmeIssuer.Challenges.BindHost == "" {
|
if acmeIssuer.Challenges.BindHost == "" {
|
||||||
// only binding to one host is supported
|
// only binding to one host is supported
|
||||||
var bindHost string
|
var bindHost string
|
||||||
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
|
if asserted, ok := cfgVal.Value.(addressesWithProtocols); ok && len(asserted.addresses) > 0 {
|
||||||
bindHost = bindHosts[0]
|
bindHost = asserted.addresses[0]
|
||||||
}
|
}
|
||||||
acmeIssuer.Challenges.BindHost = bindHost
|
acmeIssuer.Challenges.BindHost = bindHost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// first make sure this block is allowed to create an automation policy;
|
// we used to ensure this block is allowed to create an automation policy;
|
||||||
// doing so is forbidden if it has a key with no host (i.e. ":443")
|
// doing so was forbidden if it has a key with no host (i.e. ":443")
|
||||||
// and if there is a different server block that also has a key with no
|
// and if there is a different server block that also has a key with no
|
||||||
// host -- since a key with no host matches any host, we need its
|
// host -- since a key with no host matches any host, we need its
|
||||||
// associated automation policy to have an empty Subjects list, i.e. no
|
// associated automation policy to have an empty Subjects list, i.e. no
|
||||||
// host filter, which is indistinguishable between the two server blocks
|
// host filter, which is indistinguishable between the two server blocks
|
||||||
// because automation is not done in the context of a particular server...
|
// because automation is not done in the context of a particular server...
|
||||||
// this is an example of a poor mapping from Caddyfile to JSON but that's
|
// this is an example of a poor mapping from Caddyfile to JSON but that's
|
||||||
// the least-leaky abstraction I could figure out
|
// the least-leaky abstraction I could figure out -- however, this check
|
||||||
if len(sblockHosts) == 0 {
|
// was preventing certain listeners, like those provided by plugins, from
|
||||||
if serverBlocksWithTLSHostlessKey > 1 {
|
// being used as desired (see the Tailscale listener plugin), so I removed
|
||||||
// this server block and at least one other has a key with no host,
|
// the check: and I think since I originally wrote the check I added a new
|
||||||
// making the two indistinguishable; it is misleading to define such
|
// check above which *properly* detects this ambiguity without breaking the
|
||||||
// a policy within one server block since it actually will apply to
|
// listener plugin; see the check above with a commented example config
|
||||||
// others as well
|
if len(sblockHosts) == 0 && catchAllAP == nil {
|
||||||
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
|
// this server block has a key with no hosts, but there is not yet
|
||||||
}
|
// a catch-all automation policy (probably because no global options
|
||||||
if catchAllAP == nil {
|
// were set), so this one becomes it
|
||||||
// this server block has a key with no hosts, but there is not yet
|
catchAllAP = ap
|
||||||
// a catch-all automation policy (probably because no global options
|
|
||||||
// were set), so this one becomes it
|
|
||||||
catchAllAP = ap
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// associate our new automation policy with this server block's hosts
|
// associate our new automation policy with this server block's hosts
|
||||||
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
|
ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort)
|
||||||
sort.Strings(ap.Subjects) // solely for deterministic test results
|
sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
|
||||||
|
|
||||||
// if a combination of public and internal names were given
|
// if a combination of public and internal names were given
|
||||||
// for this same server block and no issuer was specified, we
|
// for this same server block and no issuer was specified, we
|
||||||
@@ -200,7 +229,11 @@ func (st ServerType) buildTLSApp(
|
|||||||
var ap2 *caddytls.AutomationPolicy
|
var ap2 *caddytls.AutomationPolicy
|
||||||
if len(ap.Issuers) == 0 {
|
if len(ap.Issuers) == 0 {
|
||||||
var internal, external []string
|
var internal, external []string
|
||||||
for _, s := range ap.Subjects {
|
for _, s := range ap.SubjectsRaw {
|
||||||
|
// do not create Issuers for Tailscale domains; they will be given a Manager instead
|
||||||
|
if isTailscaleDomain(s) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !certmagic.SubjectQualifiesForCert(s) {
|
if !certmagic.SubjectQualifiesForCert(s) {
|
||||||
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
|
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
|
||||||
}
|
}
|
||||||
@@ -218,10 +251,10 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(external) > 0 && len(internal) > 0 {
|
if len(external) > 0 && len(internal) > 0 {
|
||||||
ap.Subjects = external
|
ap.SubjectsRaw = external
|
||||||
apCopy := *ap
|
apCopy := *ap
|
||||||
ap2 = &apCopy
|
ap2 = &apCopy
|
||||||
ap2.Subjects = internal
|
ap2.SubjectsRaw = internal
|
||||||
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
|
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +319,27 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.Automation.StorageCleanInterval = storageCleanInterval
|
tlsApp.Automation.StorageCleanInterval = storageCleanInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the expired certificates renew interval if configured
|
||||||
|
if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
|
||||||
|
if tlsApp.Automation == nil {
|
||||||
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
|
}
|
||||||
|
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the OCSP check interval if configured
|
||||||
|
if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
|
||||||
|
if tlsApp.Automation == nil {
|
||||||
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
|
}
|
||||||
|
tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// set whether OCSP stapling should be disabled for manually-managed certificates
|
||||||
|
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
|
||||||
|
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||||
|
}
|
||||||
|
|
||||||
// if any hostnames appear on the same server block as a key with
|
// if any hostnames appear on the same server block as a key with
|
||||||
// no host, they will not be used with route matchers because the
|
// no host, they will not be used with route matchers because the
|
||||||
// hostless key matches all hosts, therefore, it wouldn't be
|
// hostless key matches all hosts, therefore, it wouldn't be
|
||||||
@@ -297,16 +351,18 @@ func (st ServerType) buildTLSApp(
|
|||||||
internalAP := &caddytls.AutomationPolicy{
|
internalAP := &caddytls.AutomationPolicy{
|
||||||
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
||||||
}
|
}
|
||||||
for h := range httpsHostsSharedWithHostlessKey {
|
if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
|
||||||
al = append(al, h)
|
for h := range httpsHostsSharedWithHostlessKey {
|
||||||
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
al = append(al, h)
|
||||||
internalAP.Subjects = append(internalAP.Subjects, h)
|
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
||||||
|
internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(al) > 0 {
|
if len(al) > 0 {
|
||||||
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
|
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
|
||||||
}
|
}
|
||||||
if len(internalAP.Subjects) > 0 {
|
if len(internalAP.SubjectsRaw) > 0 {
|
||||||
if tlsApp.Automation == nil {
|
if tlsApp.Automation == nil {
|
||||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
}
|
}
|
||||||
@@ -324,21 +380,17 @@ func (st ServerType) buildTLSApp(
|
|||||||
globalPreferredChains := options["preferred_chains"]
|
globalPreferredChains := options["preferred_chains"]
|
||||||
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
|
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
|
||||||
if hasGlobalACMEDefaults {
|
if hasGlobalACMEDefaults {
|
||||||
// for _, ap := range tlsApp.Automation.Policies {
|
|
||||||
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
|
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
|
||||||
ap := tlsApp.Automation.Policies[i]
|
ap := tlsApp.Automation.Policies[i]
|
||||||
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
||||||
// for public names, create default issuers which will later be filled in with configured global defaults
|
// for public names, create default issuers which will later be filled in with configured global defaults
|
||||||
// (internal names will implicitly use the internal issuer at auto-https time)
|
// (internal names will implicitly use the internal issuer at auto-https time)
|
||||||
ap.Issuers = caddytls.DefaultIssuers()
|
emailStr, _ := globalEmail.(string)
|
||||||
|
ap.Issuers = caddytls.DefaultIssuers(emailStr)
|
||||||
|
|
||||||
// if a specific endpoint is configured, can't use multiple default issuers
|
// if a specific endpoint is configured, can't use multiple default issuers
|
||||||
if globalACMECA != nil {
|
if globalACMECA != nil {
|
||||||
if strings.Contains(globalACMECA.(string), "zerossl") {
|
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
||||||
ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
|
|
||||||
} else {
|
|
||||||
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,14 +418,17 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// consolidate automation policies that are the exact same
|
// consolidate automation policies that are the exact same
|
||||||
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
|
tlsApp.Automation.Policies = consolidateAutomationPolicies(
|
||||||
|
tlsApp.Automation.Policies,
|
||||||
|
slices.Contains(autoHTTPS, "prefer_wildcard"),
|
||||||
|
)
|
||||||
|
|
||||||
// ensure automation policies don't overlap subjects (this should be
|
// ensure automation policies don't overlap subjects (this should be
|
||||||
// an error at provision-time as well, but catch it in the adapt phase
|
// an error at provision-time as well, but catch it in the adapt phase
|
||||||
// for convenience)
|
// for convenience)
|
||||||
automationHostSet := make(map[string]struct{})
|
automationHostSet := make(map[string]struct{})
|
||||||
for _, ap := range tlsApp.Automation.Policies {
|
for _, ap := range tlsApp.Automation.Policies {
|
||||||
for _, s := range ap.Subjects {
|
for _, s := range ap.SubjectsRaw {
|
||||||
if _, ok := automationHostSet[s]; ok {
|
if _, ok := automationHostSet[s]; ok {
|
||||||
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
|
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
|
||||||
}
|
}
|
||||||
@@ -395,7 +450,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
|
|
||||||
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
|
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
|
||||||
|
|
||||||
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
|
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
|
||||||
acmeWrapper, ok := issuer.(acmeCapable)
|
acmeWrapper, ok := issuer.(acmeCapable)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@@ -411,6 +466,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
globalACMEDNS := options["acme_dns"]
|
globalACMEDNS := options["acme_dns"]
|
||||||
globalACMEEAB := options["acme_eab"]
|
globalACMEEAB := options["acme_eab"]
|
||||||
globalPreferredChains := options["preferred_chains"]
|
globalPreferredChains := options["preferred_chains"]
|
||||||
|
globalCertLifetime := options["cert_lifetime"]
|
||||||
|
globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
|
||||||
|
|
||||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||||
acmeIssuer.Email = globalEmail.(string)
|
acmeIssuer.Email = globalEmail.(string)
|
||||||
@@ -418,7 +475,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
if globalACMECA != nil && acmeIssuer.CA == "" {
|
if globalACMECA != nil && acmeIssuer.CA == "" {
|
||||||
acmeIssuer.CA = globalACMECA.(string)
|
acmeIssuer.CA = globalACMECA.(string)
|
||||||
}
|
}
|
||||||
if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
||||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||||
}
|
}
|
||||||
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
||||||
@@ -434,6 +491,27 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
|
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
|
||||||
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
|
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
|
||||||
}
|
}
|
||||||
|
if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
|
||||||
|
if acmeIssuer.Challenges == nil {
|
||||||
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges.HTTP == nil {
|
||||||
|
acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
|
||||||
|
}
|
||||||
|
acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
|
||||||
|
}
|
||||||
|
if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
|
||||||
|
if acmeIssuer.Challenges == nil {
|
||||||
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges.TLSALPN == nil {
|
||||||
|
acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
|
||||||
|
}
|
||||||
|
acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
|
||||||
|
}
|
||||||
|
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||||
|
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,7 +520,11 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
// for any other automation policies. A nil policy (and no error) will be
|
// for any other automation policies. A nil policy (and no error) will be
|
||||||
// returned if there are no default/global options. However, if always is
|
// returned if there are no default/global options. However, if always is
|
||||||
// true, a non-nil value will always be returned (unless there is an error).
|
// true, a non-nil value will always be returned (unless there is an error).
|
||||||
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
|
func newBaseAutomationPolicy(
|
||||||
|
options map[string]any,
|
||||||
|
_ []caddyconfig.Warning,
|
||||||
|
always bool,
|
||||||
|
) (*caddytls.AutomationPolicy, error) {
|
||||||
issuers, hasIssuers := options["cert_issuer"]
|
issuers, hasIssuers := options["cert_issuer"]
|
||||||
_, hasLocalCerts := options["local_certs"]
|
_, hasLocalCerts := options["local_certs"]
|
||||||
keyType, hasKeyType := options["key_type"]
|
keyType, hasKeyType := options["key_type"]
|
||||||
@@ -485,7 +567,7 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
|
|||||||
|
|
||||||
// consolidateAutomationPolicies combines automation policies that are the same,
|
// consolidateAutomationPolicies combines automation policies that are the same,
|
||||||
// for a cleaner overall output.
|
// for a cleaner overall output.
|
||||||
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
|
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy {
|
||||||
// sort from most specific to least specific; we depend on this ordering
|
// sort from most specific to least specific; we depend on this ordering
|
||||||
sort.SliceStable(aps, func(i, j int) bool {
|
sort.SliceStable(aps, func(i, j int) bool {
|
||||||
if automationPolicyIsSubset(aps[i], aps[j]) {
|
if automationPolicyIsSubset(aps[i], aps[j]) {
|
||||||
@@ -494,7 +576,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
if automationPolicyIsSubset(aps[j], aps[i]) {
|
if automationPolicyIsSubset(aps[j], aps[i]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
return len(aps[i].SubjectsRaw) > len(aps[j].SubjectsRaw)
|
||||||
})
|
})
|
||||||
|
|
||||||
emptyAPCount := 0
|
emptyAPCount := 0
|
||||||
@@ -502,13 +584,13 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
// compute the number of empty policies (disregarding subjects) - see #4128
|
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||||
emptyAP := new(caddytls.AutomationPolicy)
|
emptyAP := new(caddytls.AutomationPolicy)
|
||||||
for i := 0; i < len(aps); i++ {
|
for i := 0; i < len(aps); i++ {
|
||||||
emptyAP.Subjects = aps[i].Subjects
|
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
||||||
if reflect.DeepEqual(aps[i], emptyAP) {
|
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||||
emptyAPCount++
|
emptyAPCount++
|
||||||
if !automationPolicyHasAllPublicNames(aps[i]) {
|
if !automationPolicyHasAllPublicNames(aps[i]) {
|
||||||
// if this automation policy has internal names, we might as well remove it
|
// if this automation policy has internal names, we might as well remove it
|
||||||
// so auto-https can implicitly use the internal issuer
|
// so auto-https can implicitly use the internal issuer
|
||||||
aps = append(aps[:i], aps[i+1:]...)
|
aps = slices.Delete(aps, i, i+1)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,7 +607,7 @@ outer:
|
|||||||
for j := i + 1; j < len(aps); j++ {
|
for j := i + 1; j < len(aps); j++ {
|
||||||
// if they're exactly equal in every way, just keep one of them
|
// if they're exactly equal in every way, just keep one of them
|
||||||
if reflect.DeepEqual(aps[i], aps[j]) {
|
if reflect.DeepEqual(aps[i], aps[j]) {
|
||||||
aps = append(aps[:j], aps[j+1:]...)
|
aps = slices.Delete(aps, j, j+1)
|
||||||
// must re-evaluate current i against next j; can't skip it!
|
// must re-evaluate current i against next j; can't skip it!
|
||||||
// even if i decrements to -1, will be incremented to 0 immediately
|
// even if i decrements to -1, will be incremented to 0 immediately
|
||||||
i--
|
i--
|
||||||
@@ -539,12 +621,14 @@ outer:
|
|||||||
// eaten up by the one with subjects; and if both have subjects, we
|
// eaten up by the one with subjects; and if both have subjects, we
|
||||||
// need to combine their lists
|
// need to combine their lists
|
||||||
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||||
|
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
||||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||||
aps[i].MustStaple == aps[j].MustStaple &&
|
aps[i].MustStaple == aps[j].MustStaple &&
|
||||||
aps[i].KeyType == aps[j].KeyType &&
|
aps[i].KeyType == aps[j].KeyType &&
|
||||||
aps[i].OnDemand == aps[j].OnDemand &&
|
aps[i].OnDemand == aps[j].OnDemand &&
|
||||||
|
aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys &&
|
||||||
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
|
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
|
||||||
if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
|
if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 {
|
||||||
// later policy (at j) has no subjects ("catch-all"), so we can
|
// later policy (at j) has no subjects ("catch-all"), so we can
|
||||||
// remove the identical-but-more-specific policy that comes first
|
// remove the identical-but-more-specific policy that comes first
|
||||||
// AS LONG AS it is not shadowed by another policy before it; e.g.
|
// AS LONG AS it is not shadowed by another policy before it; e.g.
|
||||||
@@ -553,21 +637,46 @@ outer:
|
|||||||
// cause example.com to be served by the less specific policy for
|
// cause example.com to be served by the less specific policy for
|
||||||
// '*.com', which might be different (yes we've seen this happen)
|
// '*.com', which might be different (yes we've seen this happen)
|
||||||
if automationPolicyShadows(i, aps) >= j {
|
if automationPolicyShadows(i, aps) >= j {
|
||||||
aps = append(aps[:i], aps[i+1:]...)
|
aps = slices.Delete(aps, i, i+1)
|
||||||
i--
|
i--
|
||||||
continue outer
|
continue outer
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// avoid repeated subjects
|
// avoid repeated subjects
|
||||||
for _, subj := range aps[j].Subjects {
|
for _, subj := range aps[j].SubjectsRaw {
|
||||||
if !sliceContains(aps[i].Subjects, subj) {
|
if !slices.Contains(aps[i].SubjectsRaw, subj) {
|
||||||
aps[i].Subjects = append(aps[i].Subjects, subj)
|
aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aps = append(aps[:j], aps[j+1:]...)
|
aps = slices.Delete(aps, j, j+1)
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if preferWildcard {
|
||||||
|
// remove subjects from i if they're covered by a wildcard in j
|
||||||
|
iSubjs := aps[i].SubjectsRaw
|
||||||
|
for iSubj := 0; iSubj < len(iSubjs); iSubj++ {
|
||||||
|
for jSubj := range aps[j].SubjectsRaw {
|
||||||
|
if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) {
|
||||||
|
iSubjs = slices.Delete(iSubjs, iSubj, iSubj+1)
|
||||||
|
iSubj--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aps[i].SubjectsRaw = iSubjs
|
||||||
|
|
||||||
|
// remove i if it has no subjects left
|
||||||
|
if len(aps[i].SubjectsRaw) == 0 {
|
||||||
|
aps = slices.Delete(aps, i, i+1)
|
||||||
|
i--
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,20 +686,16 @@ outer:
|
|||||||
// automationPolicyIsSubset returns true if a's subjects are a subset
|
// automationPolicyIsSubset returns true if a's subjects are a subset
|
||||||
// of b's subjects.
|
// of b's subjects.
|
||||||
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
|
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
|
||||||
if len(b.Subjects) == 0 {
|
if len(b.SubjectsRaw) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if len(a.Subjects) == 0 {
|
if len(a.SubjectsRaw) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, aSubj := range a.Subjects {
|
for _, aSubj := range a.SubjectsRaw {
|
||||||
var inSuperset bool
|
inSuperset := slices.ContainsFunc(b.SubjectsRaw, func(bSubj string) bool {
|
||||||
for _, bSubj := range b.Subjects {
|
return certmagic.MatchWildcard(aSubj, bSubj)
|
||||||
if certmagic.MatchWildcard(aSubj, bSubj) {
|
})
|
||||||
inSuperset = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !inSuperset {
|
if !inSuperset {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -616,17 +721,30 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
|
|||||||
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
|
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
|
||||||
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
|
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
|
||||||
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
|
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
|
||||||
|
//
|
||||||
|
// IP subjects are considered as non-qualifying for public certs. Technically, there are
|
||||||
|
// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
|
||||||
|
// is used solely for implicit automation (defaults), where it gets really complicated to
|
||||||
|
// keep track of which issuers support IP certificates in which circumstances. Currently,
|
||||||
|
// issuers that support IP certificates are very few, and all require some sort of config
|
||||||
|
// from the user anyway (such as an account credential). Since we cannot implicitly and
|
||||||
|
// automatically get public IP certs without configuration from the user, we treat IPs as
|
||||||
|
// not qualifying for public certificates. Users should expressly configure an issuer
|
||||||
|
// that supports IP certs for that purpose.
|
||||||
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
|
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
|
||||||
return !certmagic.SubjectIsIP(subj) &&
|
return !certmagic.SubjectIsIP(subj) &&
|
||||||
!certmagic.SubjectIsInternal(subj) &&
|
!certmagic.SubjectIsInternal(subj) &&
|
||||||
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automationPolicyHasAllPublicNames returns true if all the names on the policy
|
||||||
|
// do NOT qualify for public certs OR are tailscale domains.
|
||||||
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
||||||
for _, subj := range ap.Subjects {
|
return !slices.ContainsFunc(ap.SubjectsRaw, func(i string) bool {
|
||||||
if !subjectQualifiesForPublicCert(ap, subj) {
|
return !subjectQualifiesForPublicCert(ap, i) || isTailscaleDomain(i)
|
||||||
return false
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true
|
func isTailscaleDomain(name string) bool {
|
||||||
|
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
|
|||||||
expect: false,
|
expect: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
apA := &caddytls.AutomationPolicy{Subjects: test.a}
|
apA := &caddytls.AutomationPolicy{SubjectsRaw: test.a}
|
||||||
apB := &caddytls.AutomationPolicy{Subjects: test.b}
|
apB := &caddytls.AutomationPolicy{SubjectsRaw: test.b}
|
||||||
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
|
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
|
||||||
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
|
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-20
@@ -1,11 +1,26 @@
|
|||||||
|
// 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 caddyconfig
|
package caddyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -15,8 +30,14 @@ func init() {
|
|||||||
caddy.RegisterModule(HTTPLoader{})
|
caddy.RegisterModule(HTTPLoader{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
|
// HTTPLoader can load Caddy configs over HTTP(S).
|
||||||
// based on the Content-Type header of the HTTP response.
|
//
|
||||||
|
// If the response is not a JSON config, a config adapter must be specified
|
||||||
|
// either in the loader config (`adapter`), or in the Content-Type HTTP header
|
||||||
|
// returned in the HTTP response from the server. The Content-Type header is
|
||||||
|
// read just like the admin API's `/load` endpoint. Uf you don't have control
|
||||||
|
// over the HTTP server (but can still trust its response), you can override
|
||||||
|
// the Content-Type header by setting the `adapter` property in this config.
|
||||||
type HTTPLoader struct {
|
type HTTPLoader struct {
|
||||||
// The method for the request. Default: GET
|
// The method for the request. Default: GET
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
@@ -30,6 +51,11 @@ type HTTPLoader struct {
|
|||||||
// Maximum time allowed for a complete connection and request.
|
// Maximum time allowed for a complete connection and request.
|
||||||
Timeout caddy.Duration `json:"timeout,omitempty"`
|
Timeout caddy.Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
// The name of the config adapter to use, if any. Only needed
|
||||||
|
// if the HTTP response is not a JSON config and if the server's
|
||||||
|
// Content-Type header is missing or incorrect.
|
||||||
|
Adapter string `json:"adapter,omitempty"`
|
||||||
|
|
||||||
TLS *struct {
|
TLS *struct {
|
||||||
// Present this instance's managed remote identity credentials to the server.
|
// Present this instance's managed remote identity credentials to the server.
|
||||||
UseServerIdentity bool `json:"use_server_identity,omitempty"`
|
UseServerIdentity bool `json:"use_server_identity,omitempty"`
|
||||||
@@ -56,23 +82,30 @@ func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
|
|||||||
|
|
||||||
// LoadConfig loads a Caddy config.
|
// LoadConfig loads a Caddy config.
|
||||||
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
|
||||||
client, err := hl.makeClient(ctx)
|
client, err := hl.makeClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
method := hl.Method
|
method := repl.ReplaceAll(hl.Method, "")
|
||||||
if method == "" {
|
if method == "" {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(method, hl.URL, nil)
|
url := repl.ReplaceAll(hl.URL, "")
|
||||||
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header = hl.Headers
|
for key, vals := range hl.Headers {
|
||||||
|
for _, val := range vals {
|
||||||
|
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := doHttpCallWithRetries(ctx, client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,22 +114,59 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
|
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
|
// adapt the config based on either manually-configured adapter or server's response header
|
||||||
|
ct := resp.Header.Get("Content-Type")
|
||||||
|
if hl.Adapter != "" {
|
||||||
|
ct = "text/" + hl.Adapter
|
||||||
|
}
|
||||||
|
result, warnings, err := adaptByContentType(ct, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, warn := range warnings {
|
for _, warn := range warnings {
|
||||||
ctx.Logger(hl).Warn(warn.String())
|
ctx.Logger().Warn(warn.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("problem calling http loader url: %v", err)
|
||||||
|
} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
|
||||||
|
var resp *http.Response
|
||||||
|
var err error
|
||||||
|
const maxAttempts = 10
|
||||||
|
|
||||||
|
for i := 0; i < maxAttempts; i++ {
|
||||||
|
resp, err = attemptHttpCall(client, request)
|
||||||
|
if err != nil && i < maxAttempts-1 {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Millisecond * 500):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return resp, ctx.Err()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
|
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: time.Duration(hl.Timeout),
|
Timeout: time.Duration(hl.Timeout),
|
||||||
@@ -107,30 +177,27 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
|
|||||||
|
|
||||||
// client authentication
|
// client authentication
|
||||||
if hl.TLS.UseServerIdentity {
|
if hl.TLS.UseServerIdentity {
|
||||||
certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
|
certs, err := ctx.IdentityCredentials(ctx.Logger())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting server identity credentials: %v", err)
|
return nil, fmt.Errorf("getting server identity credentials: %v", err)
|
||||||
}
|
}
|
||||||
if tlsConfig == nil {
|
// See https://github.com/securego/gosec/issues/1054#issuecomment-2072235199
|
||||||
tlsConfig = new(tls.Config)
|
//nolint:gosec
|
||||||
}
|
tlsConfig = &tls.Config{Certificates: certs}
|
||||||
tlsConfig.Certificates = certs
|
|
||||||
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
|
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
|
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if tlsConfig == nil {
|
//nolint:gosec
|
||||||
tlsConfig = new(tls.Config)
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
}
|
|
||||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trusted server certs
|
// trusted server certs
|
||||||
if len(hl.TLS.RootCAPEMFiles) > 0 {
|
if len(hl.TLS.RootCAPEMFiles) > 0 {
|
||||||
rootPool := x509.NewCertPool()
|
rootPool := x509.NewCertPool()
|
||||||
for _, pemFile := range hl.TLS.RootCAPEMFiles {
|
for _, pemFile := range hl.TLS.RootCAPEMFiles {
|
||||||
pemData, err := ioutil.ReadFile(pemFile)
|
pemData, err := os.ReadFile(pemFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed reading ca cert: %v", err)
|
return nil, fmt.Errorf("failed reading ca cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-5
@@ -58,6 +58,10 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
|
|||||||
Pattern: "/load",
|
Pattern: "/load",
|
||||||
Handler: caddy.AdminHandlerFunc(al.handleLoad),
|
Handler: caddy.AdminHandlerFunc(al.handleLoad),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Pattern: "/adapt",
|
||||||
|
Handler: caddy.AdminHandlerFunc(al.handleAdapt),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +126,48 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
|
// handleAdapt adapts the given Caddy config to JSON and responds with the result.
|
||||||
|
func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
return caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
|
Err: fmt.Errorf("method not allowed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
|
buf.Reset()
|
||||||
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
|
_, err := io.Copy(buf, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("reading request body: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := struct {
|
||||||
|
Warnings []Warning `json:"warnings,omitempty"`
|
||||||
|
Result json.RawMessage `json:"result"`
|
||||||
|
}{
|
||||||
|
Warnings: warnings,
|
||||||
|
Result: result,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
return json.NewEncoder(w).Encode(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
|
||||||
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
|
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
|
||||||
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
|
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
|
||||||
// assume JSON as the default
|
// assume JSON as the default
|
||||||
@@ -144,12 +189,11 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// adapter name should be suffix of MIME type
|
// adapter name should be suffix of MIME type
|
||||||
slashIdx := strings.Index(ct, "/")
|
_, adapterName, slashFound := strings.Cut(ct, "/")
|
||||||
if slashIdx < 0 {
|
if !slashFound {
|
||||||
return nil, nil, fmt.Errorf("malformed Content-Type")
|
return nil, nil, fmt.Errorf("malformed Content-Type")
|
||||||
}
|
}
|
||||||
|
|
||||||
adapterName := ct[slashIdx+1:]
|
|
||||||
cfgAdapter := GetAdapter(adapterName)
|
cfgAdapter := GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
|
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
|
||||||
@@ -164,7 +208,7 @@ func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bufPool = sync.Pool{
|
var bufPool = sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() any {
|
||||||
return new(bytes.Buffer)
|
return new(bytes.Buffer)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-46
@@ -7,7 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -22,9 +23,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aryann/difflib"
|
"github.com/aryann/difflib"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
// plug in Caddy modules here
|
// plug in Caddy modules here
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||||
)
|
)
|
||||||
@@ -34,7 +36,7 @@ type Defaults struct {
|
|||||||
// Port we expect caddy to listening on
|
// Port we expect caddy to listening on
|
||||||
AdminPort int
|
AdminPort int
|
||||||
// Certificates we expect to be loaded before attempting to run the tests
|
// Certificates we expect to be loaded before attempting to run the tests
|
||||||
Certifcates []string
|
Certificates []string
|
||||||
// TestRequestTimeout is the time to wait for a http request to
|
// TestRequestTimeout is the time to wait for a http request to
|
||||||
TestRequestTimeout time.Duration
|
TestRequestTimeout time.Duration
|
||||||
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
|
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
|
||||||
@@ -43,8 +45,8 @@ type Defaults struct {
|
|||||||
|
|
||||||
// Default testing values
|
// Default testing values
|
||||||
var Default = Defaults{
|
var Default = Defaults{
|
||||||
AdminPort: 2019,
|
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
|
||||||
Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
|
Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
|
||||||
TestRequestTimeout: 5 * time.Second,
|
TestRequestTimeout: 5 * time.Second,
|
||||||
LoadRequestTimeout: 5 * time.Second,
|
LoadRequestTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -58,12 +60,11 @@ var (
|
|||||||
type Tester struct {
|
type Tester struct {
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
configLoaded bool
|
configLoaded bool
|
||||||
t *testing.T
|
t testing.TB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTester will create a new testing client with an attached cookie jar
|
// NewTester will create a new testing client with an attached cookie jar
|
||||||
func NewTester(t *testing.T) *Tester {
|
func NewTester(t testing.TB) *Tester {
|
||||||
|
|
||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create cookiejar: %s", err)
|
t.Fatalf("failed to create cookiejar: %s", err)
|
||||||
@@ -94,13 +95,12 @@ func timeElapsed(start time.Time, name string) {
|
|||||||
// InitServer this will configure the server with a configurion of a specific
|
// InitServer this will configure the server with a configurion of a specific
|
||||||
// type. The configType must be either "json" or the adapter type.
|
// type. The configType must be either "json" or the adapter type.
|
||||||
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
||||||
|
|
||||||
if err := tc.initServer(rawConfig, configType); err != nil {
|
if err := tc.initServer(rawConfig, configType); err != nil {
|
||||||
tc.t.Logf("failed to load config: %s", err)
|
tc.t.Logf("failed to load config: %s", err)
|
||||||
tc.t.Fail()
|
tc.t.Fail()
|
||||||
}
|
}
|
||||||
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
|
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
|
||||||
tc.t.Logf("failed ensurng config is running: %s", err)
|
tc.t.Logf("failed ensuring config is running: %s", err)
|
||||||
tc.t.Fail()
|
tc.t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,13 +108,12 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
|
|||||||
// InitServer this will configure the server with a configurion of a specific
|
// InitServer this will configure the server with a configurion of a specific
|
||||||
// type. The configType must be either "json" or the adapter type.
|
// type. The configType must be either "json" or the adapter type.
|
||||||
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
||||||
|
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
tc.t.SkipNow()
|
tc.t.SkipNow()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := validateTestPrerequisites()
|
err := validateTestPrerequisites(tc.t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
|
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -122,14 +121,13 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
|
|
||||||
tc.t.Cleanup(func() {
|
tc.t.Cleanup(func() {
|
||||||
if tc.t.Failed() && tc.configLoaded {
|
if tc.t.Failed() && tc.configLoaded {
|
||||||
|
|
||||||
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Log("unable to read the current config")
|
tc.t.Log("unable to read the current config")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
_ = json.Indent(&out, body, "", " ")
|
_ = json.Indent(&out, body, "", " ")
|
||||||
@@ -138,6 +136,20 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
rawConfig = prependCaddyFilePath(rawConfig)
|
rawConfig = prependCaddyFilePath(rawConfig)
|
||||||
|
// normalize JSON config
|
||||||
|
if configType == "json" {
|
||||||
|
tc.t.Logf("Before: %s", rawConfig)
|
||||||
|
var conf any
|
||||||
|
if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c, err := json.Marshal(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rawConfig = string(c)
|
||||||
|
tc.t.Logf("After: %s", rawConfig)
|
||||||
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: Default.LoadRequestTimeout,
|
Timeout: Default.LoadRequestTimeout,
|
||||||
}
|
}
|
||||||
@@ -162,7 +174,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
timeElapsed(start, "caddytest: config load time")
|
timeElapsed(start, "caddytest: config load time")
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("unable to read response. %s", err)
|
tc.t.Errorf("unable to read response. %s", err)
|
||||||
return err
|
return err
|
||||||
@@ -186,7 +198,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
|
|||||||
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
|
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var expected interface{}
|
var expected any
|
||||||
err := json.Unmarshal(expectedBytes, &expected)
|
err := json.Unmarshal(expectedBytes, &expected)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -196,17 +208,17 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
|
|||||||
Timeout: Default.LoadRequestTimeout,
|
Timeout: Default.LoadRequestTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchConfig := func(client *http.Client) interface{} {
|
fetchConfig := func(client *http.Client) any {
|
||||||
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
actualBytes, err := ioutil.ReadAll(resp.Body)
|
actualBytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var actual interface{}
|
var actual any
|
||||||
err = json.Unmarshal(actualBytes, &actual)
|
err = json.Unmarshal(actualBytes, &actual)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -214,37 +226,53 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
|
|||||||
return actual
|
return actual
|
||||||
}
|
}
|
||||||
|
|
||||||
for retries := 4; retries > 0; retries-- {
|
for retries := 10; retries > 0; retries-- {
|
||||||
if reflect.DeepEqual(expected, fetchConfig(client)) {
|
if reflect.DeepEqual(expected, fetchConfig(client)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
tc.t.Errorf("POSTed configuration isn't active")
|
tc.t.Errorf("POSTed configuration isn't active")
|
||||||
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
|
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initConfig = `{
|
||||||
|
admin localhost:2999
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
// validateTestPrerequisites ensures the certificates are available in the
|
// validateTestPrerequisites ensures the certificates are available in the
|
||||||
// designated path and Caddy sub-process is running.
|
// designated path and Caddy sub-process is running.
|
||||||
func validateTestPrerequisites() error {
|
func validateTestPrerequisites(t testing.TB) error {
|
||||||
|
|
||||||
// check certificates are found
|
// check certificates are found
|
||||||
for _, certName := range Default.Certifcates {
|
for _, certName := range Default.Certificates {
|
||||||
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
|
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
|
||||||
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
|
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCaddyAdminRunning() != nil {
|
if isCaddyAdminRunning() != nil {
|
||||||
|
// setup the init config file, and set the cleanup afterwards
|
||||||
|
f, err := os.CreateTemp("", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Remove(f.Name())
|
||||||
|
})
|
||||||
|
if _, err := f.WriteString(initConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// start inprocess caddy server
|
// start inprocess caddy server
|
||||||
os.Args = []string{"caddy", "run"}
|
os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
|
||||||
go func() {
|
go func() {
|
||||||
caddycmd.Main()
|
caddycmd.Main()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// wait for caddy to start serving the initial config
|
// wait for caddy to start serving the initial config
|
||||||
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
|
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +295,6 @@ func isCaddyAdminRunning() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getIntegrationDir() string {
|
func getIntegrationDir() string {
|
||||||
|
|
||||||
_, filename, _, ok := runtime.Caller(1)
|
_, filename, _, ok := runtime.Caller(1)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("unable to determine the current file path")
|
panic("unable to determine the current file path")
|
||||||
@@ -287,7 +314,6 @@ func prependCaddyFilePath(rawConfig string) string {
|
|||||||
|
|
||||||
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
||||||
func CreateTestingTransport() *http.Transport {
|
func CreateTestingTransport() *http.Transport {
|
||||||
|
|
||||||
dialer := net.Dialer{
|
dialer := net.Dialer{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
KeepAlive: 5 * time.Second,
|
KeepAlive: 5 * time.Second,
|
||||||
@@ -315,7 +341,6 @@ func CreateTestingTransport() *http.Transport {
|
|||||||
|
|
||||||
// AssertLoadError will load a config and expect an error
|
// AssertLoadError will load a config and expect an error
|
||||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||||
|
|
||||||
tc := NewTester(t)
|
tc := NewTester(t)
|
||||||
|
|
||||||
err := tc.initServer(rawConfig, configType)
|
err := tc.initServer(rawConfig, configType)
|
||||||
@@ -326,7 +351,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
|||||||
|
|
||||||
// AssertRedirect makes a request and asserts the redirection happens
|
// AssertRedirect makes a request and asserts the redirection happens
|
||||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||||
|
|
||||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
@@ -363,15 +387,14 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompareAdapt adapts a config and then compares it against an expected result
|
// CompareAdapt adapts a config and then compares it against an expected result
|
||||||
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
options := make(map[string]interface{})
|
options := make(map[string]any)
|
||||||
|
|
||||||
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
|
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -423,7 +446,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AssertAdapt adapts a config and then tests it against an expected result
|
// AssertAdapt adapts a config and then tests it against an expected result
|
||||||
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
|
func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
|
||||||
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
@@ -432,7 +455,7 @@ func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedRes
|
|||||||
|
|
||||||
// Generic request functions
|
// Generic request functions
|
||||||
|
|
||||||
func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
|
func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
|
||||||
requestContentType := ""
|
requestContentType := ""
|
||||||
for _, requestHeader := range requestHeaders {
|
for _, requestHeader := range requestHeaders {
|
||||||
arr := strings.SplitAfterN(requestHeader, ":", 2)
|
arr := strings.SplitAfterN(requestHeader, ":", 2)
|
||||||
@@ -452,14 +475,13 @@ func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
|
|||||||
|
|
||||||
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
||||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||||
|
|
||||||
resp, err := tc.Client.Do(req)
|
resp, err := tc.Client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("failed to call server %s", err)
|
tc.t.Fatalf("failed to call server %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expectedStatusCode != resp.StatusCode {
|
if expectedStatusCode != resp.StatusCode {
|
||||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
|
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
@@ -467,11 +489,10 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
|||||||
|
|
||||||
// AssertResponse request a URI and assert the status code and the body contains a string
|
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
bytes, err := ioutil.ReadAll(resp.Body)
|
bytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to read the response body %s", err)
|
tc.t.Fatalf("unable to read the response body %s", err)
|
||||||
}
|
}
|
||||||
@@ -489,7 +510,6 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
|||||||
|
|
||||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURI, nil)
|
req, err := http.NewRequest("GET", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -500,7 +520,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
|||||||
|
|
||||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -511,7 +530,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
|||||||
|
|
||||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -525,7 +543,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
|||||||
|
|
||||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -539,7 +556,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
|||||||
|
|
||||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package caddytest
|
package caddytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -31,3 +32,97 @@ func TestReplaceCertificatePaths(t *testing.T) {
|
|||||||
t.Error("expected redirect uri to be unchanged")
|
t.Error("expected redirect uri to be unchanged")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadUnorderedJSON(t *testing.T) {
|
||||||
|
tester := NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"writer": {
|
||||||
|
"output": "stdout"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sStdOutLogs": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"writer": {
|
||||||
|
"output": "stdout"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"http.*",
|
||||||
|
"admin.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sFileLogs": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"writer": {
|
||||||
|
"output": "stdout"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"http.*",
|
||||||
|
"admin.*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2999"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities" : {
|
||||||
|
"local" : {
|
||||||
|
"install_trust": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"http_port": 9080,
|
||||||
|
"https_port": 9443,
|
||||||
|
"servers": {
|
||||||
|
"s_server": {
|
||||||
|
"listen": [
|
||||||
|
":9080"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"body": "Hello"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"logs": {
|
||||||
|
"default_logger_name": "sStdOutLogs",
|
||||||
|
"logger_names": {
|
||||||
|
"localhost": "sStdOutLogs",
|
||||||
|
"127.0.0.1": "sFileLogs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "json")
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tester.AssertResponseCode(req, 200)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
"github.com/mholt/acmez/v2"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
smallstepacme "github.com/smallstep/certificates/acme"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const acmeChallengePort = 9081
|
||||||
|
|
||||||
|
// Test the basic functionality of Caddy's ACME server
|
||||||
|
func TestACMEServerWithDefaults(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
logger, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
acme.localhost {
|
||||||
|
acme_server
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
client := acmez.Client{
|
||||||
|
Client: &acme.Client{
|
||||||
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
|
HTTPClient: tester.Client,
|
||||||
|
Logger: logger,
|
||||||
|
},
|
||||||
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating account key: %v", err)
|
||||||
|
}
|
||||||
|
account := acme.Account{
|
||||||
|
Contact: []string{"mailto:you@example.com"},
|
||||||
|
TermsOfServiceAgreed: true,
|
||||||
|
PrivateKey: accountPrivateKey,
|
||||||
|
}
|
||||||
|
account, err = client.NewAccount(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("new account: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every certificate needs a key.
|
||||||
|
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating certificate key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("obtaining certificate: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACME servers should usually give you the entire certificate chain
|
||||||
|
// in PEM format, and sometimes even alternate chains! It's up to you
|
||||||
|
// which one(s) to store and use, but whatever you do, be sure to
|
||||||
|
// store the certificate and key somewhere safe and secure, i.e. don't
|
||||||
|
// lose them!
|
||||||
|
for _, cert := range certs {
|
||||||
|
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACMEServerWithMismatchedChallenges(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
logger := caddy.Log().Named("acmez")
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
acme.localhost {
|
||||||
|
acme_server {
|
||||||
|
challenges tls-alpn-01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
client := acmez.Client{
|
||||||
|
Client: &acme.Client{
|
||||||
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
|
HTTPClient: tester.Client,
|
||||||
|
Logger: logger,
|
||||||
|
},
|
||||||
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating account key: %v", err)
|
||||||
|
}
|
||||||
|
account := acme.Account{
|
||||||
|
Contact: []string{"mailto:you@example.com"},
|
||||||
|
TermsOfServiceAgreed: true,
|
||||||
|
PrivateKey: accountPrivateKey,
|
||||||
|
}
|
||||||
|
account, err = client.NewAccount(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("new account: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every certificate needs a key.
|
||||||
|
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating certificate key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
|
||||||
|
if len(certs) > 0 {
|
||||||
|
t.Errorf("expected '0' certificates, but received '%d'", len(certs))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected errors, but received none")
|
||||||
|
}
|
||||||
|
const expectedErrMsg = "no solvers available for remaining challenges (configured=[http-01] offered=[tls-alpn-01] remaining=[tls-alpn-01])"
|
||||||
|
if !strings.Contains(err.Error(), expectedErrMsg) {
|
||||||
|
t.Errorf(`received error message does not match expectation: expected="%s" received="%s"`, expectedErrMsg, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// naiveHTTPSolver is a no-op acmez.Solver for example purposes only.
|
||||||
|
type naiveHTTPSolver struct {
|
||||||
|
srv *http.Server
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error {
|
||||||
|
smallstepacme.InsecurePortHTTP01 = acmeChallengePort
|
||||||
|
s.srv = &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", acmeChallengePort),
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
host, _, err := net.SplitHostPort(r.Host)
|
||||||
|
if err != nil {
|
||||||
|
host = r.Host
|
||||||
|
}
|
||||||
|
s.logger.Info("received request on challenge server", zap.String("path", r.URL.Path))
|
||||||
|
if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) {
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
w.Write([]byte(challenge.KeyAuthorization))
|
||||||
|
r.Close = true
|
||||||
|
s.logger.Info("served key authentication",
|
||||||
|
zap.String("identifier", challenge.Identifier.Value),
|
||||||
|
zap.String("challenge", "http-01"),
|
||||||
|
zap.String("remote", r.RemoteAddr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
l, err := net.Listen("tcp", fmt.Sprintf(":%d", acmeChallengePort))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("present challenge", zap.Any("challenge", challenge))
|
||||||
|
go s.srv.Serve(l)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s naiveHTTPSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
|
||||||
|
smallstepacme.InsecurePortHTTP01 = 0
|
||||||
|
s.logger.Info("cleanup", zap.Any("challenge", challenge))
|
||||||
|
if s.srv != nil {
|
||||||
|
s.srv.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
"github.com/mholt/acmez/v2"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestACMEServerDirectory(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
local_certs
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
pki {
|
||||||
|
ca local {
|
||||||
|
name "Caddy Local Authority"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acme.localhost:9443 {
|
||||||
|
acme_server
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
tester.AssertGetResponse(
|
||||||
|
"https://acme.localhost:9443/acme/local/directory",
|
||||||
|
200,
|
||||||
|
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACMEServerAllowPolicy(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
local_certs
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
pki {
|
||||||
|
ca local {
|
||||||
|
name "Caddy Local Authority"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acme.localhost {
|
||||||
|
acme_server {
|
||||||
|
challenges http-01
|
||||||
|
allow {
|
||||||
|
domains localhost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
logger, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := acmez.Client{
|
||||||
|
Client: &acme.Client{
|
||||||
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
|
HTTPClient: tester.Client,
|
||||||
|
Logger: logger,
|
||||||
|
},
|
||||||
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating account key: %v", err)
|
||||||
|
}
|
||||||
|
account := acme.Account{
|
||||||
|
Contact: []string{"mailto:you@example.com"},
|
||||||
|
TermsOfServiceAgreed: true,
|
||||||
|
PrivateKey: accountPrivateKey,
|
||||||
|
}
|
||||||
|
account, err = client.NewAccount(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("new account: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every certificate needs a key.
|
||||||
|
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating certificate key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{
|
||||||
|
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("obtaining certificate for allowed domain: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACME servers should usually give you the entire certificate chain
|
||||||
|
// in PEM format, and sometimes even alternate chains! It's up to you
|
||||||
|
// which one(s) to store and use, but whatever you do, be sure to
|
||||||
|
// store the certificate and key somewhere safe and secure, i.e. don't
|
||||||
|
// lose them!
|
||||||
|
for _, cert := range certs {
|
||||||
|
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
||||||
|
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
|
t.Logf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACMEServerDenyPolicy(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
local_certs
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
pki {
|
||||||
|
ca local {
|
||||||
|
name "Caddy Local Authority"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acme.localhost {
|
||||||
|
acme_server {
|
||||||
|
deny {
|
||||||
|
domains deny.localhost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
logger, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := acmez.Client{
|
||||||
|
Client: &acme.Client{
|
||||||
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
|
HTTPClient: tester.Client,
|
||||||
|
Logger: logger,
|
||||||
|
},
|
||||||
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating account key: %v", err)
|
||||||
|
}
|
||||||
|
account := acme.Account{
|
||||||
|
Contact: []string{"mailto:you@example.com"},
|
||||||
|
TermsOfServiceAgreed: true,
|
||||||
|
PrivateKey: accountPrivateKey,
|
||||||
|
}
|
||||||
|
account, err = client.NewAccount(ctx, account)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("new account: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every certificate needs a key.
|
||||||
|
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("generating certificate key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{
|
||||||
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
||||||
|
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
|
t.Logf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
|
|||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
|
admin localhost:2999
|
||||||
|
skip_install_trust
|
||||||
http_port 9080
|
http_port 9080
|
||||||
https_port 9443
|
https_port 9443
|
||||||
}
|
}
|
||||||
@@ -25,6 +27,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
|
|||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
http_port 9080
|
http_port 9080
|
||||||
https_port 9443
|
https_port 9443
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,8 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
|
|||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
http_port 9080
|
http_port 9080
|
||||||
https_port 9443
|
https_port 9443
|
||||||
}
|
}
|
||||||
@@ -53,6 +59,9 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
|||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2999"
|
||||||
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"http_port": 9080,
|
"http_port": 9080,
|
||||||
@@ -74,7 +83,14 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"local": {
|
||||||
|
"install_trust": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "json")
|
`, "json")
|
||||||
@@ -85,6 +101,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
|
|||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
http_port 9080
|
http_port 9080
|
||||||
https_port 9443
|
https_port 9443
|
||||||
local_certs
|
local_certs
|
||||||
@@ -108,6 +126,8 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
|
|||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
{
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
http_port 9080
|
http_port 9080
|
||||||
https_port 9443
|
https_port 9443
|
||||||
local_certs
|
local_certs
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
pki {
|
||||||
|
ca custom-ca {
|
||||||
|
name "Custom CA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acme.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca custom-ca
|
||||||
|
challenges dns-01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "custom-ca",
|
||||||
|
"challenges": [
|
||||||
|
"dns-01"
|
||||||
|
],
|
||||||
|
"handler": "acme_server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"custom-ca": {
|
||||||
|
"name": "Custom CA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
pki {
|
||||||
|
ca custom-ca {
|
||||||
|
name "Custom CA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acme.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca custom-ca
|
||||||
|
challenges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "custom-ca",
|
||||||
|
"handler": "acme_server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"custom-ca": {
|
||||||
|
"name": "Custom CA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
pki {
|
||||||
|
ca internal {
|
||||||
|
name "Internal"
|
||||||
|
root_cn "Internal Root Cert"
|
||||||
|
intermediate_cn "Internal Intermediate Cert"
|
||||||
|
}
|
||||||
|
ca internal-long-lived {
|
||||||
|
name "Long-lived"
|
||||||
|
root_cn "Internal Root Cert 2"
|
||||||
|
intermediate_cn "Internal Intermediate Cert 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acme-internal.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca internal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acme-long-lived.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca internal-long-lived
|
||||||
|
lifetime 7d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme-long-lived.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "internal-long-lived",
|
||||||
|
"handler": "acme_server",
|
||||||
|
"lifetime": 604800000000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme-internal.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "internal",
|
||||||
|
"handler": "acme_server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"internal": {
|
||||||
|
"name": "Internal",
|
||||||
|
"root_common_name": "Internal Root Cert",
|
||||||
|
"intermediate_common_name": "Internal Intermediate Cert"
|
||||||
|
},
|
||||||
|
"internal-long-lived": {
|
||||||
|
"name": "Long-lived",
|
||||||
|
"root_common_name": "Internal Root Cert 2",
|
||||||
|
"intermediate_common_name": "Internal Intermediate Cert 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
pki {
|
||||||
|
ca custom-ca {
|
||||||
|
name "Custom CA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acme.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca custom-ca
|
||||||
|
challenges dns-01 http-01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "custom-ca",
|
||||||
|
"challenges": [
|
||||||
|
"dns-01",
|
||||||
|
"http-01"
|
||||||
|
],
|
||||||
|
"handler": "acme_server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"custom-ca": {
|
||||||
|
"name": "Custom CA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
pki {
|
||||||
|
ca internal {
|
||||||
|
name "Internal"
|
||||||
|
root_cn "Internal Root Cert"
|
||||||
|
intermediate_cn "Internal Intermediate Cert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acme.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca internal
|
||||||
|
sign_with_root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "internal",
|
||||||
|
"handler": "acme_server",
|
||||||
|
"sign_with_root": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"internal": {
|
||||||
|
"name": "Internal",
|
||||||
|
"root_common_name": "Internal Root Cert",
|
||||||
|
"intermediate_common_name": "Internal Intermediate Cert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
auto_https prefer_wildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
*.example.com {
|
||||||
|
tls {
|
||||||
|
dns mock
|
||||||
|
}
|
||||||
|
respond "fallback"
|
||||||
|
}
|
||||||
|
|
||||||
|
foo.example.com {
|
||||||
|
respond "foo"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"foo.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "foo",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"*.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "fallback",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"automatic_https": {
|
||||||
|
"prefer_wildcard": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"*.example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"provider": {
|
||||||
|
"name": "mock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
auto_https disable_redirects
|
||||||
|
admin off
|
||||||
|
}
|
||||||
|
|
||||||
|
http://localhost {
|
||||||
|
bind fd/{env.CADDY_HTTP_FD} {
|
||||||
|
protocols h1
|
||||||
|
}
|
||||||
|
log
|
||||||
|
respond "Hello, HTTP!"
|
||||||
|
}
|
||||||
|
|
||||||
|
https://localhost {
|
||||||
|
bind fd/{env.CADDY_HTTPS_FD} {
|
||||||
|
protocols h1 h2
|
||||||
|
}
|
||||||
|
bind fdgram/{env.CADDY_HTTP3_FD} {
|
||||||
|
protocols h3
|
||||||
|
}
|
||||||
|
log
|
||||||
|
respond "Hello, HTTPS!"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"admin": {
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
"fd/{env.CADDY_HTTPS_FD}",
|
||||||
|
"fdgram/{env.CADDY_HTTP3_FD}"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Hello, HTTPS!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"automatic_https": {
|
||||||
|
"disable_redirects": true
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logger_names": {
|
||||||
|
"localhost": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listen_protocols": [
|
||||||
|
[
|
||||||
|
"h1",
|
||||||
|
"h2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"h3"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"automatic_https": {
|
||||||
|
"disable_redirects": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"srv2": {
|
||||||
|
"listen": [
|
||||||
|
"fd/{env.CADDY_HTTP_FD}"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Hello, HTTP!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"automatic_https": {
|
||||||
|
"disable_redirects": true,
|
||||||
|
"skip": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logger_names": {
|
||||||
|
"localhost": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listen_protocols": [
|
||||||
|
[
|
||||||
|
"h1"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
example.com {
|
||||||
|
bind tcp6/[::]
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
"tcp6/[::]:443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
:8443 {
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8443"
|
||||||
|
],
|
||||||
|
"tls_connection_policies": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"on_demand": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+2
@@ -11,6 +11,7 @@ encode gzip zstd {
|
|||||||
header Content-Type application/xhtml+xml*
|
header Content-Type application/xhtml+xml*
|
||||||
header Content-Type application/atom+xml*
|
header Content-Type application/atom+xml*
|
||||||
header Content-Type application/rss+xml*
|
header Content-Type application/rss+xml*
|
||||||
|
header Content-Type application/wasm*
|
||||||
header Content-Type image/svg+xml*
|
header Content-Type image/svg+xml*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,7 @@ encode {
|
|||||||
"application/xhtml+xml*",
|
"application/xhtml+xml*",
|
||||||
"application/atom+xml*",
|
"application/atom+xml*",
|
||||||
"application/rss+xml*",
|
"application/rss+xml*",
|
||||||
|
"application/wasm*",
|
||||||
"image/svg+xml*"
|
"image/svg+xml*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
foo.localhost {
|
||||||
|
root * /srv
|
||||||
|
error /private* "Unauthorized" 410
|
||||||
|
error /fivehundred* "Internal Server Error" 500
|
||||||
|
|
||||||
|
handle_errors 5xx {
|
||||||
|
respond "Error In range [500 .. 599]"
|
||||||
|
}
|
||||||
|
handle_errors 410 {
|
||||||
|
respond "404 or 410 error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bar.localhost {
|
||||||
|
root * /srv
|
||||||
|
error /private* "Unauthorized" 410
|
||||||
|
error /fivehundred* "Internal Server Error" 500
|
||||||
|
|
||||||
|
handle_errors 5xx {
|
||||||
|
respond "Error In range [500 .. 599] from second site"
|
||||||
|
}
|
||||||
|
handle_errors 410 {
|
||||||
|
respond "404 or 410 error from second site"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"foo.localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Internal Server Error",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/fivehundred*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 410
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"bar.localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Internal Server Error",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/fivehundred*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 410
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"foo.localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "404 or 410 error",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} in [410]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Error In range [500 .. 599]",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"bar.localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "404 or 410 error from second site",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} in [410]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Error In range [500 .. 599] from second site",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
http_port 3010
|
||||||
|
}
|
||||||
|
localhost:3010 {
|
||||||
|
root * /srv
|
||||||
|
error /private* "Unauthorized" 410
|
||||||
|
error /hidden* "Not found" 404
|
||||||
|
|
||||||
|
handle_errors 4xx {
|
||||||
|
respond "Error in the [400 .. 499] range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 3010,
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":3010"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 410
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Not found",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/hidden*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Error in the [400 .. 499] range",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
http_port 2099
|
||||||
|
}
|
||||||
|
localhost:2099 {
|
||||||
|
root * /srv
|
||||||
|
error /private* "Unauthorized" 410
|
||||||
|
error /threehundred* "Moved Permanently" 301
|
||||||
|
error /internalerr* "Internal Server Error" 500
|
||||||
|
|
||||||
|
handle_errors 500 3xx {
|
||||||
|
respond "Error code is equal to 500 or in the [300..399] range"
|
||||||
|
}
|
||||||
|
handle_errors 4xx {
|
||||||
|
respond "Error in the [400 .. 499] range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 2099,
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":2099"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Moved Permanently",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 301
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/threehundred*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Internal Server Error",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/internalerr*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 410
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Error in the [400 .. 499] range",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Error code is equal to 500 or in the [300..399] range",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} \u003e= 300 \u0026\u0026 {http.error.status_code} \u003c= 399 || {http.error.status_code} in [500]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
http_port 3010
|
||||||
|
}
|
||||||
|
localhost:3010 {
|
||||||
|
root * /srv
|
||||||
|
error /private* "Unauthorized" 410
|
||||||
|
error /hidden* "Not found" 404
|
||||||
|
|
||||||
|
handle_errors 404 410 {
|
||||||
|
respond "404 or 410 error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 3010,
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":3010"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 410
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Not found",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/hidden*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "404 or 410 error",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} in [404, 410]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
http_port 2099
|
||||||
|
}
|
||||||
|
localhost:2099 {
|
||||||
|
root * /srv
|
||||||
|
error /private* "Unauthorized" 410
|
||||||
|
error /hidden* "Not found" 404
|
||||||
|
error /internalerr* "Internal Server Error" 500
|
||||||
|
|
||||||
|
handle_errors {
|
||||||
|
respond "Fallback route: code outside the [400..499] range"
|
||||||
|
}
|
||||||
|
handle_errors 4xx {
|
||||||
|
respond "Error in the [400 .. 499] range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 2099,
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":2099"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Internal Server Error",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/internalerr*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 410
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Not found",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/hidden*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Error in the [400 .. 499] range",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Fallback route: code outside the [400..499] range",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
(snippet) {
|
||||||
|
@g `{http.error.status_code} == 404`
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com
|
||||||
|
|
||||||
|
@a expression {http.error.status_code} == 400
|
||||||
|
abort @a
|
||||||
|
|
||||||
|
@b expression {http.error.status_code} == "401"
|
||||||
|
abort @b
|
||||||
|
|
||||||
|
@c expression {http.error.status_code} == `402`
|
||||||
|
abort @c
|
||||||
|
|
||||||
|
@d expression "{http.error.status_code} == 403"
|
||||||
|
abort @d
|
||||||
|
|
||||||
|
@e expression `{http.error.status_code} == 404`
|
||||||
|
abort @e
|
||||||
|
|
||||||
|
@f `{http.error.status_code} == 404`
|
||||||
|
abort @f
|
||||||
|
|
||||||
|
import snippet
|
||||||
|
abort @g
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == 400"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == \"401\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == `402`"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": {
|
||||||
|
"expr": "{http.error.status_code} == 403",
|
||||||
|
"name": "d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": {
|
||||||
|
"expr": "{http.error.status_code} == 404",
|
||||||
|
"name": "e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": {
|
||||||
|
"expr": "{http.error.status_code} == 404",
|
||||||
|
"name": "f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": {
|
||||||
|
"expr": "{http.error.status_code} == 404",
|
||||||
|
"name": "g"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
:8080 {
|
||||||
|
root * ./
|
||||||
|
file_server {
|
||||||
|
etag_file_extensions .b3sum .sha256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8080"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "./"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etag_file_extensions": [
|
||||||
|
".b3sum",
|
||||||
|
".sha256"
|
||||||
|
],
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
file_server {
|
||||||
|
pass_thru
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
],
|
||||||
|
"pass_thru": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
file_server {
|
||||||
|
browse {
|
||||||
|
sort size desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"browse": {
|
||||||
|
"sort": [
|
||||||
|
"size",
|
||||||
|
"desc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
app.example.com {
|
||||||
|
forward_auth authelia:9091 {
|
||||||
|
uri /api/verify?rd=https://authelia.example.com
|
||||||
|
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy backend:8080
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"app.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"Remote-Email": [
|
||||||
|
"{http.reverse_proxy.header.Remote-Email}"
|
||||||
|
],
|
||||||
|
"Remote-Groups": [
|
||||||
|
"{http.reverse_proxy.header.Remote-Groups}"
|
||||||
|
],
|
||||||
|
"Remote-Name": [
|
||||||
|
"{http.reverse_proxy.header.Remote-Name}"
|
||||||
|
],
|
||||||
|
"Remote-User": [
|
||||||
|
"{http.reverse_proxy.header.Remote-User}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"headers": {
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"X-Forwarded-Method": [
|
||||||
|
"{http.request.method}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-Uri": [
|
||||||
|
"{http.request.uri}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rewrite": {
|
||||||
|
"method": "GET",
|
||||||
|
"uri": "/api/verify?rd=https://authelia.example.com"
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "authelia:9091"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "backend:8080"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
:8881
|
||||||
|
|
||||||
|
forward_auth localhost:9000 {
|
||||||
|
uri /auth
|
||||||
|
copy_headers A>1 B C>3 {
|
||||||
|
D
|
||||||
|
E>5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8881"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"1": [
|
||||||
|
"{http.reverse_proxy.header.A}"
|
||||||
|
],
|
||||||
|
"3": [
|
||||||
|
"{http.reverse_proxy.header.C}"
|
||||||
|
],
|
||||||
|
"5": [
|
||||||
|
"{http.reverse_proxy.header.E}"
|
||||||
|
],
|
||||||
|
"B": [
|
||||||
|
"{http.reverse_proxy.header.B}"
|
||||||
|
],
|
||||||
|
"D": [
|
||||||
|
"{http.reverse_proxy.header.D}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"headers": {
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"X-Forwarded-Method": [
|
||||||
|
"{http.request.method}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-Uri": [
|
||||||
|
"{http.request.uri}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rewrite": {
|
||||||
|
"method": "GET",
|
||||||
|
"uri": "/auth"
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:9000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-9
@@ -3,6 +3,7 @@
|
|||||||
http_port 8080
|
http_port 8080
|
||||||
https_port 8443
|
https_port 8443
|
||||||
grace_period 5s
|
grace_period 5s
|
||||||
|
shutdown_delay 10s
|
||||||
default_sni localhost
|
default_sni localhost
|
||||||
order root first
|
order root first
|
||||||
storage file_system {
|
storage file_system {
|
||||||
@@ -10,13 +11,12 @@
|
|||||||
}
|
}
|
||||||
acme_ca https://example.com
|
acme_ca https://example.com
|
||||||
acme_ca_root /path/to/ca.crt
|
acme_ca_root /path/to/ca.crt
|
||||||
|
ocsp_stapling off
|
||||||
|
|
||||||
email test@example.com
|
email test@example.com
|
||||||
admin off
|
admin off
|
||||||
on_demand_tls {
|
on_demand_tls {
|
||||||
ask https://example.com
|
ask https://example.com
|
||||||
interval 30s
|
|
||||||
burst 20
|
|
||||||
}
|
}
|
||||||
local_certs
|
local_certs
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"http_port": 8080,
|
"http_port": 8080,
|
||||||
"https_port": 8443,
|
"https_port": 8443,
|
||||||
"grace_period": 5000000000,
|
"grace_period": 5000000000,
|
||||||
|
"shutdown_delay": 10000000000,
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
@@ -61,17 +62,18 @@
|
|||||||
"module": "internal"
|
"module": "internal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"key_type": "ed25519"
|
"key_type": "ed25519",
|
||||||
|
"disable_ocsp_stapling": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
"rate_limit": {
|
"permission": {
|
||||||
"interval": 30000000000,
|
"endpoint": "https://example.com",
|
||||||
"burst": 20
|
"module": "http"
|
||||||
},
|
}
|
||||||
"ask": "https://example.com"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"disable_ocsp_stapling": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+16
-7
@@ -17,10 +17,10 @@
|
|||||||
admin off
|
admin off
|
||||||
on_demand_tls {
|
on_demand_tls {
|
||||||
ask https://example.com
|
ask https://example.com
|
||||||
interval 30s
|
|
||||||
burst 20
|
|
||||||
}
|
}
|
||||||
storage_clean_interval 7d
|
storage_clean_interval 7d
|
||||||
|
renew_interval 1d
|
||||||
|
ocsp_interval 2d
|
||||||
|
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,14 @@
|
|||||||
"issuers": [
|
"issuers": [
|
||||||
{
|
{
|
||||||
"ca": "https://example.com",
|
"ca": "https://example.com",
|
||||||
|
"challenges": {
|
||||||
|
"http": {
|
||||||
|
"alternate_port": 8080
|
||||||
|
},
|
||||||
|
"tls-alpn": {
|
||||||
|
"alternate_port": 8443
|
||||||
|
}
|
||||||
|
},
|
||||||
"email": "test@example.com",
|
"email": "test@example.com",
|
||||||
"external_account": {
|
"external_account": {
|
||||||
"key_id": "4K2scIVbBpNd-78scadB2g",
|
"key_id": "4K2scIVbBpNd-78scadB2g",
|
||||||
@@ -76,12 +84,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
"rate_limit": {
|
"permission": {
|
||||||
"interval": 30000000000,
|
"endpoint": "https://example.com",
|
||||||
"burst": 20
|
"module": "http"
|
||||||
},
|
}
|
||||||
"ask": "https://example.com"
|
|
||||||
},
|
},
|
||||||
|
"ocsp_interval": 172800000000000,
|
||||||
|
"renew_interval": 86400000000000,
|
||||||
"storage_clean_interval": 604800000000000
|
"storage_clean_interval": 604800000000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-7
@@ -16,8 +16,6 @@
|
|||||||
}
|
}
|
||||||
on_demand_tls {
|
on_demand_tls {
|
||||||
ask https://example.com
|
ask https://example.com
|
||||||
interval 30s
|
|
||||||
burst 20
|
|
||||||
}
|
}
|
||||||
local_certs
|
local_certs
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
@@ -71,11 +69,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
"rate_limit": {
|
"permission": {
|
||||||
"interval": 30000000000,
|
"endpoint": "https://example.com",
|
||||||
"burst": 20
|
"module": "http"
|
||||||
},
|
}
|
||||||
"ask": "https://example.com"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
http_port 8080
|
||||||
|
persist_config off
|
||||||
|
admin {
|
||||||
|
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:80
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2019",
|
||||||
|
"origins": [
|
||||||
|
"localhost:2019",
|
||||||
|
"[::1]:2019",
|
||||||
|
"127.0.0.1:2019",
|
||||||
|
"192.168.10.128"
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"persist": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 8080,
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
|
||||||
|
:8881 {
|
||||||
|
log {
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"exclude": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"log0": {
|
||||||
|
"encoder": {
|
||||||
|
"format": "console"
|
||||||
|
},
|
||||||
|
"level": "DEBUG",
|
||||||
|
"include": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8881"
|
||||||
|
],
|
||||||
|
"logs": {
|
||||||
|
"default_logger_name": "log0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
default_bind tcp4/0.0.0.0 tcp6/[::]
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
example.org:12345 {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
"tcp4/0.0.0.0:12345",
|
||||||
|
"tcp6/[::]:12345"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.org"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
"tcp4/0.0.0.0:443",
|
||||||
|
"tcp6/[::]:443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user