mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
850 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d16ede358a | |||
| c82c231ba7 | |||
| 3ee663dee1 | |||
| 8ec51bbede | |||
| bc453fa6ae | |||
| e3324aa6de | |||
| d55d50b3b3 | |||
| b95b87381a | |||
| b01bb275b3 | |||
| 309c1fec62 | |||
| b88e2b6a49 | |||
| 4217217bad | |||
| 1c5969b576 | |||
| 0ee4378227 | |||
| 9859ab8148 | |||
| 00e6b77fe4 | |||
| d4f249741e | |||
| 04f50a9759 | |||
| 4cd7ae35b3 | |||
| 24f34780b6 | |||
| 724b74d981 | |||
| 4940325844 | |||
| 744d04c258 | |||
| ecbc1f85c5 | |||
| 997ef522bc | |||
| 0279a57ac4 | |||
| c94f5bb7dd | |||
| 0afbab8667 | |||
| fc65320e9c | |||
| e385be9225 | |||
| 66863aad3b | |||
| c42bfaf31e | |||
| e2f913bb7f | |||
| 65a09524c3 | |||
| c6d6a775a1 | |||
| 4accf737a6 | |||
| ff19bddac5 | |||
| 584eba94a4 | |||
| 904f149e5b | |||
| 8b80a3201f | |||
| 68529e2f9e | |||
| 399eff415c | |||
| c054a818a1 | |||
| af5c148ed1 | |||
| 514eef33fe | |||
| 3860b235d0 | |||
| 6f73a358f4 | |||
| 6a14e2c2a8 | |||
| 2bc30bb780 | |||
| 28d870c193 | |||
| fb9d874fa9 | |||
| 6cea1f239d | |||
| 2ae8c11927 | |||
| e9b1d7dcb4 | |||
| bd9d796e6e | |||
| 246a31aacd | |||
| 0665a86eb7 | |||
| 3fdaf50785 | |||
| 19cc2bd3c3 | |||
| 705de11bef | |||
| 8a0fff58aa | |||
| 6f0f159ba5 | |||
| 6eafd4e82f | |||
| eda54c22a6 | |||
| 2c71fb116b | |||
| 724613a1be | |||
| 735c86658d | |||
| a2dae1d43f | |||
| efc0cc5e85 | |||
| 0bf2565c37 | |||
| 7bfe5b6c95 | |||
| 2a5599e2ad | |||
| c35820012b | |||
| 2d0f8831f8 | |||
| d7dbf85525 | |||
| 77f233a484 | |||
| ddd690de4c | |||
| 6004d3f779 | |||
| caca55e582 | |||
| c9049bdc24 | |||
| 21c00a3cd2 | |||
| 61b7002d26 | |||
| b1480eb52f | |||
| 5bc4777be9 | |||
| 3af15c0725 | |||
| 6db3615547 | |||
| 32cafbb630 | |||
| 003403ecbc | |||
| 5b48f784ae | |||
| d84a5d8427 | |||
| 7da32f493a | |||
| cb0d9838cb | |||
| d81a69ef16 | |||
| 99dcc10f31 | |||
| fa4cdde7d8 | |||
| d55c3b31eb | |||
| 6d03fb48f9 | |||
| b3bff13f7d | |||
| 7211101c52 | |||
| 90dba172cb | |||
| 4b10ae5ce6 | |||
| 1dfb11486e | |||
| 11a132d48b | |||
| 9dafa63933 | |||
| 21c1da101c | |||
| 7a99835dab | |||
| 7b0962ba4d | |||
| 2d1f7b9da8 | |||
| a285fe4129 | |||
| 97e61c16a3 | |||
| 83551edf3e | |||
| e18c373064 | |||
| 9a7756c6e4 | |||
| fdf2a77feb | |||
| a496308f6e | |||
| d5d7fb5954 | |||
| 996af0915d | |||
| 6c051cd27d | |||
| 9415feca7c | |||
| 881b826fb5 | |||
| 538ddb8587 | |||
| 69b5643130 | |||
| e5bbed1046 | |||
| 294910c68c | |||
| 8c5d00b2bc | |||
| aa20878887 | |||
| c1e5c09294 | |||
| ffc125d6f5 | |||
| 22055c5e0f | |||
| dfe802aed3 | |||
| 7a365af5df | |||
| 0cbf467b3f | |||
| bb67e19d7b | |||
| 1dc4ec2d77 | |||
| 452d4726f7 | |||
| 2a8a198568 | |||
| cc8fb488d3 | |||
| fae064262d | |||
| 9ee01dceac | |||
| 812278acd8 | |||
| c47ddbeffb | |||
| 483e31b978 | |||
| 41a682ddde | |||
| 7243454a96 | |||
| 3fb2c394d1 | |||
| 21de227fe9 | |||
| 62c9f2cf3e | |||
| bde3823b76 | |||
| 4df56c77e3 | |||
| cee5589b98 | |||
| 90c7b4b0a1 | |||
| aef560c7fc | |||
| 44536a7594 | |||
| ea7e4b4024 | |||
| ef6e53bb5f | |||
| 35e1d92d58 | |||
| dc9f4f13fc | |||
| 4c55d26f11 | |||
| d534162556 | |||
| 5bde8d705b | |||
| 7960b4259d | |||
| 2c91688f39 | |||
| 513e0240fd | |||
| bf8c3c25c1 | |||
| c8da8ca673 | |||
| 43fba378d6 | |||
| cd9317e5df | |||
| 8dbc5f70a5 | |||
| 07c6076ea0 | |||
| 28ab0bfb13 | |||
| 1c17e6c6bb | |||
| b814c0af9c | |||
| 9e5d9e2530 | |||
| 9408dacc27 | |||
| 12cfc19487 | |||
| afecd90a6c | |||
| 2f59467ac3 | |||
| 184e8e9f71 | |||
| 1e8c9764df | |||
| 41c7bd27b4 | |||
| 96d6d277a4 | |||
| 26e559662d | |||
| 52305618df | |||
| e051e119d1 | |||
| 8e42661060 | |||
| 86a4f2c9f4 | |||
| a507a5bbc7 | |||
| d0770dbbb3 | |||
| a77bd1d887 | |||
| bca610fbde | |||
| 1fa8c185a8 | |||
| a1796c2f14 | |||
| f931c26f68 | |||
| 10db57027d | |||
| c11d0e47a3 | |||
| 9770ce7c9f | |||
| 5ae1a5617c | |||
| 83c85c53f5 | |||
| 768383a610 | |||
| 570d84f7d3 | |||
| a6761153cb | |||
| 02845bc9fd | |||
| 97ed9e111d | |||
| 100d19e3af | |||
| ebf07f853b | |||
| 1b061815b2 | |||
| 026937fab5 | |||
| 295604d6df | |||
| bacf50a59e | |||
| da8686c4b9 | |||
| e3a8f72f1c | |||
| bae4f15fad | |||
| 0798459e44 | |||
| f980170909 | |||
| 6963a72a63 | |||
| 76bbb473a5 | |||
| 3c70950fa1 | |||
| 7c171542ed | |||
| 9a572635f5 | |||
| f5ccb904a3 | |||
| 829e36d535 | |||
| 2609a72893 | |||
| ec456811bb | |||
| 68cebb28d0 | |||
| a3bdc22234 | |||
| d3383ced2a | |||
| c024ae096d | |||
| 3bee569a8a | |||
| 999ab22b8c | |||
| 9991fdc495 | |||
| f29023bf8f | |||
| 85f5f47f31 | |||
| 6e4132eb89 | |||
| d89ad2fd5b | |||
| d33926b63f | |||
| c5f9227a48 | |||
| 88d391c1f5 | |||
| b4a7d6267f | |||
| e5dc76b054 | |||
| 7dfd69cdc5 | |||
| 28fdf64dc5 | |||
| 0fe98038b6 | |||
| 6e4c688ea7 | |||
| 5110643201 | |||
| 4d9b63d909 | |||
| e30deedcc1 | |||
| fbd9515d35 | |||
| 95f6bd7e5c | |||
| b1ce9d4db7 | |||
| 61679b74f5 | |||
| 2c1b663156 | |||
| 8b2dbc52ec | |||
| 657f0cab17 | |||
| 7be747fbe9 | |||
| 5b355cbed0 | |||
| a3cfe437b1 | |||
| 437d5095a6 | |||
| 145aebbba5 | |||
| 6a32daa225 | |||
| 81cdebf648 | |||
| 84c729e96a | |||
| 346c33b4d5 | |||
| 78717ce5b0 | |||
| 3d6fc1e1b7 | |||
| c7ac7de38a | |||
| 05164c895a | |||
| 1e8af27329 | |||
| b6482e53c1 | |||
| 20f6795413 | |||
| 84f16852ab | |||
| 1456f15f9a | |||
| fdfe2ae53b | |||
| 1c190b001b | |||
| 3634c4593f | |||
| 7ca15861dd | |||
| 8ff330c555 | |||
| 626f19a264 | |||
| 6ca5828221 | |||
| 6fe04a30b1 | |||
| 19b45546a7 | |||
| d322de6b42 | |||
| ce3ca541d8 | |||
| 581f1defcb | |||
| 0d2a3511dc | |||
| 73643ea736 | |||
| 809e72792c | |||
| 9fb0b1e838 | |||
| 244b839f98 | |||
| 904d9cab39 | |||
| ac65f690ae | |||
| 37aa516a6e | |||
| 105acfa086 | |||
| deba26d225 | |||
| 178ba024fe | |||
| e207240f9a | |||
| 397e04ebd9 | |||
| d2c15bea1b | |||
| 8da9eaee34 | |||
| ea3688e1c0 | |||
| c87f82f0ce | |||
| 5c55e5d53f | |||
| 7ee3ab7baa | |||
| ba08833b2a | |||
| 9eecd698da | |||
| 0fa1a3b630 | |||
| 673d3d00f2 | |||
| 2acb208e32 | |||
| e02117cb8a | |||
| 95b2863df2 | |||
| 341d4fb805 | |||
| 745cb0e9e6 | |||
| 9af05719bc | |||
| d08cbefff8 | |||
| 2eede58b3a | |||
| 235357abc8 | |||
| 4b4e16edaf | |||
| ee64719d93 | |||
| 2491336c11 | |||
| 1698838685 | |||
| 4c43bf8cc8 | |||
| 348cb798e2 | |||
| e211491407 | |||
| 6e2fabb2a4 | |||
| 8cc60e6896 | |||
| bea8dedfb2 | |||
| f2ce81cc8b | |||
| 2cab475ba5 | |||
| c32f383a01 | |||
| 37093befd5 | |||
| d692d503a3 | |||
| 3c1def2430 | |||
| b583007c49 | |||
| 6b60a301c0 | |||
| d6632e2145 | |||
| 903776238e | |||
| f741ab3463 | |||
| 76ac28a624 | |||
| 61b427fa47 | |||
| 42a6628935 | |||
| 6a4d638c1e | |||
| aa6c5fde07 | |||
| 31c6ac097e | |||
| 406df22a16 | |||
| afb2ca27c1 | |||
| ce45353e61 | |||
| 89124aa570 | |||
| ab2fc9d066 | |||
| fc7340e11a | |||
| 3f48a2eb45 | |||
| f192ae5ea5 | |||
| b62f8e0582 | |||
| ae86f6dd91 | |||
| b550ea433b | |||
| e42514ad4a | |||
| f596fd77bb | |||
| 0433f9d075 | |||
| c67c8e60cc | |||
| 8f8ecd2e2a | |||
| 115b877e1a | |||
| 2ce3deb540 | |||
| acf4dde1dd | |||
| 7a4548c582 | |||
| 6cbd93736f | |||
| c447236357 | |||
| 5a19db5dc2 | |||
| cfe85a9fe6 | |||
| 90f1f7bce7 | |||
| 2762f8f058 | |||
| 99d34f1c1d | |||
| 36a6c7daf0 | |||
| ca6e54bbb8 | |||
| fb5168d3b4 | |||
| 217419f6d9 | |||
| 4d18587192 | |||
| b216d285df | |||
| b8cba62643 | |||
| 3f5d27cd5d | |||
| 26fb8b3efd | |||
| e6c6210772 | |||
| 1324da2241 | |||
| 71e81d262b | |||
| 5fe69ac4ab | |||
| e717028f83 | |||
| a60da8e7ab | |||
| 00e99df209 | |||
| c83d40ccd4 | |||
| e4ec08e977 | |||
| 03ab55b51a | |||
| cef6e098bb | |||
| 260982b2df | |||
| 0130b699df | |||
| ca5c679880 | |||
| e2d41ee761 | |||
| 86b785e51c | |||
| f6ae092507 | |||
| a2a41a5bdf | |||
| 6fb98ba188 | |||
| 063ed1e7f9 | |||
| 2de0acc11f | |||
| 5d97522d18 | |||
| f6b9cb7122 | |||
| 78760c0ddc | |||
| b0a491aec8 | |||
| 45b171ff3a | |||
| 623a1c588e | |||
| 7cca291d62 | |||
| e3591009dc | |||
| 30c14084ab | |||
| 99f91c4c6f | |||
| 0005e3acdc | |||
| 0b09b070e5 | |||
| 7f9cfcc0f2 | |||
| 87a742c1e5 | |||
| 57c6f22684 | |||
| dd103a6787 | |||
| 23cc26d585 | |||
| bc2e406572 | |||
| bf776e7de7 | |||
| f42b138fb1 | |||
| 2cc5d2227d | |||
| 15bf9c196c | |||
| eb80165583 | |||
| 17d938fc54 | |||
| 98bbc54fdc | |||
| 9bdd6caa0b | |||
| f7f6e371ef | |||
| b8cf4d5897 | |||
| 04ec3c5f05 | |||
| 8b28c36d48 | |||
| 4a07a5d41e | |||
| b81ae38686 | |||
| 5c7ca7d96e | |||
| ec56c25708 | |||
| c0f827e0bd | |||
| 490cd02f82 | |||
| 9639fe7d28 | |||
| 3592e59399 | |||
| f74fed3f54 | |||
| 8b2ad61220 | |||
| 6614d1c495 | |||
| c6bddbfbe2 | |||
| 0742530d3d | |||
| 6b6cd934d0 | |||
| 5b878d5bd3 | |||
| 2105d59936 | |||
| 9a1370c2c8 | |||
| d810637a9f | |||
| 5d3ccf1eb7 | |||
| aad9f90cad | |||
| 07ef4b0c7d | |||
| 2bfaf8e896 | |||
| 372540f0ee | |||
| 793a405810 | |||
| 85ff0e3604 | |||
| e51e56a494 | |||
| 35174a8ba8 | |||
| 21643a007a | |||
| 2466ed1484 | |||
| a66f461201 | |||
| 07ad4655db | |||
| 271b5af148 | |||
| 99e2b56519 | |||
| 64f0173948 | |||
| fe5a531c58 | |||
| 8c0c1a7b88 | |||
| 25dea2903e | |||
| d876de61e5 | |||
| 8be1f0ea66 | |||
| 2eda21ec6d | |||
| d418e319ab | |||
| ba514f9660 | |||
| 3dcc34d341 | |||
| 871abf1053 | |||
| 29315847a8 | |||
| 994b9033e9 | |||
| 590480513a | |||
| 7527c01705 | |||
| 8aef859a55 | |||
| a5ebec0041 | |||
| 7c419d5349 | |||
| 3828a3aaac | |||
| 8bae8f5f5a | |||
| 21f1f95e7b | |||
| 78e98c40d3 | |||
| 5c99267dd8 | |||
| a6df4cdbbc | |||
| dff78d82ce | |||
| 8c7c2e4af2 | |||
| 3d9f8eac08 | |||
| 06ea0a5295 | |||
| 788462bd4c | |||
| 5a0603ed72 | |||
| 984d384d14 | |||
| fdabac51a8 | |||
| 95d944613b | |||
| 2b33d9a5e5 | |||
| 5c8b502964 | |||
| 82bebfab8a | |||
| be3849c267 | |||
| 16ee985c22 | |||
| 95ed603de7 | |||
| cbb405f6aa | |||
| 724c728678 | |||
| 21408212da | |||
| fe516575db | |||
| 080a62d5c5 | |||
| dae4913fe3 | |||
| 6455efa5d3 | |||
| 5ab17a3a37 | |||
| c3bcd967bd | |||
| 6ea121ddf8 | |||
| 8005b7ab73 | |||
| b1a456cfe3 | |||
| 5e9d81b507 | |||
| 09a8517065 | |||
| 87b6cf470b | |||
| f935458e3e | |||
| 2e0615270d | |||
| fab5e4372a | |||
| 3c90e370a4 | |||
| a8533e5630 | |||
| b07f6958ac | |||
| 33a318d173 | |||
| 68adfdc559 | |||
| a841688cc0 | |||
| 52ae5f70d2 | |||
| 44f23a67bb | |||
| 8b7d6a9ee8 | |||
| 7c7ef8d40e | |||
| 14d3fd7d03 | |||
| 512b004332 | |||
| db4293cb5f | |||
| 6e10586303 | |||
| 8de1a76227 | |||
| 9fe54e1c60 | |||
| b43e986a52 | |||
| 1228dd7d93 | |||
| af26a03da1 | |||
| 8025ad9107 | |||
| 6cdb2392d7 | |||
| 0ca109db4a | |||
| 0fc97211ab | |||
| ad90b273db | |||
| 93bc1b72e3 | |||
| a19da07b72 | |||
| 16782d9988 | |||
| dfdddcfacb | |||
| 7ff02f37b6 | |||
| e4a2add73f | |||
| 95615f5377 | |||
| 8e515289cb | |||
| 6e95477224 | |||
| 97d918df3e | |||
| f5c6a8553c | |||
| 263ffbfaec | |||
| bf363f061d | |||
| 7129f6c1c0 | |||
| cb25dd72ab | |||
| d55fa68902 | |||
| b1f41d0ff1 | |||
| 6011ce120a | |||
| 27e288ab19 | |||
| 35f70c98fa | |||
| fb06c041c4 | |||
| 8ef0a0b4f8 | |||
| 8d3c64932e | |||
| 0dd9243478 | |||
| 432b94239d | |||
| 4611537f06 | |||
| 76c22c7b38 | |||
| c7da6175bc | |||
| 11a2733dc2 | |||
| 1be121cec7 | |||
| dccba71276 | |||
| be36aade9a | |||
| ba0000678d | |||
| c4c45f8e01 | |||
| 54e458b756 | |||
| d803561212 | |||
| 813fff0584 | |||
| d2e7baed8d | |||
| d6dad04e96 | |||
| 442fd748f6 | |||
| b00dfd3965 | |||
| 6c533558a3 | |||
| 2fbe2ff40b | |||
| faf67b1067 | |||
| 208f2ff93c | |||
| 19e834cf36 | |||
| bce2edd22d | |||
| a458544d9f | |||
| 2f91b44587 | |||
| e3726588b4 | |||
| abf5ab340e | |||
| acf7dea68f | |||
| bc738991b6 | |||
| fcd8869f51 | |||
| 1e31be8de0 | |||
| 4aa3af4b78 | |||
| 8715a28320 | |||
| 715e6ddf51 | |||
| 9c0bf311f9 | |||
| 5300949e0d | |||
| 411152016e | |||
| 5c7640a8d9 | |||
| f8366c2f09 | |||
| fe36d26b63 | |||
| b38365ff3b | |||
| 26cc883708 | |||
| 93943a6ac2 | |||
| 85ce15a5ad | |||
| dedcfd4e3d | |||
| 20fe9cf024 | |||
| bcbe1c220d | |||
| a53b27c62e | |||
| 03306e646e | |||
| 53dd600b4d | |||
| ce1205239a | |||
| bc3e44c1a6 | |||
| 8c55167f71 | |||
| be7abda7d4 | |||
| 6fd28b81dc | |||
| 65c060f56e | |||
| 44cb804b9e | |||
| c11e3bffd6 | |||
| f29a9eee0d | |||
| 370b78c5c7 | |||
| 1ecb216001 | |||
| 94f98c0733 | |||
| 2c3657bb8a | |||
| 5b36424cf0 | |||
| 0006df6026 | |||
| c95db3551d | |||
| 8eb2c37251 | |||
| 1e66226217 | |||
| 7b4aa108c7 | |||
| 8b11ed347b | |||
| b249b45d10 | |||
| c12bf4054c | |||
| 735d6ce405 | |||
| 7b33c8db31 | |||
| 11696793bd | |||
| 3e8bff594a | |||
| 2f684e42d5 | |||
| ba29f9d41d | |||
| 40e05e5a01 | |||
| 39d61cad2d | |||
| bc9f944837 | |||
| 4c289fc6ad | |||
| 19f36667f7 | |||
| 484cee1ac1 | |||
| d030bfdae0 | |||
| db4c73dd58 | |||
| f15f0d5839 | |||
| e73b117332 | |||
| 2fd22139c6 | |||
| 5c9ebe3af1 | |||
| 2ab2d5bf9e | |||
| c09e86fddc | |||
| 46aaf02371 | |||
| 3b80c505fb | |||
| 1d1e194229 | |||
| 839507e24e | |||
| 833d67446f | |||
| d0c1756fc5 | |||
| ed40a5dcab | |||
| 7799554baa | |||
| 2cb01d43cf | |||
| 758269124e | |||
| b4dce74e59 | |||
| fe389fcbd7 | |||
| 005a11cf4b | |||
| 194df652eb | |||
| 53bbdf1766 | |||
| e48d83452e | |||
| 2459c292a4 | |||
| 0cf592fa2e | |||
| d9136fb0a0 | |||
| c32b7e8865 | |||
| 1ce10b453f | |||
| 0c8ad52be1 | |||
| d67d8cf5a8 | |||
| 44b7ce9850 | |||
| b4f4fcd437 | |||
| 50e62d06bc | |||
| 9169cd43d4 | |||
| e12c62e60b | |||
| 3e9e7555ef | |||
| f6126acf37 | |||
| 97ace2a39e | |||
| 4bd9496525 | |||
| 14f9662f9c | |||
| 21d7b662e7 | |||
| 3ba9e143a2 | |||
| d2e46c2be0 | |||
| 80b54f3b9d | |||
| 0830fbad03 | |||
| a60d54dbfd | |||
| acb8f0e0c2 | |||
| 652460e03e | |||
| 4a1e1649bc | |||
| ccfb12347b | |||
| 50961ecc77 | |||
| 026df7c5cb | |||
| 8e821b5039 | |||
| 9d8bff28c2 | |||
| d242f10eda | |||
| 2dc4fcc62b | |||
| afd154119a | |||
| e34ff21a71 | |||
| af25f0254e | |||
| a0fd2b6c0a | |||
| c0da7d487a | |||
| 8420a2f250 | |||
| 59910923d1 | |||
| 0544f0266a | |||
| b2aa679c33 | |||
| fa334c4bdf | |||
| d73b650c26 | |||
| c9980fd367 | |||
| 42f75a4ca9 | |||
| c4159ef76d | |||
| ab885f07b8 | |||
| 4950ce485f | |||
| c8b0a97b1c | |||
| 95a447de9c | |||
| d98f2faef9 | |||
| b855e66170 | |||
| 0d3f99e85a | |||
| 28df6cedfe | |||
| dd6aa91d72 | |||
| b44a22a9d4 | |||
| bdf92ee84e | |||
| f217181293 | |||
| ccb5d19c25 | |||
| b780f0f49b | |||
| 2141626269 | |||
| 63674ba081 | |||
| 9722dbe18a | |||
| 4698352b20 | |||
| eb8625f774 | |||
| 9343403358 | |||
| 4a3a418156 | |||
| 6dfba5fda8 | |||
| d25008d2c8 | |||
| 4eb5fc541b | |||
| 42acdad9e5 | |||
| 84f9f7cd60 | |||
| 79216d356c | |||
| 9429c843c8 | |||
| 6bcba91fbe | |||
| ab101d75d0 | |||
| 7512ea1a64 | |||
| 902ec37062 | |||
| bed05f2450 | |||
| fdd871e177 | |||
| 94c28a2574 | |||
| 42386a7272 | |||
| 5e858a15f7 | |||
| 533d1afb4b | |||
| 9f8d3611eb | |||
| 3177ee8010 | |||
| 7a7c5f00c0 | |||
| fee0b38b48 | |||
| d5ae3a4966 | |||
| 31ab737bf2 | |||
| a4bdf249db | |||
| 006dc1792f | |||
| a63cb3e3fd | |||
| 2b22d2e6ea | |||
| a524bcfe78 | |||
| 91b03dccb0 | |||
| 6000855c82 | |||
| 38677aaa58 | |||
| d49f762f6d | |||
| 81a9e125b5 | |||
| 70c788ce0c | |||
| 1c443beb9c | |||
| 269b1e9aa3 | |||
| 6d0350d04e | |||
| 15647bdfb7 | |||
| 2663dd176d | |||
| 6706c9225a | |||
| 5137859e47 | |||
| b8e7453fef | |||
| f93dab755b | |||
| 0c8763a728 | |||
| f5b4f268dc | |||
| ef5f29cfb2 | |||
| 8947ae0cc1 | |||
| 878ae0002a | |||
| 37da91cfe7 | |||
| b79f86f256 | |||
| 613aecb898 | |||
| 39db06d9c4 | |||
| f064889a4f | |||
| 3439933235 | |||
| 1b6b422c63 | |||
| 2265db9028 | |||
| bf54615efc | |||
| da6a8cfc86 | |||
| 9cd6f35e9d | |||
| 210d0cf7f1 | |||
| 5a4a1421de | |||
| 34a25dd558 | |||
| 9e576c76e7 | |||
| c24a3e389f | |||
| f976451d19 | |||
| 869fbac632 | |||
| 284fb3a98c | |||
| bc00d840e8 | |||
| be9b6e7b57 | |||
| 2fd98cb040 | |||
| 67d32e6779 | |||
| 9d54f655aa | |||
| 65195a726d | |||
| b84cb05848 | |||
| a969872850 | |||
| aaacab1bc3 | |||
| d22f64e6d4 | |||
| 22995e5655 | |||
| 043eb1d9e5 | |||
| fec7fa8bfd | |||
| 1a20fe330e | |||
| 1f0c061ce3 | |||
| ff5b4639d5 | |||
| f9d93ead4e | |||
| 8ae0d6a509 | |||
| 48b5a80320 | |||
| ad3d408067 | |||
| e40bbecb16 | |||
| 8eba582efe | |||
| fbea3374e9 | |||
| 2eb3593327 | |||
| 1136e2cfee | |||
| 5859cd8dad | |||
| 43961b542b | |||
| 2d056fbe66 | |||
| 545f28008e | |||
| d42529348f | |||
| 27ecc7f384 | |||
| 402f423693 | |||
| 3eae6d43b6 | |||
| 59a5d0db28 | |||
| f976aa7443 | |||
| 6621406fa8 | |||
| 27ff6aeccb | |||
| a8dc73b4d9 | |||
| 86e2d1b0a4 | |||
| 859b5d7ea3 |
@@ -1,21 +0,0 @@
|
||||
# shell scripts should not use tabs to indent!
|
||||
*.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
|
||||
# files for systemd (shell-similar)
|
||||
*.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
|
||||
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
|
||||
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
|
||||
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
.git* text eol=auto core.whitespace whitespace=trailing-space
|
||||
+46
-22
@@ -8,7 +8,7 @@ For starters, we invite you to join [the Caddy forum](https://caddy.community) w
|
||||
## Common Tasks
|
||||
|
||||
- [Contributing code](#contributing-code)
|
||||
- [Writing a plugin](#writing-a-plugin)
|
||||
- [Writing a Caddy module](#writing-a-caddy-module)
|
||||
- [Asking or answering questions for help using Caddy](#getting-help-using-caddy)
|
||||
- [Reporting a bug](#reporting-bugs)
|
||||
- [Suggesting an enhancement or a new feature](#suggesting-features)
|
||||
@@ -17,21 +17,21 @@ For starters, we invite you to join [the Caddy forum](https://caddy.community) w
|
||||
Other menu items:
|
||||
|
||||
- [Values](#values)
|
||||
- [Responsible Disclosure](#responsible-disclosure)
|
||||
- [Coordinated Disclosure](#coordinated-disclosure)
|
||||
- [Thank You](#thank-you)
|
||||
|
||||
|
||||
### Contributing code
|
||||
|
||||
You can have a direct impact on the project by helping with its code. To contribute code to Caddy, 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/-/search).
|
||||
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, 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/-/search).
|
||||
|
||||
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. :wink: 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 mergable.
|
||||
|
||||
Here are some of the expectations we have of contributors:
|
||||
|
||||
- If your change is more than just a minor alteration, **open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that changes are in-line with the project's goals and the best interests of its users. If there's already an issue about it, comment on the existing issue to claim it.
|
||||
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it.
|
||||
|
||||
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we DON'T do.](https://twitter.com/iamdevloper/status/397664295875805184)
|
||||
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -54,9 +54,9 @@ Contributing to Go projects on GitHub is fun and easy. We recommend the followin
|
||||
|
||||
1. [Fork this repo](https://github.com/caddyserver/caddy). This makes a copy of the code you can write to.
|
||||
|
||||
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, get it with `go get github.com/caddyserver/caddy/caddy`.
|
||||
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, get it with `go get github.com/caddyserver/caddy/v2`.
|
||||
|
||||
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/caddy.git`
|
||||
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/<your-username>/caddy.git`
|
||||
|
||||
4. Make your changes in the caddyserver/caddy.git repo on your computer.
|
||||
|
||||
@@ -67,11 +67,11 @@ Contributing to Go projects on GitHub is fun and easy. We recommend the followin
|
||||
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
|
||||
|
||||
|
||||
### Writing a plugin
|
||||
### Writing a Caddy module
|
||||
|
||||
Caddy can do more with plugins! Anyone can write a plugin. Plugins are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, change how the Caddyfile is loaded, and even implement new server types (e.g. HTTP, DNS). When it's ready, you can submit your plugin to the Caddy website so others can download it.
|
||||
Caddy can do more with modules! Anyone can write one. Caddy modules are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, add new configuration adapters, and even implement new server types (e.g. HTTP, DNS).
|
||||
|
||||
[Learn how to write and submit a plugin](https://github.com/caddyserver/caddy/wiki) on the wiki. You should also share and discuss your plugin idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for plugins.
|
||||
[Learn how to write a module here](https://caddyserver.com/docs/extending-caddy). You should also share and discuss your module idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for third-party modules.
|
||||
|
||||
|
||||
### Getting help using Caddy
|
||||
@@ -83,7 +83,7 @@ Many people on the forums could benefit from your experience and expertise, too.
|
||||
|
||||
### Reporting bugs
|
||||
|
||||
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy, not plugins.)
|
||||
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
|
||||
|
||||
**You can help stop bugs in their tracks!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
|
||||
|
||||
@@ -93,19 +93,44 @@ We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.o
|
||||
|
||||
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else!
|
||||
|
||||
#### Bug reporting expectations
|
||||
|
||||
Maintainers---or more generally, developers---need three things to act on bugs:
|
||||
|
||||
1. To agree or be convinced that it's a bug (reporter's responsibility).
|
||||
- A bug is undesired or surprising behavior which violates documentation or the spec.
|
||||
|
||||
2. To be able to understand what is happening (mostly reporter's responsibility).
|
||||
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix.
|
||||
- Otherwise, the burden is on the reporter to test possible solutions. This is discouraged because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
|
||||
|
||||
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
|
||||
- Sometimes the best solution is a documentation change.
|
||||
- Usually the developers have the best domain knowledge for inventing a solution, but reporters may have ideas or preferences for how they would like the software to work.
|
||||
- Security, correctness, and project goals/vision all take priority over a user's preferences.
|
||||
- It's simply good business to yield a solution that satisfies the users, and it's even better business to leave them impressed.
|
||||
|
||||
Thus, at the very least, the reporter is expected to:
|
||||
|
||||
1. Convince the reader that it's a bug (if it's not obvious).
|
||||
2. Reduce the problem down to the minimum specific steps required to reproduce it.
|
||||
|
||||
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
|
||||
|
||||
|
||||
|
||||
### Suggesting features
|
||||
|
||||
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. You don't have to follow the bug template for feature requests. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and without clarification will have to be closed.
|
||||
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
|
||||
|
||||
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good plugins](https://github.com/caddyserver/caddy/wiki), though, which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
|
||||
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
|
||||
|
||||
|
||||
### Improving documentation
|
||||
|
||||
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs). If you would like to make a fix to the docs, please submit an issue here describing the change to make.
|
||||
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs) and its source is in the [website repo](https://github.com/caddyserver/website). If you would like to make a fix to the docs, please submit an issue there describing the change to make.
|
||||
|
||||
Note that plugin documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual plugin authors, and you will have to contact them to change their documentation.
|
||||
Note that third-party module documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual module authors, and you will have to contact them to change their documentation.
|
||||
|
||||
|
||||
|
||||
@@ -128,9 +153,9 @@ Collaborators have push rights to the repository. We grant this permission after
|
||||
|
||||
- **Prefer squashed commits over a messy merge.** If there are many little commits, please [squash the commits](https://stackoverflow.com/a/11732910/1048862) so we don't clutter the commit history.
|
||||
|
||||
- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. For those that are, Caddy vendors all dependencies with the help of [gvt](https://github.com/FiloSottile/gvt). All external dependencies must be vendored, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
|
||||
- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. For those that are, Caddy uses [go modules](https://github.com/golang/go/wiki/Modules). All external dependencies must be installed as modules, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
|
||||
|
||||
- **Be extra careful in some areas of the code.** There are some critical areas in the Caddy code base that we review extra meticulously: the `caddy` and `caddytls` packages especially.
|
||||
- **Be extra careful in some areas of the code.** There are some critical areas in the Caddy code base that we review extra meticulously: the `caddyhttp` and `caddytls` packages especially.
|
||||
|
||||
- **Make sure tests test the actual thing.** Double-check that the tests fail without the change, and pass with it. It's important that they assert what they're purported to assert.
|
||||
|
||||
@@ -149,12 +174,11 @@ Collaborators have push rights to the repository. We grant this permission after
|
||||
- The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good.
|
||||
|
||||
|
||||
## Responsible Disclosure
|
||||
## Security Policy
|
||||
|
||||
If you've found a security vulnerability, please email me, the author, directly: Matthew dot Holt at Gmail. I'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch. If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give me the name to use. Thanks for responsibly helping Caddy—and thousands of websites—be more secure!
|
||||
If you think you've found a security vulnerability, please refer to our [Security Policy](https://github.com/caddyserver/caddy/security/policy) document.
|
||||
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! Caddy would not be what it is today without your
|
||||
contributions.
|
||||
Thanks for your help! Caddy would not be what it is today without your contributions.
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: For behaviors which violate documentation or cause incorrect results
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
This template is for bug reports. The lack of a feature is not a bug; to request a feature, please switch templates.
|
||||
|
||||
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
|
||||
|
||||
Please do not skip relevant questions; this will slow down the debugging process and your issue may be closed.
|
||||
-->
|
||||
|
||||
## 1. Which version of Caddy are you using (`caddy -version`)?
|
||||
<!-- If there is no version information, please paste commit SHA instead. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. What are you trying to do?
|
||||
<!-- Please clearly describe what you are trying to do thoroughly enough so that a reader with no background information can repeat it. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 3. What is your Caddyfile?
|
||||
```text
|
||||
paste entire Caddyfile here - DO NOT REDACT ANYTHING (except credentials)
|
||||
```
|
||||
<!-- Changing or hiding parts of your Caddyfile only slows things down and may result in your report being closed.
|
||||
For more information, see https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
|
||||
<!-- If you are unable to post this publicly, we offer private support: https://caddyserver.com/products/support -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 4. How did you run Caddy (give the full command and describe the execution environment)?
|
||||
<!-- IMPORTANT: Please eliminate Docker, systemd, reverse proxies, upstream dependencies, caches, firewalls, and other unnecessary, external factors from your setup first. This will help prove that this is a bug in Caddy and not a misconfiguration of your environment. We may close issues that are too complex to replicate. Thank you! -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 5. Please paste any relevant HTTP request(s) here.
|
||||
<!-- Paste curl command, or full HTTP request including headers and body. You may skip this if the bug does not require HTTP requests. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 6. What did you expect to see?
|
||||
<!-- Describe your expected results as precisely as possible. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 7. What did you see instead (give full error messages and/or log)?
|
||||
<!-- Please run Caddy with the -log flag, and use the log and errors directives as needed. DO NOT REDACT INFORMATION except for credentials. See https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 8. Why is this a bug, and how do you think this should be fixed?
|
||||
<!-- Help us understand why it is a bug; it is not always obvious. You can help us get this resolved faster by thinking about the problem and describing possible solutions! -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 9. What are you doing to work around the problem in the meantime?
|
||||
<!-- This can help others who encounter the same problem, until we implement a fix. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 10. Please link to any related issues, pull requests, and/or discussion.
|
||||
<!-- This can add crucial context to your report. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## Bonus: What do you use Caddy for? Why did you choose Caddy?
|
||||
<!-- We'd like to know! -->
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
This template is for feature requests. If you are reporting a bug instead, please switch templates.
|
||||
|
||||
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
|
||||
-->
|
||||
|
||||
|
||||
## 1. What would you like to have changed?
|
||||
<!-- Fully describe the feature or enhancement you are requesting; examples can be helpful too -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. Why is this feature a useful, necessary, and/or important addition to this project?
|
||||
<!-- Please justify why this change adds value to the project, considering the added maintenance burden and complexity the change introduces -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 3. What alternatives are there, or what are you doing in the meantime to work around the lack of this feature?
|
||||
<!-- We want to get an idea of what is being done in practice, or how other projects support your feature -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 4. Please link to any relevant issues, pull requests, or other discussions.
|
||||
<!-- This adds crucial context to your feature request and can speed things up -->
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: Pull request
|
||||
about: Propose changes to the code
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for contributing to Caddy! Please fill this out to help us make the most of your pull request.
|
||||
|
||||
Was this change discussed in an issue first? That can help save time in case the change is not a good fit for the project. Not all pull requests get merged.
|
||||
|
||||
It is not uncommon for pull requests to go through several, iterative reviews. Please be patient with us! Every reviewer is a volunteer, and each has their own style.
|
||||
-->
|
||||
|
||||
## 1. What does this change do, exactly?
|
||||
<!-- Please be specific. Motivate the problem, and justify why this is the best solution. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. Please link to the relevant issues.
|
||||
<!-- This adds crucial context to your change. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 3. Which documentation changes (if any) need to be made because of this PR?
|
||||
<!-- Reviewers will often reference this first in order to know what to expect from the change. Please be specific enough so that they can paste your wording into the documentation directly. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 4. Checklist
|
||||
|
||||
- [ ] I have written tests and verified that they fail without my change
|
||||
- [ ] I have squashed any insignificant commits
|
||||
- [ ] This change has comments explaining package types, values, functions, and non-obvious lines of code
|
||||
- [ ] I am willing to help maintain this change if there are issues with it later
|
||||
@@ -0,0 +1,27 @@
|
||||
# Security Policy
|
||||
|
||||
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
||||
|
||||
Some security problems are more the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please report only vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing or BGP hijacks a vulnerability in the Caddy web server).
|
||||
|
||||
Please note that 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.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x | :white_check_mark: |
|
||||
| 1.x | :white_check_mark: (EOL 1 Oct 2020) |
|
||||
| < 1.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please email Matt Holt (the author) directly: matt [at] ardanlabs [dot com].
|
||||
|
||||
We'll need enough information to verify the bug and make a patch. 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, resources permitting. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. Thank you for understanding.
|
||||
|
||||
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
|
||||
|
||||
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use.
|
||||
|
||||
Thanks for responsibly helping Caddy—and thousands of websites—be more secure!
|
||||
@@ -0,0 +1,175 @@
|
||||
# Used as inspiration: https://github.com/mvdan/github-actions-golang
|
||||
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
go: [ '1.14', '1.15' ]
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||
SUCCESS: 0
|
||||
|
||||
- os: macos-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||
SUCCESS: 0
|
||||
|
||||
- os: windows-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
|
||||
SUCCESS: 'True'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# These tools would be useful if we later decide to reinvestigate
|
||||
# publishing test/coverage reports to some tool for easier consumption
|
||||
# - name: Install test and coverage analysis tools
|
||||
# run: |
|
||||
# go get github.com/axw/gocov/gocov
|
||||
# go get github.com/AlekSi/gocov-xml
|
||||
# go get -u github.com/jstemmer/go-junit-report
|
||||
# echo "::add-path::$(go env GOPATH)/bin"
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
# Calculate the short SHA1 hash of the git commit
|
||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||
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
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
# mkdir test-results
|
||||
|
||||
- name: Build Caddy
|
||||
working-directory: ./cmd/caddy
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
go build -trimpath -ldflags="-w -s" -v
|
||||
|
||||
- name: Publish Build Artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
||||
path: ${{ matrix.CADDY_BIN_PATH }}
|
||||
|
||||
# Commented bits below were useful to allow the job to continue
|
||||
# even if the tests fail, so we can publish the report separately
|
||||
# For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status
|
||||
- name: Run tests
|
||||
# id: step_test
|
||||
# continue-on-error: true
|
||||
run: |
|
||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
||||
# echo "::set-output name=status::$?"
|
||||
|
||||
# Relevant step if we reinvestigate publishing test/coverage reports
|
||||
# - name: Prepare coverage reports
|
||||
# run: |
|
||||
# mkdir coverage
|
||||
# gocov convert cover-profile.out > coverage/coverage.json
|
||||
# # Because Windows doesn't work with input redirection like *nix, but output redirection works.
|
||||
# (cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
|
||||
|
||||
# To return the correct result even though we set 'continue-on-error: true'
|
||||
# - name: Coerce correct build result
|
||||
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
|
||||
# run: |
|
||||
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
|
||||
# exit 1
|
||||
|
||||
s390x-test:
|
||||
name: test (s390x on IBM Z)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Checkout code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: Run Tests
|
||||
run: |
|
||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||
|
||||
# short sha is enough?
|
||||
short_sha=$(git rev-parse --short HEAD)
|
||||
|
||||
# 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"
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; CGO_ENABLED=0 /usr/local/go/bin/go test -v ./..."
|
||||
test_result=$?
|
||||
|
||||
# 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'"
|
||||
|
||||
echo "Test exit code: $test_result"
|
||||
exit $test_result
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
|
||||
|
||||
# From https://github.com/reviewdog/action-golangci-lint
|
||||
golangci-lint:
|
||||
name: runner / golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: reviewdog/action-golangci-lint@v1
|
||||
# uses: docker://reviewdog/action-golangci-lint:v1 # pre-build docker image
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
goreleaser-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: check
|
||||
env:
|
||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Cross-Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
cross-build-test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
||||
go: [ '1.14', '1.15' ]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
working-directory: ./cmd/caddy
|
||||
run: |
|
||||
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "::warning ::$GOOS Build Failed"
|
||||
exit 0
|
||||
fi
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Fuzzing
|
||||
|
||||
on:
|
||||
# Daily midnight fuzzing
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
fuzzing:
|
||||
name: Fuzzing
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
go: [ '1.14' ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
|
||||
# If we decide we need to prevent this from running on forks, we can use this line:
|
||||
# if: github.repository == 'caddyserver/caddy'
|
||||
run: |
|
||||
|
||||
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
|
||||
chmod a+x fuzzit
|
||||
mv fuzzit $(go env GOPATH)/bin
|
||||
echo "::add-path::$(go env GOPATH)/bin"
|
||||
|
||||
- name: Generate fuzzers & submit them to Fuzzit
|
||||
continue-on-error: true
|
||||
env:
|
||||
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
|
||||
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
|
||||
BUILD_SOURCEVERSION: ${{ github.sha }}
|
||||
run: |
|
||||
# debug
|
||||
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
|
||||
echo "Source version: $BUILD_SOURCEVERSION"
|
||||
|
||||
declare -A fuzzers_funcs=(\
|
||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
|
||||
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
|
||||
["./replacer_fuzz.go"]="FuzzReplacer" \
|
||||
)
|
||||
|
||||
declare -A fuzzers_targets=(\
|
||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
|
||||
["./listeners_fuzz.go"]="parse-network-address" \
|
||||
["./replacer_fuzz.go"]="replacer" \
|
||||
)
|
||||
|
||||
fuzz_type="fuzzing"
|
||||
|
||||
for f in $(find . -name \*_fuzz.go); do
|
||||
FUZZER_DIRECTORY=$(dirname "$f")
|
||||
|
||||
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
|
||||
|
||||
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
|
||||
|
||||
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
|
||||
|
||||
echo "Completed $f"
|
||||
done
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
go: [ '1.15' ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# So GoReleaser can generate the changelog properly
|
||||
- name: Unshallowify the repo clone
|
||||
run: git fetch --prune --unshallow
|
||||
|
||||
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
|
||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
||||
|
||||
# Parse semver
|
||||
TAG=${GITHUB_REF/refs\/tags\//}
|
||||
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
|
||||
TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"`
|
||||
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
|
||||
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
|
||||
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
|
||||
echo "::set-output name=tag_major::${TAG_MAJOR}"
|
||||
echo "::set-output name=tag_minor::${TAG_MINOR}"
|
||||
echo "::set-output name=tag_patch::${TAG_PATCH}"
|
||||
echo "::set-output name=tag_special::${TAG_SPECIAL}"
|
||||
|
||||
- name: Cache the build cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.vars.outputs.go_cache }}
|
||||
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go${{ matrix.go }}-release
|
||||
|
||||
# GoReleaser will take care of publishing those artifacts into the release
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||
|
||||
# Only publish on non-special tags (e.g. non-beta)
|
||||
- name: Publish .deb to Gemfury
|
||||
if: ${{ steps.vars.outputs.tag_special == '' }}
|
||||
env:
|
||||
GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }}
|
||||
run: |
|
||||
for filename in dist/*.deb; do
|
||||
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
|
||||
done
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Release Published
|
||||
|
||||
# Event payload: https://developer.github.com/webhooks/event-payloads/#release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release Published
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
||||
# See https://github.com/peter-evans/repository-dispatch
|
||||
- name: Trigger event on caddyserver/dist
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/dist
|
||||
event-type: release-tagged
|
||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||
|
||||
- name: Trigger event on caddyserver/caddy-docker
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: caddyserver/caddy-docker
|
||||
event-type: release-tagged
|
||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||
|
||||
+17
-16
@@ -1,22 +1,23 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
_gitignore/
|
||||
Vagrantfile
|
||||
.vagrant/
|
||||
/.idea
|
||||
|
||||
dist/builds/
|
||||
dist/release/
|
||||
|
||||
error.log
|
||||
access.log
|
||||
|
||||
/*.conf
|
||||
*.log
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
og_static/
|
||||
# artifacts from pprof tooling
|
||||
*.prof
|
||||
*.test
|
||||
|
||||
.vscode/
|
||||
# build artifacts
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
|
||||
*.bat
|
||||
# mac specific
|
||||
.DS_Store
|
||||
|
||||
# go modules
|
||||
vendor
|
||||
|
||||
# goreleaser artifacts
|
||||
dist
|
||||
caddy-build
|
||||
caddy-dist
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule
|
||||
ignoretests: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- prealloc
|
||||
- unconvert
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- ineffassign
|
||||
- misspell
|
||||
|
||||
run:
|
||||
# default concurrency is a available CPU number.
|
||||
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
||||
deadline: 5m
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
format: 'colored-line-number'
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# we aren't calling unknown URL
|
||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
||||
linters:
|
||||
- gosec
|
||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||
- text: "G203" # G203: Use of unescaped data in HTML templates
|
||||
linters:
|
||||
- gosec
|
||||
# we're shelling out to known commands, not relying on user-defined input.
|
||||
- text: "G204" # G204: Audit use of command execution
|
||||
linters:
|
||||
- gosec
|
||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
before:
|
||||
hooks:
|
||||
# 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
|
||||
# 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.
|
||||
- mkdir -p caddy-build
|
||||
- cp cmd/caddy/main.go caddy-build/main.go
|
||||
- cp ./go.mod caddy-build/go.mod
|
||||
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
|
||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
||||
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
||||
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
||||
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
- GO111MODULE=on
|
||||
main: main.go
|
||||
dir: ./caddy-build
|
||||
binary: caddy
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- s390x
|
||||
- ppc64le
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: ppc64le
|
||||
- goos: darwin
|
||||
goarch: s390x
|
||||
- goos: windows
|
||||
goarch: ppc64le
|
||||
- goos: windows
|
||||
goarch: s390x
|
||||
- goos: freebsd
|
||||
goarch: ppc64le
|
||||
- goos: freebsd
|
||||
goarch: s390x
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 5
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
|
||||
archives:
|
||||
- format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
replacements:
|
||||
darwin: mac
|
||||
checksum:
|
||||
algorithm: sha512
|
||||
|
||||
nfpms:
|
||||
- id: default
|
||||
package_name: caddy
|
||||
|
||||
vendor: Light Code Labs
|
||||
homepage: https://caddyserver.com
|
||||
maintainer: Matthew Holt <mholt@users.noreply.github.com>
|
||||
description: |
|
||||
Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
|
||||
license: Apache 2.0
|
||||
|
||||
formats:
|
||||
- deb
|
||||
# - rpm
|
||||
|
||||
bindir: /usr/bin
|
||||
files:
|
||||
./caddy-dist/init/caddy.service: /lib/systemd/system/caddy.service
|
||||
./caddy-dist/init/caddy-api.service: /lib/systemd/system/caddy-api.service
|
||||
./caddy-dist/welcome/index.html: /usr/share/caddy/index.html
|
||||
./caddy-dist/scripts/completions/bash-completion: /etc/bash_completion.d/caddy
|
||||
config_files:
|
||||
./caddy-dist/config/Caddyfile: /etc/caddy/Caddyfile
|
||||
|
||||
scripts:
|
||||
postinstall: ./caddy-dist/scripts/postinstall.sh
|
||||
preremove: ./caddy-dist/scripts/preremove.sh
|
||||
postremove: ./caddy-dist/scripts/postremove.sh
|
||||
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: caddyserver
|
||||
name: caddy
|
||||
draft: true
|
||||
prerelease: auto
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^chore:'
|
||||
- '^ci:'
|
||||
- '^docs?:'
|
||||
- '^readme:'
|
||||
- '^tests?:'
|
||||
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
|
||||
@@ -0,0 +1,10 @@
|
||||
# This is the official list of Caddy Authors for copyright purposes.
|
||||
# Authors may be either individual people or legal entities.
|
||||
#
|
||||
# Not all individual contributors are authors. For the full list of
|
||||
# contributors, refer to the project's page on GitHub or the repo's
|
||||
# commit history.
|
||||
|
||||
Matthew Holt <Matthew.Holt@gmail.com>
|
||||
Light Code Labs <sales@lightcodelabs.com>
|
||||
Ardan Labs <info@ardanlabs.com>
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -178,7 +179,7 @@
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
@@ -186,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -1,225 +1,151 @@
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
|
||||
</p>
|
||||
<h3 align="center">Every Site on HTTPS <!-- Serve Confidently --></h3>
|
||||
<p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</p>
|
||||
<h3 align="center">Every site on HTTPS</h3>
|
||||
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
||||
<p align="center">
|
||||
<a href="https://dev.azure.com/mholt-dev/Caddy/_build?definitionId=5"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/5/master.svg?label=cross-platform%20tests"></a>
|
||||
<a href="https://godoc.org/github.com/caddyserver/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
||||
<a href="https://goreportcard.com/report/caddyserver/caddy"><img src="https://goreportcard.com/badge/github.com/caddyserver/caddy"></a>
|
||||
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
|
||||
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
||||
<br>
|
||||
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
|
||||
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com/download">Download</a> ·
|
||||
<a href="https://caddyserver.com/docs">Documentation</a> ·
|
||||
<a href="https://caddy.community">Community</a>
|
||||
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
||||
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
||||
<a href="https://caddy.community">Get Help</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
|
||||
|
||||
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
|
||||
|
||||
<p align="center">
|
||||
<b>Thanks to our special sponsor:</b>
|
||||
<br><br>
|
||||
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
|
||||
</p>
|
||||
|
||||
## Menu
|
||||
### Menu
|
||||
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Running in Production](#running-in-production)
|
||||
- [Contributing](#contributing)
|
||||
- [Donors](#donors)
|
||||
- [About the Project](#about-the-project)
|
||||
|
||||
## Features
|
||||
|
||||
- **Easy configuration** with the Caddyfile
|
||||
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
|
||||
- **HTTP/2** by default
|
||||
- **Virtual hosting** so multiple sites just work
|
||||
- Experimental **QUIC support** for cutting-edge transmissions
|
||||
- TLS session ticket **key rotation** for more secure connections
|
||||
- **Extensible with plugins** because a convenient web server is a helpful one
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
|
||||
[See a more complete list of features built into Caddy.](https://caddyserver.com/features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
|
||||
|
||||
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
|
||||
|
||||
- [Build from source](#build-from-source)
|
||||
- [For development](#for-development)
|
||||
- [With version information and/or plugins](#with-version-information-andor-plugins)
|
||||
- [Quick start](#quick-start)
|
||||
- [Overview](#overview)
|
||||
- [Full documentation](#full-documentation)
|
||||
- [Getting help](#getting-help)
|
||||
- [About](#about)
|
||||
|
||||
<p align="center">
|
||||
<b>Powered by</b>
|
||||
<br>
|
||||
<a href="https://github.com/mholt/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"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Install
|
||||
## Features
|
||||
|
||||
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
|
||||
|
||||
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
|
||||
- **[Latest release](https://github.com/caddyserver/caddy/releases/latest)** for pre-built, vanilla binaries
|
||||
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
|
||||
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
|
||||
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
|
||||
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
|
||||
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
|
||||
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
|
||||
- **Automatic HTTPS** by default
|
||||
- [Let's Encrypt](https://letsencrypt.org) for public sites
|
||||
- Fully-managed local CA for internal names & IPs
|
||||
- Can coordinate with other Caddy instances in a cluster
|
||||
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
||||
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
|
||||
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
- Written in Go, a language with higher **memory safety guarantees** than other servers
|
||||
- Actually **fun to use**
|
||||
- So, so much more to discover
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
|
||||
## Build from source
|
||||
|
||||
**To build Caddy without plugins:**
|
||||
Requirements:
|
||||
|
||||
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
|
||||
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
|
||||
2. Run `go get github.com/caddyserver/caddy/caddy`
|
||||
- [Go 1.14 or newer](https://golang.org/dl/)
|
||||
|
||||
Caddy will be installed to your `$GOPATH/bin` folder.
|
||||
|
||||
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
|
||||
|
||||
**To build Caddy with plugins (and with version information):**
|
||||
|
||||
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
|
||||
|
||||
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
|
||||
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
|
||||
2. Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
// plug in plugins here, for example:
|
||||
// _ "import/path/here"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// optional: disable telemetry
|
||||
// caddymain.EnableTelemetry = false
|
||||
caddymain.Run()
|
||||
}
|
||||
```
|
||||
3. `go mod init caddy`
|
||||
4. Run `go get github.com/caddyserver/caddy`
|
||||
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
|
||||
|
||||
**To install Caddy's source code for development:**
|
||||
|
||||
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
|
||||
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
|
||||
2. Run `git clone https://github.com/caddyserver/caddy.git` in any folder (doesn't have to be in GOPATH).
|
||||
|
||||
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
|
||||
|
||||
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
To serve static files from the current working directory, run:
|
||||
|
||||
```
|
||||
caddy
|
||||
### For development
|
||||
|
||||
```bash
|
||||
$ git clone "https://github.com/caddyserver/caddy.git"
|
||||
$ cd caddy/cmd/caddy/
|
||||
$ go build
|
||||
```
|
||||
|
||||
Caddy's default port is 2015, so open your browser to [http://localhost:2015](http://localhost:2015).
|
||||
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
|
||||
|
||||
### Go from 0 to HTTPS in 5 seconds
|
||||
### With version information and/or plugins
|
||||
|
||||
If the `caddy` binary has permission to bind to low ports and your domain name's DNS records point to the machine you're on:
|
||||
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
||||
|
||||
```
|
||||
caddy -host example.com
|
||||
$ xcaddy build
|
||||
```
|
||||
|
||||
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
|
||||
...the following steps are automated:
|
||||
|
||||
### Customizing your site
|
||||
|
||||
To customize how your site is served, create a file named Caddyfile by your site and paste this into it:
|
||||
|
||||
```plain
|
||||
localhost
|
||||
|
||||
push
|
||||
browse
|
||||
websocket /echo cat
|
||||
ext .html
|
||||
log /var/log/access.log
|
||||
proxy /api 127.0.0.1:7005
|
||||
header /api Access-Control-Allow-Origin *
|
||||
```
|
||||
|
||||
When you run `caddy` in that directory, it will automatically find and use that Caddyfile.
|
||||
|
||||
This simple file enables server push (via Link headers), allows directory browsing (for folders without an index file), hosts a WebSocket echo server at /echo, serves clean URLs, logs requests to an access log, proxies all API requests to a backend on port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from the API.
|
||||
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
### Doing more with Caddy
|
||||
|
||||
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
|
||||
|
||||
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
|
||||
Caddy has a nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
1. Create a new folder: `mkdir caddy`
|
||||
2. Change into it: `cd caddy`
|
||||
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
|
||||
4. Initialize a Go module: `go mod init caddy`
|
||||
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag or commit.
|
||||
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
|
||||
7. Compile: `go build`
|
||||
|
||||
|
||||
## Running in Production
|
||||
|
||||
Caddy is production-ready if you find it to be a good fit for your site and workflow.
|
||||
|
||||
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
||||
|
||||
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/caddyserver/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
|
||||
|
||||
How you choose to run Caddy is up to you. Many users are satisfied with `nohup caddy &`. Others use `screen`. Users who need Caddy to come back up after reboots either do so in the script that caused the reboot, add a command to an init script, or configure a service with their OS.
|
||||
|
||||
If you have questions or concerns about Caddy' underlying crypto implementations, consult Go's [crypto packages](https://golang.org/pkg/crypto), starting with their documentation, then issues, then the code itself; as Caddy uses mainly those libraries.
|
||||
|
||||
|
||||
## Contributing
|
||||
## Quick start
|
||||
|
||||
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/)!
|
||||
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
|
||||
|
||||
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
|
||||
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
|
||||
|
||||
We use GitHub issues and pull requests only for discussing bug reports and the development of specific changes. We welcome all other topics on the [forum](https://caddy.community)!
|
||||
|
||||
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/caddy/issues/new) describing the change that should be made.
|
||||
|
||||
### Good First Issue
|
||||
|
||||
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
|
||||
|
||||
|
||||
## Donors
|
||||
|
||||
- [DigitalOcean](https://m.do.co/c/6d7bdafccf96) is hosting the Caddy project.
|
||||
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
|
||||
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
|
||||
|
||||
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://caddyserver.com/pricing)!**
|
||||
|
||||
|
||||
## About the Project
|
||||
## Overview
|
||||
|
||||
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from [spark](https://github.com/rif/spark), [nginx](https://github.com/nginx/nginx), lighttpd,
|
||||
[Websocketd](https://github.com/joewalnes/websocketd) and [Vagrant](https://www.vagrantup.com/), which provides a pleasant mixture of features from each of them.
|
||||
Caddy is most often used as an HTTPS server, but it is suitable for any long-running Go program. First and foremost, it is a platform to run Go applications. Caddy "apps" are just Go programs that are implemented as Caddy modules. Two apps -- `tls` and `http` -- ship standard with Caddy.
|
||||
|
||||
**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". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
|
||||
Caddy apps instantly benefit from [automated documentation](https://caddyserver.com/docs/json/), graceful on-line [config changes via API](https://caddyserver.com/docs/api), and unification with other Caddy apps.
|
||||
|
||||
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
Although [JSON](https://caddyserver.com/docs/json/) is Caddy's native config language, Caddy can accept input from [config adapters](https://caddyserver.com/docs/config-adapters) which can essentially convert any config format of your choice into JSON: Caddyfile, JSON 5, YAML, TOML, NGINX config, and more.
|
||||
|
||||
The primary way to configure Caddy is through [its API](https://caddyserver.com/docs/api), but if you prefer config files, the [command-line interface](https://caddyserver.com/docs/command-line) supports those too.
|
||||
|
||||
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
|
||||
|
||||
To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
|
||||
|
||||
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
|
||||
|
||||
|
||||
## Full documentation
|
||||
|
||||
Our website has complete documentation:
|
||||
|
||||
**https://caddyserver.com/docs/**
|
||||
|
||||
The docs are also open source. You can contribute to them here: https://github.com/caddyserver/website
|
||||
|
||||
|
||||
|
||||
## Getting help
|
||||
|
||||
- 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.
|
||||
|
||||
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
|
||||
|
||||
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
|
||||
|
||||
|
||||
|
||||
## About
|
||||
|
||||
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Light Code Labs, LLC.
|
||||
|
||||
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
||||
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
|
||||
|
||||
@@ -0,0 +1,865 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
|
||||
|
||||
// AdminConfig configures Caddy's API endpoint, which is used
|
||||
// to manage Caddy while it is running.
|
||||
type AdminConfig struct {
|
||||
// If true, the admin endpoint will be completely disabled.
|
||||
// Note that this makes any runtime changes to the config
|
||||
// impossible, since the interface to do so is through the
|
||||
// admin endpoint.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
|
||||
// The address to which the admin endpoint's listener should
|
||||
// bind itself. Can be any single network address that can be
|
||||
// parsed by Caddy. Default: localhost:2019
|
||||
Listen string `json:"listen,omitempty"`
|
||||
|
||||
// If true, CORS headers will be emitted, and requests to the
|
||||
// API will be rejected if their `Host` and `Origin` headers
|
||||
// do not match the expected value(s). Use `origins` to
|
||||
// customize which origins/hosts are allowed.If `origins` is
|
||||
// not set, the listen address is the only value allowed by
|
||||
// default.
|
||||
EnforceOrigin bool `json:"enforce_origin,omitempty"`
|
||||
|
||||
// The list of allowed origins/hosts for API requests. Only needed
|
||||
// if accessing the admin endpoint from a host different from the
|
||||
// socket's network interface or if `enforce_origin` is true. If not
|
||||
// set, the listener address will be the default value. If set but
|
||||
// empty, no origins will be allowed.
|
||||
Origins []string `json:"origins,omitempty"`
|
||||
|
||||
// Options related to configuration management.
|
||||
Config *ConfigSettings `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigSettings configures the, uh, configuration... and
|
||||
// management thereof.
|
||||
type ConfigSettings struct {
|
||||
// Whether to keep a copy of the active config on disk. Default is true.
|
||||
Persist *bool `json:"persist,omitempty"`
|
||||
}
|
||||
|
||||
// listenAddr extracts a singular listen address from ac.Listen,
|
||||
// returning the network and the address of the listener.
|
||||
func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
|
||||
input := admin.Listen
|
||||
if input == "" {
|
||||
input = DefaultAdminListen
|
||||
}
|
||||
listenAddr, err := ParseNetworkAddress(input)
|
||||
if err != nil {
|
||||
return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
|
||||
}
|
||||
if listenAddr.PortRangeSize() != 1 {
|
||||
return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
|
||||
}
|
||||
return listenAddr, nil
|
||||
}
|
||||
|
||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||
func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
|
||||
muxWrap := adminHandler{
|
||||
enforceOrigin: admin.EnforceOrigin,
|
||||
enforceHost: !addr.isWildcardInterface(),
|
||||
allowedOrigins: admin.allowedOrigins(addr),
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
|
||||
labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
|
||||
h = promhttp.InstrumentHandlerCounter(
|
||||
adminMetrics.requestCount.MustCurryWith(labels),
|
||||
h,
|
||||
)
|
||||
muxWrap.mux.Handle(pattern, h)
|
||||
}
|
||||
// addRoute just calls muxWrap.mux.Handle after
|
||||
// wrapping the handler with error handling
|
||||
addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
|
||||
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
labels := prometheus.Labels{
|
||||
"path": pattern,
|
||||
"handler": handlerLabel,
|
||||
"method": r.Method,
|
||||
}
|
||||
adminMetrics.requestErrors.With(labels).Inc()
|
||||
}
|
||||
muxWrap.handleError(w, r, err)
|
||||
})
|
||||
addRouteWithMetrics(pattern, handlerLabel, wrapper)
|
||||
}
|
||||
|
||||
const handlerLabel = "admin"
|
||||
|
||||
// register standard config control endpoints
|
||||
addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
|
||||
addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
|
||||
addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
|
||||
|
||||
// register debugging endpoints
|
||||
addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
|
||||
addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
|
||||
addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
|
||||
addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
|
||||
addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
|
||||
addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
|
||||
|
||||
// register third-party module endpoints
|
||||
for _, m := range GetModules("admin.api") {
|
||||
router := m.New().(AdminRouter)
|
||||
handlerLabel := m.ID.Name()
|
||||
for _, route := range router.Routes() {
|
||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
return muxWrap
|
||||
}
|
||||
|
||||
// allowedOrigins returns a list of origins that are allowed.
|
||||
// If admin.Origins is nil (null), the provided listen address
|
||||
// will be used as the default origin. If admin.Origins is
|
||||
// empty, no origins will be allowed, effectively bricking the
|
||||
// endpoint for non-unix-socket endpoints, but whatever.
|
||||
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
|
||||
uniqueOrigins := make(map[string]struct{})
|
||||
for _, o := range admin.Origins {
|
||||
uniqueOrigins[o] = struct{}{}
|
||||
}
|
||||
if admin.Origins == nil {
|
||||
if addr.isLoopback() {
|
||||
if addr.IsUnixNetwork() {
|
||||
// RFC 2616, Section 14.26:
|
||||
// "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
|
||||
// name for the service being requested, then the Host header field MUST
|
||||
// be given with an empty value."
|
||||
uniqueOrigins[""] = struct{}{}
|
||||
} else {
|
||||
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
||||
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
||||
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
|
||||
}
|
||||
}
|
||||
if !addr.IsUnixNetwork() {
|
||||
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
|
||||
}
|
||||
}
|
||||
allowed := make([]string, 0, len(uniqueOrigins))
|
||||
for origin := range uniqueOrigins {
|
||||
allowed = append(allowed, origin)
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
// replaceAdmin replaces the running admin server according
|
||||
// to the relevant configuration in cfg. If no configuration
|
||||
// for the admin endpoint exists in cfg, a default one is
|
||||
// used, so that there is always an admin server (unless it
|
||||
// is explicitly configured to be disabled).
|
||||
func replaceAdmin(cfg *Config) error {
|
||||
// always be sure to close down the old admin endpoint
|
||||
// as gracefully as possible, even if the new one is
|
||||
// disabled -- careful to use reference to the current
|
||||
// (old) admin endpoint since it will be different
|
||||
// when the function returns
|
||||
oldAdminServer := adminServer
|
||||
defer func() {
|
||||
// do the shutdown asynchronously so that any
|
||||
// current API request gets a response; this
|
||||
// goroutine may last a few seconds
|
||||
if oldAdminServer != nil {
|
||||
go func(oldAdminServer *http.Server) {
|
||||
err := stopAdminServer(oldAdminServer)
|
||||
if err != nil {
|
||||
Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
|
||||
}
|
||||
}(oldAdminServer)
|
||||
}
|
||||
}()
|
||||
|
||||
// always get a valid admin config
|
||||
adminConfig := DefaultAdminConfig
|
||||
if cfg != nil && cfg.Admin != nil {
|
||||
adminConfig = cfg.Admin
|
||||
}
|
||||
|
||||
// if new admin endpoint is to be disabled, we're done
|
||||
if adminConfig.Disabled {
|
||||
Log().Named("admin").Warn("admin endpoint disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract a singular listener address
|
||||
addr, err := adminConfig.listenAddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := adminConfig.newAdminHandler(addr)
|
||||
|
||||
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminServer = &http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 1024 * 64,
|
||||
}
|
||||
|
||||
adminLogger := Log().Named("admin")
|
||||
go func() {
|
||||
if err := adminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
adminLogger.Info("admin endpoint started",
|
||||
zap.String("address", addr.String()),
|
||||
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
||||
zap.Strings("origins", handler.allowedOrigins))
|
||||
|
||||
if !handler.enforceHost {
|
||||
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
|
||||
zap.String("address", addr.String()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopAdminServer(srv *http.Server) error {
|
||||
if srv == nil {
|
||||
return fmt.Errorf("no admin server")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := srv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down admin server: %v", err)
|
||||
}
|
||||
Log().Named("admin").Info("stopped previous server")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminRouter is a type which can return routes for the admin API.
|
||||
type AdminRouter interface {
|
||||
Routes() []AdminRoute
|
||||
}
|
||||
|
||||
// AdminRoute represents a route for the admin endpoint.
|
||||
type AdminRoute struct {
|
||||
Pattern string
|
||||
Handler AdminHandler
|
||||
}
|
||||
|
||||
type adminHandler struct {
|
||||
enforceOrigin bool
|
||||
enforceHost bool
|
||||
allowedOrigins []string
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// ServeHTTP is the external entry point for API requests.
|
||||
// It will only be called once per request.
|
||||
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Log().Named("admin.api").Info("received request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("host", r.Host),
|
||||
zap.String("uri", r.RequestURI),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Reflect("headers", r.Header),
|
||||
)
|
||||
h.serveHTTP(w, r)
|
||||
}
|
||||
|
||||
// serveHTTP is the internal entry point for API requests. It may
|
||||
// be called more than once per request, for example if a request
|
||||
// is rewritten (i.e. internal redirect).
|
||||
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
||||
// I've never been able demonstrate a vulnerability myself, but apparently
|
||||
// WebSocket connections originating from browsers aren't subject to CORS
|
||||
// restrictions, so we'll just be on the safe side
|
||||
h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
if h.enforceHost {
|
||||
// DNS rebinding mitigation
|
||||
err := h.checkHost(r)
|
||||
if err != nil {
|
||||
h.handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if h.enforceOrigin {
|
||||
// cross-site mitigation
|
||||
origin, err := h.checkOrigin(r)
|
||||
if err != nil {
|
||||
h.handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// TODO: authentication & authorization, if configured
|
||||
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == ErrInternalRedir {
|
||||
h.serveHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
apiErr, ok := err.(APIError)
|
||||
if !ok {
|
||||
apiErr = APIError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if apiErr.Code == 0 {
|
||||
apiErr.Code = http.StatusInternalServerError
|
||||
}
|
||||
if apiErr.Message == "" && apiErr.Err != nil {
|
||||
apiErr.Message = apiErr.Err.Error()
|
||||
}
|
||||
|
||||
Log().Named("admin.api").Error("request error",
|
||||
zap.Error(err),
|
||||
zap.Int("status_code", apiErr.Code),
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(apiErr.Code)
|
||||
json.NewEncoder(w).Encode(apiErr)
|
||||
}
|
||||
|
||||
// checkHost returns a handler that wraps next such that
|
||||
// it will only be called if the request's Host header matches
|
||||
// a trustworthy/expected value. This helps to mitigate DNS
|
||||
// rebinding attacks.
|
||||
func (h adminHandler) checkHost(r *http.Request) error {
|
||||
var allowed bool
|
||||
for _, allowedHost := range h.allowedOrigins {
|
||||
if r.Host == allowedHost {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("host not allowed: %s", r.Host),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkOrigin ensures that the Origin header, if
|
||||
// set, matches the intended target; prevents arbitrary
|
||||
// sites from issuing requests to our listener. It
|
||||
// returns the origin that was obtained from r.
|
||||
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
||||
origin := h.getOriginHost(r)
|
||||
if origin == "" {
|
||||
return origin, APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("missing required Origin header"),
|
||||
}
|
||||
}
|
||||
if !h.originAllowed(origin) {
|
||||
return origin, APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
||||
}
|
||||
}
|
||||
return origin, nil
|
||||
}
|
||||
|
||||
func (h adminHandler) getOriginHost(r *http.Request) string {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = r.Header.Get("Referer")
|
||||
}
|
||||
originURL, err := url.Parse(origin)
|
||||
if err == nil && originURL.Host != "" {
|
||||
origin = originURL.Host
|
||||
}
|
||||
return origin
|
||||
}
|
||||
|
||||
func (h adminHandler) originAllowed(origin string) bool {
|
||||
for _, allowedOrigin := range h.allowedOrigins {
|
||||
originCopy := origin
|
||||
if !strings.Contains(allowedOrigin, "://") {
|
||||
// no scheme specified, so allow both
|
||||
originCopy = strings.TrimPrefix(originCopy, "http://")
|
||||
originCopy = strings.TrimPrefix(originCopy, "https://")
|
||||
}
|
||||
if originCopy == allowedOrigin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := readConfig(r.URL.Path, w)
|
||||
if err != nil {
|
||||
return APIError{Code: http.StatusBadRequest, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete:
|
||||
|
||||
// DELETE does not use a body, but the others do
|
||||
var body []byte
|
||||
if r.Method != http.MethodDelete {
|
||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
|
||||
}
|
||||
}
|
||||
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer bufPool.Put(buf)
|
||||
|
||||
_, err := io.Copy(buf, r.Body)
|
||||
if err != nil {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("reading request body: %v", err),
|
||||
}
|
||||
}
|
||||
body = buf.Bytes()
|
||||
}
|
||||
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method %s not allowed", r.Method),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||
idPath := r.URL.Path
|
||||
|
||||
parts := strings.Split(idPath, "/")
|
||||
if len(parts) < 3 || parts[2] == "" {
|
||||
return fmt.Errorf("request path is missing object ID")
|
||||
}
|
||||
if parts[0] != "" || parts[1] != "id" {
|
||||
return fmt.Errorf("malformed object path")
|
||||
}
|
||||
id := parts[2]
|
||||
|
||||
// map the ID to the expanded path
|
||||
currentCfgMu.RLock()
|
||||
expanded, ok := rawCfgIndex[id]
|
||||
defer currentCfgMu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown object ID '%s'", id)
|
||||
}
|
||||
|
||||
// piece the full URL path back together
|
||||
parts = append([]string{expanded}, parts[3:]...)
|
||||
r.URL.Path = path.Join(parts...)
|
||||
|
||||
return ErrInternalRedir
|
||||
}
|
||||
|
||||
func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||
err := handleUnload(w, r)
|
||||
if err != nil {
|
||||
Log().Named("admin.api").Error("unload error", zap.Error(err))
|
||||
}
|
||||
if adminServer != nil {
|
||||
// use goroutine so that we can finish responding to API request
|
||||
go func() {
|
||||
err := stopAdminServer(adminServer)
|
||||
var exitCode int
|
||||
if err != nil {
|
||||
exitCode = ExitCodeFailedQuit
|
||||
Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
|
||||
}
|
||||
Log().Named("admin.api").Info("stopping now, bye!! 👋")
|
||||
os.Exit(exitCode)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUnload stops the current configuration that is running.
|
||||
// Note that doing this can also be accomplished with DELETE /config/
|
||||
// but we leave this function because handleStop uses it.
|
||||
func handleUnload(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodPost {
|
||||
return APIError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method not allowed"),
|
||||
}
|
||||
}
|
||||
Log().Named("admin.api").Info("unloading")
|
||||
if err := stopAndCleanup(); err != nil {
|
||||
Log().Named("admin.api").Error("error unloading", zap.Error(err))
|
||||
} else {
|
||||
Log().Named("admin.api").Info("unloading completed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsyncedConfigAccess traverses into the current config and performs
|
||||
// the operation at path according to method, using body and out as
|
||||
// needed. This is a low-level, unsynchronized function; most callers
|
||||
// will want to use changeConfig or readConfig instead. This requires a
|
||||
// read or write lock on currentCfgMu, depending on method (GET needs
|
||||
// only a read lock; all others need a write lock).
|
||||
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
||||
var err error
|
||||
var val interface{}
|
||||
|
||||
// if there is a request body, decode it into the
|
||||
// variable that will be set in the config according
|
||||
// to method and path
|
||||
if len(body) > 0 {
|
||||
err = json.Unmarshal(body, &val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding request body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(out)
|
||||
|
||||
cleanPath := strings.Trim(path, "/")
|
||||
if cleanPath == "" {
|
||||
return fmt.Errorf("no traversable path")
|
||||
}
|
||||
|
||||
parts := strings.Split(cleanPath, "/")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("path missing")
|
||||
}
|
||||
|
||||
// A path that ends with "..." implies:
|
||||
// 1) the part before it is an array
|
||||
// 2) the payload is an array
|
||||
// and means that the user wants to expand the elements
|
||||
// in the payload array and append each one into the
|
||||
// destination array, like so:
|
||||
// array = append(array, elems...)
|
||||
// This special case is handled below.
|
||||
ellipses := parts[len(parts)-1] == "..."
|
||||
if ellipses {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
|
||||
var ptr interface{} = rawCfg
|
||||
|
||||
traverseLoop:
|
||||
for i, part := range parts {
|
||||
switch v := ptr.(type) {
|
||||
case map[string]interface{}:
|
||||
// if the next part enters a slice, and the slice is our destination,
|
||||
// handle it specially (because appending to the slice copies the slice
|
||||
// header, which does not replace the original one like we want)
|
||||
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
|
||||
var idx int
|
||||
if method != http.MethodPost {
|
||||
idxStr := parts[len(parts)-1]
|
||||
idx, err = strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
||||
path, idxStr, err)
|
||||
}
|
||||
if idx < 0 || idx >= len(arr) {
|
||||
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
||||
}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
err = enc.Encode(arr[idx])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
if ellipses {
|
||||
valArray, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("final element is not an array")
|
||||
}
|
||||
v[part] = append(arr, valArray...)
|
||||
} else {
|
||||
v[part] = append(arr, val)
|
||||
}
|
||||
case http.MethodPut:
|
||||
// avoid creation of new slice and a second copy (see
|
||||
// https://github.com/golang/go/wiki/SliceTricks#insert)
|
||||
arr = append(arr, nil)
|
||||
copy(arr[idx+1:], arr[idx:])
|
||||
arr[idx] = val
|
||||
v[part] = arr
|
||||
case http.MethodPatch:
|
||||
arr[idx] = val
|
||||
case http.MethodDelete:
|
||||
v[part] = append(arr[:idx], arr[idx+1:]...)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
}
|
||||
break traverseLoop
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
err = enc.Encode(v[part])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
// if the part is an existing list, POST appends to
|
||||
// it, otherwise it just sets or creates the value
|
||||
if arr, ok := v[part].([]interface{}); ok {
|
||||
if ellipses {
|
||||
valArray, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("final element is not an array")
|
||||
}
|
||||
v[part] = append(arr, valArray...)
|
||||
} else {
|
||||
v[part] = append(arr, val)
|
||||
}
|
||||
} else {
|
||||
v[part] = val
|
||||
}
|
||||
case http.MethodPut:
|
||||
if _, ok := v[part]; ok {
|
||||
return fmt.Errorf("[%s] key already exists: %s", path, part)
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodPatch:
|
||||
if _, ok := v[part]; !ok {
|
||||
return fmt.Errorf("[%s] key does not exist: %s", path, part)
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodDelete:
|
||||
delete(v, part)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
}
|
||||
} else {
|
||||
// if we are "PUTting" a new resource, the key(s) in its path
|
||||
// might not exist yet; that's OK but we need to make them as
|
||||
// we go, while we still have a pointer from the level above
|
||||
if v[part] == nil && method == http.MethodPut {
|
||||
v[part] = make(map[string]interface{})
|
||||
}
|
||||
ptr = v[part]
|
||||
}
|
||||
|
||||
case []interface{}:
|
||||
partInt, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||
strings.Join(parts[:i+1], "/"), part, err)
|
||||
}
|
||||
if partInt < 0 || partInt >= len(v) {
|
||||
return fmt.Errorf("[/%s] array index out of bounds: %s",
|
||||
strings.Join(parts[:i+1], "/"), part)
|
||||
}
|
||||
ptr = v[partInt]
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveMetaFields removes meta fields like "@id" from a JSON message
|
||||
// by using a simple regular expression. (An alternate way to do this
|
||||
// would be to delete them from the raw, map[string]interface{}
|
||||
// representation as they are indexed, then iterate the index we made
|
||||
// and add them back after encoding as JSON, but this is simpler.)
|
||||
func RemoveMetaFields(rawJSON []byte) []byte {
|
||||
return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte {
|
||||
// matches with a comma on both sides (when "@id" property is
|
||||
// not the first or last in the object) need to keep exactly
|
||||
// one comma for correct JSON syntax
|
||||
comma := []byte{','}
|
||||
if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
|
||||
return comma
|
||||
}
|
||||
return []byte{}
|
||||
})
|
||||
}
|
||||
|
||||
// AdminHandler is like http.Handler except ServeHTTP may return an error.
|
||||
//
|
||||
// If any handler encounters an error, it should be returned for proper
|
||||
// handling.
|
||||
type AdminHandler interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request) error
|
||||
}
|
||||
|
||||
// AdminHandlerFunc is a convenience type like http.HandlerFunc.
|
||||
type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
// ServeHTTP implements the Handler interface.
|
||||
func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
return f(w, r)
|
||||
}
|
||||
|
||||
// APIError is a structured error that every API
|
||||
// handler should return for consistency in logging
|
||||
// and client responses. If Message is unset, then
|
||||
// Err.Error() will be serialized in its place.
|
||||
type APIError struct {
|
||||
Code int `json:"-"`
|
||||
Err error `json:"-"`
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultAdminListen is the address for the admin
|
||||
// listener, if none is specified at startup.
|
||||
DefaultAdminListen = "localhost:2019"
|
||||
|
||||
// ErrInternalRedir indicates an internal redirect
|
||||
// and is useful when admin API handlers rewrite
|
||||
// the request; in that case, authentication and
|
||||
// authorization needs to happen again for the
|
||||
// rewritten request.
|
||||
ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
|
||||
|
||||
// DefaultAdminConfig is the default configuration
|
||||
// for the administration endpoint.
|
||||
DefaultAdminConfig = &AdminConfig{
|
||||
Listen: DefaultAdminListen,
|
||||
}
|
||||
)
|
||||
|
||||
// PIDFile writes a pidfile to the file at filename. It
|
||||
// will get deleted before the process gracefully exits.
|
||||
func PIDFile(filename string) error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
err := ioutil.WriteFile(filename, pid, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pidfile = filename
|
||||
return nil
|
||||
}
|
||||
|
||||
// idRegexp is used to match ID fields and their associated values
|
||||
// in the config. It also matches adjacent commas so that syntax
|
||||
// can be preserved no matter where in the object the field appears.
|
||||
// It supports string and most numeric values.
|
||||
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
|
||||
|
||||
// pidfile is the name of the pidfile, if any.
|
||||
var pidfile string
|
||||
|
||||
const (
|
||||
rawConfigKey = "config"
|
||||
idKey = "@id"
|
||||
)
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
var adminServer *http.Server
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||
// each test is performed in sequence, so
|
||||
// each change builds on the previous ones;
|
||||
// the config is not reset between tests
|
||||
for i, tc := range []struct {
|
||||
method string
|
||||
path string // rawConfigKey will be prepended
|
||||
payload string
|
||||
expect string // JSON representation of what the whole config is expected to be after the request
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
method: "POST",
|
||||
path: "",
|
||||
payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
|
||||
expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/foo",
|
||||
payload: `"jet"`,
|
||||
expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/bar",
|
||||
payload: `{"aa": "bb", "qq": "zz"}`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/bar/qq",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/list",
|
||||
payload: `"e"`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
|
||||
},
|
||||
{
|
||||
method: "PUT",
|
||||
path: "/list/3",
|
||||
payload: `"d"`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/list/3",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
|
||||
},
|
||||
{
|
||||
method: "PATCH",
|
||||
path: "/list/3",
|
||||
payload: `"d"`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/list/...",
|
||||
payload: `["e", "f", "g"]`,
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
|
||||
},
|
||||
} {
|
||||
err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
|
||||
|
||||
if tc.shouldErr && err == nil {
|
||||
t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
|
||||
}
|
||||
if !tc.shouldErr && err != nil {
|
||||
t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
|
||||
}
|
||||
|
||||
// decode the expected config so we can do a convenient DeepEqual
|
||||
var expectedDecoded interface{}
|
||||
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
|
||||
}
|
||||
|
||||
// make sure the resulting config is as we expect it
|
||||
if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
|
||||
t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
|
||||
i, expectedDecoded, rawCfg[rawConfigKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cfg := []byte(`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"myserver": {
|
||||
"listen": ["tcp/localhost:8080-8084"],
|
||||
"read_timeout": "30s"
|
||||
},
|
||||
"yourserver": {
|
||||
"listen": ["127.0.0.1:5000"],
|
||||
"read_header_timeout": "15s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
Load(cfg, true)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// AssetsPath returns the path to the folder
|
||||
// where the application may store data. If
|
||||
// CADDYPATH env variable is set, that value
|
||||
// is used. Otherwise, the path is the result
|
||||
// of evaluating "$HOME/.caddy".
|
||||
func AssetsPath() string {
|
||||
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
|
||||
return caddyPath
|
||||
}
|
||||
return filepath.Join(userHomeDir(), ".caddy")
|
||||
}
|
||||
|
||||
// userHomeDir returns the user's home directory according to
|
||||
// environment variables.
|
||||
//
|
||||
// Credit: http://stackoverflow.com/a/7922977/1048862
|
||||
func userHomeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
return home
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAssetsPath(t *testing.T) {
|
||||
if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
|
||||
err := os.Setenv("CADDYPATH", "testpath")
|
||||
if err != nil {
|
||||
t.Error("Could not set CADDYPATH")
|
||||
}
|
||||
if actual, expected := AssetsPath(), "testpath"; actual != expected {
|
||||
t.Errorf("Expected path to be %v, got: %v", expected, actual)
|
||||
}
|
||||
err = os.Setenv("CADDYPATH", "")
|
||||
if err != nil {
|
||||
t.Error("Could not set CADDYPATH")
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
# Mutilated beyond recognition from the example at:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
|
||||
|
||||
trigger:
|
||||
- master
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
linux:
|
||||
imageName: ubuntu-16.04
|
||||
gorootDir: /usr/local
|
||||
mac:
|
||||
imageName: macos-10.13
|
||||
gorootDir: /usr/local
|
||||
windows:
|
||||
imageName: windows-2019
|
||||
gorootDir: C:\
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
variables:
|
||||
GOROOT: $(gorootDir)/go
|
||||
GOPATH: $(system.defaultWorkingDirectory)/gopath
|
||||
GOBIN: $(GOPATH)/bin
|
||||
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
|
||||
# TODO: modules should be the default in Go 1.13, so this won't be needed
|
||||
GO111MODULE: on
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
latestGo=$(curl "https://golang.org/VERSION?m=text")
|
||||
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
|
||||
echo "Latest Go version: $latestGo"
|
||||
displayName: "Get latest Go version"
|
||||
|
||||
- bash: |
|
||||
sudo rm -f $(which go)
|
||||
echo '##vso[task.prependpath]$(GOBIN)'
|
||||
echo '##vso[task.prependpath]$(GOROOT)/bin'
|
||||
mkdir -p '$(modulePath)'
|
||||
shopt -s extglob
|
||||
shopt -s dotglob
|
||||
mv !(gopath) '$(modulePath)'
|
||||
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
|
||||
|
||||
# Install Go (this varies by platform)
|
||||
|
||||
- bash: |
|
||||
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
|
||||
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
|
||||
condition: eq( variables['Agent.OS'], 'Linux' )
|
||||
displayName: Install Go on Linux
|
||||
|
||||
- bash: |
|
||||
wget "https://dl.google.com/go/$(LATEST_GO).darwin-amd64.tar.gz"
|
||||
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).darwin-amd64.tar.gz"
|
||||
condition: eq( variables['Agent.OS'], 'Darwin' )
|
||||
displayName: Install Go on macOS
|
||||
|
||||
- powershell: |
|
||||
Write-Host "Downloading Go... (please be patient, I am very slow)"
|
||||
(New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/$(LATEST_GO).windows-amd64.zip", "$(LATEST_GO).windows-amd64.zip")
|
||||
Write-Host "Extracting Go... (I'm slow too)"
|
||||
Expand-Archive "$(LATEST_GO).windows-amd64.zip" -DestinationPath "$(gorootDir)"
|
||||
condition: eq( variables['Agent.OS'], 'Windows_NT' )
|
||||
displayName: Install Go on Windows
|
||||
|
||||
# TODO: When this issue is fixed, replace with installer script:
|
||||
# https://github.com/golangci/golangci-lint/issues/472
|
||||
- script: go get -v github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
displayName: Install golangci-lint
|
||||
|
||||
- bash: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
displayName: Print Go version and environment
|
||||
|
||||
- script: |
|
||||
go get -v -t -d ./...
|
||||
golangci-lint run -E gofmt -E goimports -E misspell
|
||||
go test -race ./...
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Run tests
|
||||
@@ -1,612 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 caddymain
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
"github.com/google/uuid"
|
||||
"github.com/klauspost/cpuid"
|
||||
"github.com/mholt/certmagic"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
|
||||
// This is where other plugins get plugged in (imported)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.TrapSignals()
|
||||
|
||||
flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
|
||||
flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", certmagic.Default.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
|
||||
flag.BoolVar(&certmagic.Default.DisableHTTPChallenge, "disable-http-challenge", certmagic.Default.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
|
||||
flag.BoolVar(&certmagic.Default.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.Default.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
|
||||
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
|
||||
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
|
||||
flag.StringVar(&envFile, "envfile", "", "Path to file with environment variables to load in KEY=VALUE format")
|
||||
flag.BoolVar(&fromJSON, "json-to-caddyfile", false, "From JSON stdin to Caddyfile stdout")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&certmagic.Default.Email, "email", "", "Default ACME CA account email address")
|
||||
flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
flag.BoolVar(&logTimestamps, "log-timestamps", true, "Enable timestamps for the process log")
|
||||
flag.IntVar(&logRollMB, "log-roll-mb", 100, "Roll process log when it reaches this many megabytes (0 to disable rolling)")
|
||||
flag.BoolVar(&logRollCompress, "log-roll-compress", true, "Gzip-compress rolled process log files")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
|
||||
flag.StringVar(&serverType, "type", "http", "Type of server to run")
|
||||
flag.BoolVar(&toJSON, "caddyfile-to-json", false, "From Caddyfile stdin to JSON stdout")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&validate, "validate", false, "Parse the Caddyfile but do not start the server")
|
||||
|
||||
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
|
||||
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
|
||||
}
|
||||
|
||||
// Run is Caddy's main() function.
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
|
||||
module := getBuildModule()
|
||||
cleanModVersion := strings.TrimPrefix(module.Version, "v")
|
||||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = module.Version
|
||||
caddy.OnProcessExit = append(caddy.OnProcessExit, certmagic.CleanUpOwnLocks)
|
||||
certmagic.UserAgent = appName + "/" + cleanModVersion
|
||||
|
||||
if !logTimestamps {
|
||||
// Disable timestamps for logging
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
// Set up process log before anything bad happens
|
||||
switch logfile {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
case "stderr":
|
||||
log.SetOutput(os.Stderr)
|
||||
case "":
|
||||
log.SetOutput(ioutil.Discard)
|
||||
default:
|
||||
if logRollMB > 0 {
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: logRollMB,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
Compress: logRollCompress,
|
||||
})
|
||||
} else {
|
||||
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
// don't close file; log should be writeable for duration of process
|
||||
log.SetOutput(f)
|
||||
}
|
||||
}
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if err := LoadEnvFromFile(envFile); err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
if printEnv {
|
||||
for _, v := range os.Environ() {
|
||||
fmt.Println(v)
|
||||
}
|
||||
}
|
||||
|
||||
// initialize telemetry client
|
||||
if EnableTelemetry {
|
||||
err := initTelemetry()
|
||||
if err != nil {
|
||||
mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
|
||||
}
|
||||
} else if disabledMetrics != "" {
|
||||
mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
|
||||
}
|
||||
|
||||
// Check for one-time actions
|
||||
if revoke != "" {
|
||||
err := caddytls.Revoke(revoke)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
fmt.Printf("Revoked certificate for %s\n", revoke)
|
||||
os.Exit(0)
|
||||
}
|
||||
if version {
|
||||
if module.Sum != "" {
|
||||
// a build with a known version will also have a checksum
|
||||
fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
|
||||
} else {
|
||||
fmt.Println(module.Version)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if plugins {
|
||||
fmt.Println(caddy.DescribePlugins())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check if we just need to do a Caddyfile Convert and exit
|
||||
checkJSONCaddyfile()
|
||||
|
||||
// Set CPU cap
|
||||
err := setCPU(cpu)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// Executes Startup events
|
||||
caddy.EmitEvent(caddy.StartupEvent, nil)
|
||||
|
||||
// Get Caddyfile input
|
||||
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
if validate {
|
||||
err := caddy.ValidateAndExecuteDirectives(caddyfileinput, nil, true)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
msg := "Caddyfile is valid"
|
||||
fmt.Println(msg)
|
||||
log.Printf("[INFO] %s", msg)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Log Caddy version before start
|
||||
log.Printf("[INFO] Caddy version: %s", module.Version)
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(caddyfileinput)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// Begin telemetry (these are no-ops if telemetry disabled)
|
||||
telemetry.Set("caddy_version", module.Version)
|
||||
telemetry.Set("num_listeners", len(instance.Servers()))
|
||||
telemetry.Set("server_type", serverType)
|
||||
telemetry.Set("os", runtime.GOOS)
|
||||
telemetry.Set("arch", runtime.GOARCH)
|
||||
telemetry.Set("cpu", struct {
|
||||
BrandName string `json:"brand_name,omitempty"`
|
||||
NumLogical int `json:"num_logical,omitempty"`
|
||||
AESNI bool `json:"aes_ni,omitempty"`
|
||||
}{
|
||||
BrandName: cpuid.CPU.BrandName,
|
||||
NumLogical: runtime.NumCPU(),
|
||||
AESNI: cpuid.CPU.AesNi(),
|
||||
})
|
||||
if containerized := detectContainer(); containerized {
|
||||
telemetry.Set("container", containerized)
|
||||
}
|
||||
telemetry.StartEmitting()
|
||||
|
||||
// Twiddle your thumbs
|
||||
instance.Wait()
|
||||
}
|
||||
|
||||
// mustLogFatalf wraps log.Fatalf() in a way that ensures the
|
||||
// output is always printed to stderr so the user can see it
|
||||
// if the user is still there, even if the process log was not
|
||||
// enabled. If this process is an upgrade, however, and the user
|
||||
// might not be there anymore, this just logs to the process
|
||||
// log and exits.
|
||||
func mustLogFatalf(format string, args ...interface{}) {
|
||||
if !caddy.IsUpgrade() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
log.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// confLoader loads the Caddyfile using the -conf flag.
|
||||
func confLoader(serverType string) (caddy.Input, error) {
|
||||
if conf == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
|
||||
}
|
||||
|
||||
var contents []byte
|
||||
if strings.Contains(conf, "*") {
|
||||
// Let caddyfile.doImport logic handle the globbed path
|
||||
contents = []byte("import " + conf)
|
||||
} else {
|
||||
var err error
|
||||
contents, err = ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// defaultLoader loads the Caddyfile from the current working directory.
|
||||
func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBuildModule returns the build info of Caddy
|
||||
// from debug.BuildInfo (requires Go modules). If
|
||||
// no version information is available, a non-nil
|
||||
// value will still be returned, but with an
|
||||
// unknown version.
|
||||
func getBuildModule() *debug.Module {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
// The recommended way to build Caddy involves
|
||||
// creating a separate main module, which
|
||||
// preserves caddy a read-only dependency
|
||||
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
||||
for _, mod := range bi.Deps {
|
||||
if mod.Path == "github.com/caddyserver/caddy" {
|
||||
return mod
|
||||
}
|
||||
}
|
||||
}
|
||||
return &debug.Module{Version: "unknown"}
|
||||
}
|
||||
|
||||
func checkJSONCaddyfile() {
|
||||
if fromJSON {
|
||||
jsonBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
caddyfileBytes, err := caddyfile.FromJSON(jsonBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Converting from JSON failed: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Println(string(caddyfileBytes))
|
||||
os.Exit(0)
|
||||
}
|
||||
if toJSON {
|
||||
caddyfileBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
jsonBytes, err := caddyfile.ToJSON(caddyfileBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Converting to JSON failed: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
// If the percent resolves to less than a single
|
||||
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
|
||||
func setCPU(cpu string) error {
|
||||
var numCPU int
|
||||
|
||||
availCPU := runtime.NumCPU()
|
||||
|
||||
if strings.HasSuffix(cpu, "%") {
|
||||
// Percent
|
||||
var percent float32
|
||||
pctStr := cpu[:len(cpu)-1]
|
||||
pctInt, err := strconv.Atoi(pctStr)
|
||||
if err != nil || pctInt < 1 || pctInt > 100 {
|
||||
return errors.New("invalid CPU value: percentage must be between 1-100")
|
||||
}
|
||||
percent = float32(pctInt) / 100
|
||||
numCPU = int(float32(availCPU) * percent)
|
||||
if numCPU < 1 {
|
||||
numCPU = 1
|
||||
}
|
||||
} else {
|
||||
// Number
|
||||
num, err := strconv.Atoi(cpu)
|
||||
if err != nil || num < 1 {
|
||||
return errors.New("invalid CPU value: provide a number or percent greater than 0")
|
||||
}
|
||||
numCPU = num
|
||||
}
|
||||
|
||||
if numCPU > availCPU {
|
||||
numCPU = availCPU
|
||||
}
|
||||
|
||||
runtime.GOMAXPROCS(numCPU)
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectContainer attempts to determine whether the process is
|
||||
// being run inside a container. References:
|
||||
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
||||
// https://stackoverflow.com/a/20012536/1048862
|
||||
// https://gist.github.com/anantkamath/623ce7f5432680749e087cf8cfba9b69
|
||||
func detectContainer() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
file, err := os.Open("/proc/1/cgroup")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
i := 0
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
i++
|
||||
if i > 1000 {
|
||||
return false
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(parts[2], "docker") ||
|
||||
strings.Contains(parts[2], "lxc") ||
|
||||
strings.Contains(parts[2], "moby") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// initTelemetry initializes the telemetry engine.
|
||||
func initTelemetry() error {
|
||||
uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid")
|
||||
if customUUIDFile := os.Getenv("CADDY_UUID_FILE"); customUUIDFile != "" {
|
||||
uuidFilename = customUUIDFile
|
||||
}
|
||||
|
||||
newUUID := func() uuid.UUID {
|
||||
id := uuid.New()
|
||||
err := os.MkdirAll(caddy.AssetsPath(), 0700)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Persisting instance UUID: %v", err)
|
||||
return id
|
||||
}
|
||||
err = ioutil.WriteFile(uuidFilename, []byte(id.String()), 0600) // human-readable as a string
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Persisting instance UUID: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
var id uuid.UUID
|
||||
|
||||
// load UUID from storage, or create one if we don't have one
|
||||
if uuidFile, err := os.Open(uuidFilename); os.IsNotExist(err) {
|
||||
// no UUID exists yet; create a new one and persist it
|
||||
id = newUUID()
|
||||
} else if err != nil {
|
||||
log.Printf("[ERROR] Loading persistent UUID: %v", err)
|
||||
id = newUUID()
|
||||
} else {
|
||||
defer uuidFile.Close()
|
||||
uuidBytes, err := ioutil.ReadAll(uuidFile)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Reading persistent UUID: %v", err)
|
||||
id = newUUID()
|
||||
} else {
|
||||
id, err = uuid.ParseBytes(uuidBytes)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Parsing UUID: %v", err)
|
||||
id = newUUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse and check the list of disabled metrics
|
||||
var disabledMetricsSlice []string
|
||||
if len(disabledMetrics) > 0 {
|
||||
if len(disabledMetrics) > 1024 {
|
||||
// mitigate disk space exhaustion at the collection endpoint
|
||||
return fmt.Errorf("too many metrics to disable")
|
||||
}
|
||||
disabledMetricsSlice = splitTrim(disabledMetrics, ",")
|
||||
for _, metric := range disabledMetricsSlice {
|
||||
if metric == "instance_id" || metric == "timestamp" || metric == "disabled_metrics" {
|
||||
return fmt.Errorf("instance_id, timestamp, and disabled_metrics cannot be disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize telemetry
|
||||
telemetry.Init(id, disabledMetricsSlice)
|
||||
|
||||
// if any metrics were disabled, report which ones (so we know how representative the data is)
|
||||
if len(disabledMetricsSlice) > 0 {
|
||||
telemetry.Set("disabled_metrics", disabledMetricsSlice)
|
||||
log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Split string s into all substrings separated by sep and returns a slice of
|
||||
// the substrings between those separators.
|
||||
//
|
||||
// If s does not contain sep and sep is not empty, Split returns a
|
||||
// slice of length 1 whose only element is s.
|
||||
//
|
||||
// If sep is empty, Split splits after each UTF-8 sequence. If both s
|
||||
// and sep are empty, Split returns an empty slice.
|
||||
//
|
||||
// Each item that in result is trim space and not empty string
|
||||
func splitTrim(s string, sep string) []string {
|
||||
splitItems := strings.Split(s, sep)
|
||||
trimItems := make([]string, 0, len(splitItems))
|
||||
for _, item := range splitItems {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
trimItems = append(trimItems, item)
|
||||
}
|
||||
}
|
||||
return trimItems
|
||||
}
|
||||
|
||||
// LoadEnvFromFile loads additional envs if file provided and exists
|
||||
// Envs in file should be in KEY=VALUE format
|
||||
func LoadEnvFromFile(envFile string) error {
|
||||
if envFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(envFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
envMap, err := ParseEnvFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range envMap {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseEnvFile implements parse logic for environment files
|
||||
func ParseEnvFile(envInput io.Reader) (map[string]string, error) {
|
||||
envMap := make(map[string]string)
|
||||
|
||||
scanner := bufio.NewScanner(envInput)
|
||||
var line string
|
||||
lineNumber := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
lineNumber++
|
||||
|
||||
// skip lines starting with comment
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip empty line
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("Can't parse line %d; line should be in KEY=VALUE format", lineNumber)
|
||||
}
|
||||
|
||||
if strings.Contains(fields[0], " ") {
|
||||
return nil, fmt.Errorf("Can't parse line %d; KEY contains whitespace", lineNumber)
|
||||
}
|
||||
|
||||
key := fields[0]
|
||||
val := fields[1]
|
||||
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("Can't parse line %d; KEY can't be empty string", lineNumber)
|
||||
}
|
||||
envMap[key] = val
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return envMap, nil
|
||||
}
|
||||
|
||||
const appName = "Caddy"
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
envFile string
|
||||
fromJSON bool
|
||||
logfile string
|
||||
logTimestamps bool
|
||||
logRollMB int
|
||||
logRollCompress bool
|
||||
revoke string
|
||||
toJSON bool
|
||||
version bool
|
||||
plugins bool
|
||||
printEnv bool
|
||||
validate bool
|
||||
disabledMetrics string
|
||||
)
|
||||
|
||||
// EnableTelemetry defines whether telemetry is enabled in Run.
|
||||
var EnableTelemetry = true
|
||||
@@ -1,118 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 caddymain
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetCPU(t *testing.T) {
|
||||
currentCPU := runtime.GOMAXPROCS(-1)
|
||||
maxCPU := runtime.NumCPU()
|
||||
halfCPU := int(0.5 * float32(maxCPU))
|
||||
if halfCPU < 1 {
|
||||
halfCPU = 1
|
||||
}
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
output int
|
||||
shouldErr bool
|
||||
}{
|
||||
{"1", 1, false},
|
||||
{"-1", currentCPU, true},
|
||||
{"0", currentCPU, true},
|
||||
{"100%", maxCPU, false},
|
||||
{"50%", halfCPU, false},
|
||||
{"110%", currentCPU, true},
|
||||
{"-10%", currentCPU, true},
|
||||
{"invalid input", currentCPU, true},
|
||||
{"invalid input%", currentCPU, true},
|
||||
{"9999", maxCPU, false}, // over available CPU
|
||||
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
|
||||
} {
|
||||
err := setCPU(test.input)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
|
||||
}
|
||||
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
|
||||
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
// teardown
|
||||
runtime.GOMAXPROCS(currentCPU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitTrim(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
output []string
|
||||
sep string
|
||||
}{
|
||||
{"os,arch,cpu,caddy_version", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os,arch,cpu,caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os,,, arch, cpu, caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{", , os, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os, ,, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
} {
|
||||
got := splitTrim(test.input, test.sep)
|
||||
if len(got) != len(test.output) {
|
||||
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
|
||||
continue
|
||||
}
|
||||
for j, item := range test.output {
|
||||
if item != got[j] {
|
||||
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{"parsing KEY=VALUE", "PORT=4096", map[string]string{"PORT": "4096"}, false},
|
||||
{"empty KEY", "=4096", nil, true},
|
||||
{"one value", "test", nil, true},
|
||||
{"comments skipped", "#TEST=1\nPORT=8888", map[string]string{"PORT": "8888"}, false},
|
||||
{"empty line", "\nPORT=7777", map[string]string{"PORT": "7777"}, false},
|
||||
{"comments with space skipped", " #TEST=1", map[string]string{}, false},
|
||||
{"KEY with space", "PORT =8888", nil, true},
|
||||
{"only spaces", " ", map[string]string{}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.input)
|
||||
got, err := ParseEnvFile(reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseEnvFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseEnvFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+49
-187
@@ -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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,198 +15,60 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
// TODO
|
||||
func TestCaddyStartStop(t *testing.T) {
|
||||
caddyfile := "localhost:1984"
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
_, err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting, iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(2 * time.Second),
|
||||
}
|
||||
resp, err := client.Get("http://localhost:1984")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Error stopping, iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// CallbackTestContext implements Context interface
|
||||
type CallbackTestContext struct {
|
||||
// If MakeServersFail is set to true then MakeServers returns an error
|
||||
MakeServersFail bool
|
||||
}
|
||||
|
||||
func (h *CallbackTestContext) InspectServerBlocks(name string, sblock []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
return sblock, nil
|
||||
}
|
||||
func (h *CallbackTestContext) MakeServers() ([]Server, error) {
|
||||
if h.MakeServersFail {
|
||||
return make([]Server, 0), fmt.Errorf("MakeServers failed")
|
||||
}
|
||||
return make([]Server, 0), nil
|
||||
}
|
||||
|
||||
func TestCaddyRestartCallbacks(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
restartFail bool
|
||||
expectedCalls []string
|
||||
}{
|
||||
{false, []string{"OnRestart", "OnShutdown"}},
|
||||
{true, []string{"OnRestart", "OnRestartFailed"}},
|
||||
} {
|
||||
serverName := fmt.Sprintf("%v", i)
|
||||
// RegisterServerType to make successful restart possible
|
||||
RegisterServerType(serverName, ServerType{
|
||||
Directives: func() []string { return []string{} },
|
||||
// If MakeServersFail is true then the restart will fail due to context failure
|
||||
NewContext: func(inst *Instance) Context { return &CallbackTestContext{MakeServersFail: test.restartFail} },
|
||||
})
|
||||
c := NewTestController(serverName, "")
|
||||
c.instance = &Instance{
|
||||
serverType: serverName,
|
||||
wg: new(sync.WaitGroup),
|
||||
}
|
||||
|
||||
// Register callbacks which save the calls order
|
||||
calls := make([]string, 0)
|
||||
c.OnRestart(func() error {
|
||||
calls = append(calls, "OnRestart")
|
||||
return nil
|
||||
})
|
||||
c.OnRestartFailed(func() error {
|
||||
calls = append(calls, "OnRestartFailed")
|
||||
return nil
|
||||
})
|
||||
c.OnShutdown(func() error {
|
||||
calls = append(calls, "OnShutdown")
|
||||
return nil
|
||||
})
|
||||
|
||||
_, err := c.instance.Restart(CaddyfileInput{Contents: []byte(""), ServerTypeName: serverName})
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Restart failed: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(calls, test.expectedCalls) {
|
||||
t.Errorf("Test %d: Callbacks expected: %v, got: %v", i, test.expectedCalls, calls)
|
||||
}
|
||||
|
||||
err = c.instance.Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Stop failed: %v", err)
|
||||
}
|
||||
|
||||
c.instance.Wait()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsLoopback(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
func TestParseDuration(t *testing.T) {
|
||||
const day = 24 * time.Hour
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expect bool
|
||||
expect time.Duration
|
||||
}{
|
||||
{"example.com", false},
|
||||
{"localhost", true},
|
||||
{"localhost:1234", true},
|
||||
{"localhost:", true},
|
||||
{"127.0.0.1", true},
|
||||
{"127.0.0.1:443", true},
|
||||
{"127.0.1.5", true},
|
||||
{"10.0.0.5", false},
|
||||
{"12.7.0.1", false},
|
||||
{"[::1]", true},
|
||||
{"[::1]:1234", true},
|
||||
{"::1", true},
|
||||
{"::", false},
|
||||
{"[::]", false},
|
||||
{"local", false},
|
||||
{
|
||||
input: "3h",
|
||||
expect: 3 * time.Hour,
|
||||
},
|
||||
{
|
||||
input: "1d",
|
||||
expect: day,
|
||||
},
|
||||
{
|
||||
input: "1d30m",
|
||||
expect: day + 30*time.Minute,
|
||||
},
|
||||
{
|
||||
input: "1m2d",
|
||||
expect: time.Minute + day*2,
|
||||
},
|
||||
{
|
||||
input: "1m2d30s",
|
||||
expect: time.Minute + day*2 + 30*time.Second,
|
||||
},
|
||||
{
|
||||
input: "1d2d",
|
||||
expect: 3 * day,
|
||||
},
|
||||
{
|
||||
input: "1.5d",
|
||||
expect: time.Duration(1.5 * float64(day)),
|
||||
},
|
||||
{
|
||||
input: "4m1.25d",
|
||||
expect: 4*time.Minute + time.Duration(1.25*float64(day)),
|
||||
},
|
||||
{
|
||||
input: "-1.25d12h",
|
||||
expect: time.Duration(-1.25*float64(day)) - 12*time.Hour,
|
||||
},
|
||||
} {
|
||||
if got, want := IsLoopback(test.input), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInternal(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{"9.255.255.255", false},
|
||||
{"10.0.0.0", true},
|
||||
{"10.0.0.1", true},
|
||||
{"10.255.255.254", true},
|
||||
{"10.255.255.255", true},
|
||||
{"11.0.0.0", false},
|
||||
{"10.0.0.5:1234", true},
|
||||
{"11.0.0.5:1234", false},
|
||||
|
||||
{"172.15.255.255", false},
|
||||
{"172.16.0.0", true},
|
||||
{"172.16.0.1", true},
|
||||
{"172.31.255.254", true},
|
||||
{"172.31.255.255", true},
|
||||
{"172.32.0.0", false},
|
||||
{"172.16.0.1:1234", true},
|
||||
|
||||
{"192.167.255.255", false},
|
||||
{"192.168.0.0", true},
|
||||
{"192.168.0.1", true},
|
||||
{"192.168.255.254", true},
|
||||
{"192.168.255.255", true},
|
||||
{"192.169.0.0", false},
|
||||
{"192.168.0.1:1234", true},
|
||||
|
||||
{"fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false},
|
||||
{"fc00::", true},
|
||||
{"fc00::1", true},
|
||||
{"[fc00::1]", true},
|
||||
{"[fc00::1]:8888", true},
|
||||
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", true},
|
||||
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
|
||||
{"fe00::", false},
|
||||
{"fd12:3456:789a:1::1:1234", true},
|
||||
|
||||
{"example.com", false},
|
||||
{"localhost", false},
|
||||
{"localhost:1234", false},
|
||||
{"localhost:", false},
|
||||
{"127.0.0.1", false},
|
||||
{"127.0.0.1:443", false},
|
||||
{"127.0.1.5", false},
|
||||
{"12.7.0.1", false},
|
||||
{"[::1]", false},
|
||||
{"[::1]:1234", false},
|
||||
{"::1", false},
|
||||
{"::", false},
|
||||
{"[::]", false},
|
||||
{"local", false},
|
||||
} {
|
||||
if got, want := IsInternal(test.input), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
|
||||
actual, err := ParseDuration(tc.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d ('%s'): Got error: %v", i, tc.input, err)
|
||||
continue
|
||||
}
|
||||
if actual != tc.expect {
|
||||
t.Errorf("Test %d ('%s'): Expected=%s Actual=%s", i, tc.input, tc.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
)
|
||||
|
||||
// Adapter adapts Caddyfile to Caddy JSON.
|
||||
type Adapter struct {
|
||||
ServerType ServerType
|
||||
}
|
||||
|
||||
// Adapt converts the Caddyfile config in body to Caddy JSON.
|
||||
func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
|
||||
if a.ServerType == nil {
|
||||
return nil, nil, fmt.Errorf("no server type")
|
||||
}
|
||||
if options == nil {
|
||||
options = make(map[string]interface{})
|
||||
}
|
||||
|
||||
filename, _ := options["filename"].(string)
|
||||
if filename == "" {
|
||||
filename = "Caddyfile"
|
||||
}
|
||||
|
||||
serverBlocks, err := Parse(filename, body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cfg, warnings, err := a.ServerType.Setup(serverBlocks, options)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
marshalFunc := json.Marshal
|
||||
if options["pretty"] == "true" {
|
||||
marshalFunc = caddyconfig.JSONIndent
|
||||
}
|
||||
result, err := marshalFunc(cfg)
|
||||
|
||||
return result, warnings, err
|
||||
}
|
||||
|
||||
// Unmarshaler is a type that can unmarshal
|
||||
// Caddyfile tokens to set itself up for a
|
||||
// JSON encoding. The goal of an unmarshaler
|
||||
// is not to set itself up for actual use,
|
||||
// but to set itself up for being marshaled
|
||||
// into JSON. Caddyfile-unmarshaled values
|
||||
// will not be used directly; they will be
|
||||
// encoded as JSON and then used from that.
|
||||
// Implementations must be able to support
|
||||
// multiple segments (instances of their
|
||||
// directive or batch of tokens); typically
|
||||
// this means wrapping all token logic in
|
||||
// a loop: `for d.Next() { ... }`.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalCaddyfile(d *Dispenser) error
|
||||
}
|
||||
|
||||
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
|
||||
type ServerType interface {
|
||||
// Setup takes the server blocks which
|
||||
// contain tokens, as well as options
|
||||
// (e.g. CLI flags) and creates a Caddy
|
||||
// config, along with any warnings or
|
||||
// an error.
|
||||
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ caddyconfig.Adapter = (*Adapter)(nil)
|
||||
Executable
+396
@@ -0,0 +1,396 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure. An empty
|
||||
// Dispenser is invalid; call NewDispenser to make a proper instance.
|
||||
type Dispenser struct {
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser filled with the given tokens.
|
||||
func NewDispenser(tokens []Token) *Dispenser {
|
||||
return &Dispenser{
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestDispenser parses input into tokens and creates a new
|
||||
// Dispenser for test purposes only; any errors are fatal.
|
||||
func NewTestDispenser(input string) *Dispenser {
|
||||
tokens, err := allTokens("Testfile", []byte(input))
|
||||
if err != nil && err != io.EOF {
|
||||
log.Fatalf("getting all tokens from input: %v", err)
|
||||
}
|
||||
return NewDispenser(tokens)
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Prev moves to the previous token. It does the inverse
|
||||
// of Next(), except this function may decrement the cursor
|
||||
// to -1 so that the next call to Next() points to the
|
||||
// first token; this allows dispensing to "start over". This
|
||||
// method returns true if the cursor ends up pointing to a
|
||||
// valid token.
|
||||
func (d *Dispenser) Prev() bool {
|
||||
if d.cursor > -1 {
|
||||
d.cursor--
|
||||
return d.cursor > -1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line and if it is not a block opening (open curly
|
||||
// brace). Returns true if an argument token was
|
||||
// loaded; false otherwise. If false, all tokens on
|
||||
// the line have been consumed except for potentially
|
||||
// a block opening. It handles imported tokens
|
||||
// correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if !d.nextOnSameLine() {
|
||||
return false
|
||||
}
|
||||
if d.Val() == "{" {
|
||||
// roll back; a block opening is not an argument
|
||||
d.cursor--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nextOnSameLine advances the cursor if the next
|
||||
// token is on the same line of the same file.
|
||||
func (d *Dispenser) nextOnSameLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block nested more than initialNestingLevel.
|
||||
// In other words, a loop over NextBlock() will iterate
|
||||
// all tokens in the block assuming the next token is an
|
||||
// open curly brace, until the matching closing brace.
|
||||
// The open and closing brace tokens for the outer-most
|
||||
// block will be consumed internally and omitted from
|
||||
// the iteration.
|
||||
//
|
||||
// Proper use of this method looks like this:
|
||||
//
|
||||
// for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
// }
|
||||
//
|
||||
// However, in simple cases where it is known that the
|
||||
// Dispenser is new and has not already traversed state
|
||||
// by a loop over NextBlock(), this will do:
|
||||
//
|
||||
// for d.NextBlock(0) {
|
||||
// }
|
||||
//
|
||||
// As with other token parsing logic, a loop over
|
||||
// NextBlock() should be contained within a loop over
|
||||
// Next(), as it is usually prudent to skip the initial
|
||||
// token.
|
||||
func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
|
||||
if d.nesting > initialNestingLevel {
|
||||
if !d.Next() {
|
||||
return false // should be EOF error
|
||||
}
|
||||
if d.Val() == "}" && !d.nextOnSameLine() {
|
||||
d.nesting--
|
||||
} else if d.Val() == "{" && !d.nextOnSameLine() {
|
||||
d.nesting++
|
||||
}
|
||||
return d.nesting > initialNestingLevel
|
||||
}
|
||||
if !d.nextOnSameLine() { // block must open on same line
|
||||
return false
|
||||
}
|
||||
if d.Val() != "{" {
|
||||
d.cursor-- // roll back if not opening brace
|
||||
return false
|
||||
}
|
||||
d.Next() // consume open curly brace
|
||||
if d.Val() == "}" {
|
||||
return false // open and then closed right away
|
||||
}
|
||||
d.nesting++
|
||||
return true
|
||||
}
|
||||
|
||||
// Nesting returns the current nesting level. Necessary
|
||||
// if using NextBlock()
|
||||
func (d *Dispenser) Nesting() int {
|
||||
return d.nesting
|
||||
}
|
||||
|
||||
// Val gets the text of the current token. If there is no token
|
||||
// loaded, it returns empty string.
|
||||
func (d *Dispenser) Val() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token.
|
||||
// If there is no token loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].Line
|
||||
}
|
||||
|
||||
// File gets the filename where the current token originated.
|
||||
func (d *Dispenser) File() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].File
|
||||
}
|
||||
|
||||
// Args is a convenience function that loads the next arguments
|
||||
// (tokens on the same line) into an arbitrary number of strings
|
||||
// pointed to in targets. If there are not enough argument tokens
|
||||
// available to fill targets, false is returned and the remaining
|
||||
// targets are left unchanged. If all the targets are filled,
|
||||
// then true is returned.
|
||||
func (d *Dispenser) Args(targets ...*string) bool {
|
||||
for i := 0; i < len(targets); i++ {
|
||||
if !d.NextArg() {
|
||||
return false
|
||||
}
|
||||
*targets[i] = d.Val()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AllArgs is like Args, but if there are more argument tokens
|
||||
// available than there are targets, false is returned. The
|
||||
// number of available argument tokens must match the number of
|
||||
// targets exactly to return true.
|
||||
func (d *Dispenser) AllArgs(targets ...*string) bool {
|
||||
if !d.Args(targets...) {
|
||||
return false
|
||||
}
|
||||
if d.NextArg() {
|
||||
d.Prev()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// NewFromNextSegment returns a new dispenser with a copy of
|
||||
// the tokens from the current token until the end of the
|
||||
// "directive" whether that be to the end of the line or
|
||||
// the end of a block that starts at the end of the line;
|
||||
// in other words, until the end of the segment.
|
||||
func (d *Dispenser) NewFromNextSegment() *Dispenser {
|
||||
return NewDispenser(d.NextSegment())
|
||||
}
|
||||
|
||||
// NextSegment returns a copy of the tokens from the current
|
||||
// token until the end of the line or block that starts at
|
||||
// the end of the line.
|
||||
func (d *Dispenser) NextSegment() Segment {
|
||||
tkns := Segment{d.Token()}
|
||||
for d.NextArg() {
|
||||
tkns = append(tkns, d.Token())
|
||||
}
|
||||
var openedBlock bool
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
if !openedBlock {
|
||||
// because NextBlock() consumes the initial open
|
||||
// curly brace, we rewind here to append it, since
|
||||
// our case is special in that we want the new
|
||||
// dispenser to have all the tokens including
|
||||
// surrounding curly braces
|
||||
d.Prev()
|
||||
tkns = append(tkns, d.Token())
|
||||
d.Next()
|
||||
openedBlock = true
|
||||
}
|
||||
tkns = append(tkns, d.Token())
|
||||
}
|
||||
if openedBlock {
|
||||
// include closing brace
|
||||
tkns = append(tkns, d.Token())
|
||||
|
||||
// do not consume the closing curly brace; the
|
||||
// next iteration of the enclosing loop will
|
||||
// call Next() and consume it
|
||||
}
|
||||
return tkns
|
||||
}
|
||||
|
||||
// Token returns the current token.
|
||||
func (d *Dispenser) Token() Token {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return Token{}
|
||||
}
|
||||
return d.tokens[d.cursor]
|
||||
}
|
||||
|
||||
// Reset sets d's cursor to the beginning, as
|
||||
// if this was a new and unused dispenser.
|
||||
func (d *Dispenser) Reset() {
|
||||
d.cursor = -1
|
||||
d.nesting = 0
|
||||
}
|
||||
|
||||
// ArgErr returns an argument error, meaning that another
|
||||
// argument was expected but not found. In other words,
|
||||
// a line break or open curly brace was encountered instead of
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Delete deletes the current token and returns the updated slice
|
||||
// of tokens. The cursor is not advanced to the next token.
|
||||
// Because deletion modifies the underlying slice, this method
|
||||
// should only be called if you have access to the original slice
|
||||
// of tokens and/or are using the slice of tokens outside this
|
||||
// Dispenser instance. If you do not re-assign the slice with the
|
||||
// return value of this method, inconsistencies in the token
|
||||
// array will become apparent (or worse, hide from you like they
|
||||
// did me for 3 and a half freaking hours late one night).
|
||||
func (d *Dispenser) Delete() []Token {
|
||||
if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 {
|
||||
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
|
||||
d.cursor--
|
||||
}
|
||||
return d.tokens
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
}
|
||||
Regular → Executable
+9
-9
@@ -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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -25,7 +25,7 @@ func TestDispenser_Val_Next(t *testing.T) {
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
if val := d.Val(); val != "" {
|
||||
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
|
||||
@@ -63,7 +63,7 @@ func TestDispenser_NextArg(t *testing.T) {
|
||||
input := `dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.Next() != shouldLoad {
|
||||
@@ -110,7 +110,7 @@ func TestDispenser_NextLine(t *testing.T) {
|
||||
input := `host:port
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.NextLine() != shouldLoad {
|
||||
@@ -143,10 +143,10 @@ func TestDispenser_NextBlock(t *testing.T) {
|
||||
}
|
||||
foobar2 {
|
||||
}`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
|
||||
if loaded := d.NextBlock(); loaded != shouldLoad {
|
||||
if loaded := d.NextBlock(0); loaded != shouldLoad {
|
||||
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
@@ -173,7 +173,7 @@ func TestDispenser_Args(t *testing.T) {
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 arg7
|
||||
dir4`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
@@ -240,7 +240,7 @@ func TestDispenser_RemainingArgs(t *testing.T) {
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 { arg7
|
||||
dir4`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
@@ -277,7 +277,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
input := `dir1 {
|
||||
}
|
||||
dir2 arg1 arg2`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
d := NewTestDispenser(input)
|
||||
|
||||
d.cursor = 1 // {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Format formats the input Caddyfile to a standard, nice-looking
|
||||
// appearance. It works by reading each rune of the input and taking
|
||||
// control over all the bracing and whitespace that is written; otherwise,
|
||||
// words, comments, placeholders, and escaped characters are all treated
|
||||
// literally and written as they appear in the input.
|
||||
func Format(input []byte) []byte {
|
||||
input = bytes.TrimSpace(input)
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
rdr := bytes.NewReader(input)
|
||||
|
||||
var (
|
||||
last rune // the last character that was written to the result
|
||||
|
||||
space = true // whether current/previous character was whitespace (beginning of input counts as space)
|
||||
beginningOfLine = true // whether we are at beginning of line
|
||||
|
||||
openBrace bool // whether current word/token is or started with open curly brace
|
||||
openBraceWritten bool // if openBrace, whether that brace was written or not
|
||||
openBraceSpace bool // whether there was a non-newline space before open brace
|
||||
|
||||
newLines int // count of newlines consumed
|
||||
|
||||
comment bool // whether we're in a comment
|
||||
quoted bool // whether we're in a quoted segment
|
||||
escaped bool // whether current char is escaped
|
||||
|
||||
nesting int // indentation level
|
||||
)
|
||||
|
||||
write := func(ch rune) {
|
||||
out.WriteRune(ch)
|
||||
last = ch
|
||||
}
|
||||
|
||||
indent := func() {
|
||||
for tabs := nesting; tabs > 0; tabs-- {
|
||||
write('\t')
|
||||
}
|
||||
}
|
||||
|
||||
nextLine := func() {
|
||||
write('\n')
|
||||
beginningOfLine = true
|
||||
}
|
||||
|
||||
for {
|
||||
ch, _, err := rdr.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if comment {
|
||||
if ch == '\n' {
|
||||
comment = false
|
||||
} else {
|
||||
write(ch)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !escaped && ch == '\\' {
|
||||
if space {
|
||||
write(' ')
|
||||
space = false
|
||||
}
|
||||
write(ch)
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if escaped {
|
||||
write(ch)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if ch == '"' {
|
||||
quoted = false
|
||||
}
|
||||
write(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if space && ch == '"' {
|
||||
quoted = true
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
space = true
|
||||
if ch == '\n' {
|
||||
newLines++
|
||||
}
|
||||
continue
|
||||
}
|
||||
spacePrior := space
|
||||
space = false
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// I find it helpful to think of the formatting loop in two
|
||||
// main sections; by the time we reach this point, we
|
||||
// know we are in a "regular" part of the file: we know
|
||||
// the character is not a space, not in a literal segment
|
||||
// like a comment or quoted, it's not escaped, etc.
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
if ch == '#' {
|
||||
comment = true
|
||||
}
|
||||
|
||||
if openBrace && spacePrior && !openBraceWritten {
|
||||
if nesting == 0 && last == '}' {
|
||||
nextLine()
|
||||
nextLine()
|
||||
}
|
||||
|
||||
openBrace = false
|
||||
if beginningOfLine {
|
||||
indent()
|
||||
} else if !openBraceSpace {
|
||||
write(' ')
|
||||
}
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
nextLine()
|
||||
newLines = 0
|
||||
nesting++
|
||||
}
|
||||
|
||||
switch {
|
||||
case ch == '{':
|
||||
openBrace = true
|
||||
openBraceWritten = false
|
||||
openBraceSpace = spacePrior && !beginningOfLine
|
||||
if openBraceSpace {
|
||||
write(' ')
|
||||
}
|
||||
continue
|
||||
|
||||
case ch == '}' && (spacePrior || !openBrace):
|
||||
if last != '\n' {
|
||||
nextLine()
|
||||
}
|
||||
if nesting > 0 {
|
||||
nesting--
|
||||
}
|
||||
indent()
|
||||
write('}')
|
||||
newLines = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if newLines > 2 {
|
||||
newLines = 2
|
||||
}
|
||||
for i := 0; i < newLines; i++ {
|
||||
nextLine()
|
||||
}
|
||||
newLines = 0
|
||||
if beginningOfLine {
|
||||
indent()
|
||||
}
|
||||
if nesting == 0 && last == '}' && beginningOfLine {
|
||||
nextLine()
|
||||
nextLine()
|
||||
}
|
||||
|
||||
if !beginningOfLine && spacePrior {
|
||||
write(' ')
|
||||
}
|
||||
|
||||
if openBrace && !openBraceWritten {
|
||||
write('{')
|
||||
openBraceWritten = true
|
||||
}
|
||||
write(ch)
|
||||
|
||||
beginningOfLine = false
|
||||
}
|
||||
|
||||
// the Caddyfile does not need any leading or trailing spaces, but...
|
||||
trimmedResult := bytes.TrimSpace(out.Bytes())
|
||||
|
||||
// ...Caddyfiles should, however, end with a newline because
|
||||
// newlines are significant to the syntax of the file
|
||||
return append(trimmedResult, '\n')
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
// 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 (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatter(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
description string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
description: "very simple",
|
||||
input: `abc def
|
||||
g hi jkl
|
||||
mn`,
|
||||
expect: `abc def
|
||||
g hi jkl
|
||||
mn`,
|
||||
},
|
||||
{
|
||||
description: "basic indentation, line breaks, and nesting",
|
||||
input: ` a
|
||||
b
|
||||
|
||||
c {
|
||||
d
|
||||
}
|
||||
|
||||
e { f
|
||||
}
|
||||
|
||||
|
||||
|
||||
g {
|
||||
h {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
j { k {
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
m {
|
||||
n { o
|
||||
}
|
||||
p { q r
|
||||
s }
|
||||
}
|
||||
|
||||
{
|
||||
{ t
|
||||
u
|
||||
|
||||
v
|
||||
|
||||
w
|
||||
}
|
||||
}`,
|
||||
expect: `a
|
||||
b
|
||||
|
||||
c {
|
||||
d
|
||||
}
|
||||
|
||||
e {
|
||||
f
|
||||
}
|
||||
|
||||
g {
|
||||
h {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
j {
|
||||
k {
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
m {
|
||||
n {
|
||||
o
|
||||
}
|
||||
p {
|
||||
q r
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
{
|
||||
t
|
||||
u
|
||||
|
||||
v
|
||||
|
||||
w
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "block spacing",
|
||||
input: `a{
|
||||
b
|
||||
}
|
||||
|
||||
c{ d
|
||||
}`,
|
||||
expect: `a {
|
||||
b
|
||||
}
|
||||
|
||||
c {
|
||||
d
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "advanced spacing",
|
||||
input: `abc {
|
||||
def
|
||||
}ghi{
|
||||
jkl mno
|
||||
pqr}`,
|
||||
expect: `abc {
|
||||
def
|
||||
}
|
||||
|
||||
ghi {
|
||||
jkl mno
|
||||
pqr
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "env var placeholders",
|
||||
input: `{$A}
|
||||
|
||||
b {
|
||||
{$C}
|
||||
}
|
||||
|
||||
d { {$E}
|
||||
}
|
||||
|
||||
{ {$F}
|
||||
}
|
||||
`,
|
||||
expect: `{$A}
|
||||
|
||||
b {
|
||||
{$C}
|
||||
}
|
||||
|
||||
d {
|
||||
{$E}
|
||||
}
|
||||
|
||||
{
|
||||
{$F}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "comments",
|
||||
input: `#a "\n"
|
||||
|
||||
#b {
|
||||
c
|
||||
}
|
||||
|
||||
d {
|
||||
e#f
|
||||
# g
|
||||
}
|
||||
|
||||
h { # i
|
||||
}`,
|
||||
expect: `#a "\n"
|
||||
|
||||
#b {
|
||||
c
|
||||
}
|
||||
|
||||
d {
|
||||
e#f
|
||||
# g
|
||||
}
|
||||
|
||||
h {
|
||||
# i
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "quotes and escaping",
|
||||
input: `"a \"b\" "#c
|
||||
d
|
||||
|
||||
e {
|
||||
"f"
|
||||
}
|
||||
|
||||
g { "h"
|
||||
}
|
||||
|
||||
i {
|
||||
"foo
|
||||
bar"
|
||||
}
|
||||
|
||||
j {
|
||||
"\"k\" l m"
|
||||
}`,
|
||||
expect: `"a \"b\" "#c
|
||||
d
|
||||
|
||||
e {
|
||||
"f"
|
||||
}
|
||||
|
||||
g {
|
||||
"h"
|
||||
}
|
||||
|
||||
i {
|
||||
"foo
|
||||
bar"
|
||||
}
|
||||
|
||||
j {
|
||||
"\"k\" l m"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "bad nesting (too many open)",
|
||||
input: `a
|
||||
{
|
||||
{
|
||||
}`,
|
||||
expect: `a {
|
||||
{
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "bad nesting (too many close)",
|
||||
input: `a
|
||||
{
|
||||
{
|
||||
}}}`,
|
||||
expect: `a {
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "json",
|
||||
input: `foo
|
||||
bar "{\"key\":34}"
|
||||
`,
|
||||
expect: `foo
|
||||
bar "{\"key\":34}"`,
|
||||
},
|
||||
{
|
||||
description: "escaping after spaces",
|
||||
input: `foo \"literal\"`,
|
||||
expect: `foo \"literal\"`,
|
||||
},
|
||||
{
|
||||
description: "simple placeholders as standalone tokens",
|
||||
input: `foo {bar}`,
|
||||
expect: `foo {bar}`,
|
||||
},
|
||||
{
|
||||
description: "simple placeholders within tokens",
|
||||
input: `foo{bar} foo{bar}baz`,
|
||||
expect: `foo{bar} foo{bar}baz`,
|
||||
},
|
||||
{
|
||||
description: "placeholders and malformed braces",
|
||||
input: `foo{bar} foo{ bar}baz`,
|
||||
expect: `foo{bar} foo {
|
||||
bar
|
||||
}
|
||||
|
||||
baz`,
|
||||
},
|
||||
{
|
||||
description: "hash within string is not a comment",
|
||||
input: `redir / /some/#/path`,
|
||||
expect: `redir / /some/#/path`,
|
||||
},
|
||||
} {
|
||||
// the formatter should output a trailing newline,
|
||||
// even if the tests aren't written to expect that
|
||||
if !strings.HasSuffix(tc.expect, "\n") {
|
||||
tc.expect += "\n"
|
||||
}
|
||||
|
||||
actual := Format([]byte(tc.input))
|
||||
|
||||
if string(actual) != tc.expect {
|
||||
t.Errorf("\n[TEST %d: %s]\n====== EXPECTED ======\n%s\n====== ACTUAL ======\n%s^^^^^^^^^^^^^^^^^^^^^",
|
||||
i, tc.description, string(tc.expect), string(actual))
|
||||
}
|
||||
}
|
||||
}
|
||||
Regular → Executable
+61
-22
@@ -16,6 +16,7 @@ package caddyfile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
@@ -26,9 +27,10 @@ type (
|
||||
// are separated by whitespace. A word can be enclosed
|
||||
// in quotes if it contains whitespace.
|
||||
lexer struct {
|
||||
reader *bufio.Reader
|
||||
token Token
|
||||
line int
|
||||
reader *bufio.Reader
|
||||
token Token
|
||||
line int
|
||||
skippedLines int
|
||||
}
|
||||
|
||||
// Token represents a single parsable unit.
|
||||
@@ -72,7 +74,7 @@ func (l *lexer) load(input io.Reader) error {
|
||||
// a token was loaded; false otherwise.
|
||||
func (l *lexer) next() bool {
|
||||
var val []rune
|
||||
var comment, quoted, escaped bool
|
||||
var comment, quoted, btQuoted, escaped bool
|
||||
|
||||
makeToken := func() bool {
|
||||
l.token.Text = string(val)
|
||||
@@ -91,27 +93,32 @@ func (l *lexer) next() bool {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if !escaped {
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
} else if ch == '"' {
|
||||
quoted = false
|
||||
if !escaped && !btQuoted && ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if quoted || btQuoted {
|
||||
if quoted && escaped {
|
||||
// all is literal in quoted area,
|
||||
// so only escape quotes
|
||||
if ch != '"' {
|
||||
val = append(val, '\\')
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
if quoted && ch == '"' {
|
||||
return makeToken()
|
||||
}
|
||||
if btQuoted && ch == '`' {
|
||||
return makeToken()
|
||||
}
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line++
|
||||
}
|
||||
if escaped {
|
||||
// only escape quotes
|
||||
if ch != '"' {
|
||||
val = append(val, '\\')
|
||||
}
|
||||
l.line += 1 + l.skippedLines
|
||||
l.skippedLines = 0
|
||||
}
|
||||
val = append(val, ch)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -120,7 +127,13 @@ func (l *lexer) next() bool {
|
||||
continue
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line++
|
||||
if escaped {
|
||||
l.skippedLines++
|
||||
escaped = false
|
||||
} else {
|
||||
l.line += 1 + l.skippedLines
|
||||
l.skippedLines = 0
|
||||
}
|
||||
comment = false
|
||||
}
|
||||
if len(val) > 0 {
|
||||
@@ -129,10 +142,9 @@ func (l *lexer) next() bool {
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '#' {
|
||||
if ch == '#' && len(val) == 0 {
|
||||
comment = true
|
||||
}
|
||||
|
||||
if comment {
|
||||
continue
|
||||
}
|
||||
@@ -143,8 +155,35 @@ func (l *lexer) next() bool {
|
||||
quoted = true
|
||||
continue
|
||||
}
|
||||
if ch == '`' {
|
||||
btQuoted = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if escaped {
|
||||
val = append(val, '\\')
|
||||
escaped = false
|
||||
}
|
||||
|
||||
val = append(val, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 l.next() {
|
||||
l.token.File = filename
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
Regular → Executable
+126
-34
@@ -15,37 +15,35 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexerTestCase struct {
|
||||
input string
|
||||
input []byte
|
||||
expected []Token
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testCases := []lexerTestCase{
|
||||
{
|
||||
input: `host:123`,
|
||||
input: []byte(`host:123`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123
|
||||
input: []byte(`host:123
|
||||
|
||||
directive`,
|
||||
directive`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 3, Text: "directive"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
input: []byte(`host:123 {
|
||||
directive
|
||||
}`,
|
||||
}`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
@@ -54,7 +52,7 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 { directive }`,
|
||||
input: []byte(`host:123 { directive }`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
@@ -63,12 +61,12 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
input: []byte(`host:123 {
|
||||
#comment
|
||||
directive
|
||||
# comment
|
||||
foobar # another comment
|
||||
}`,
|
||||
}`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
@@ -78,8 +76,28 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `a "quoted value" b
|
||||
foobar`,
|
||||
input: []byte(`host:123 {
|
||||
# hash inside string is not a comment
|
||||
redir / /some/#/path
|
||||
}`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 3, Text: "redir"},
|
||||
{Line: 3, Text: "/"},
|
||||
{Line: 3, Text: "/some/#/path"},
|
||||
{Line: 4, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("# comment at beginning of file\n# comment at beginning of line\nhost:123"),
|
||||
expected: []Token{
|
||||
{Line: 3, Text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`a "quoted value" b
|
||||
foobar`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "a"},
|
||||
{Line: 1, Text: "quoted value"},
|
||||
@@ -88,7 +106,7 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted \"value\" inside" B`,
|
||||
input: []byte(`A "quoted \"value\" inside" B`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "A"},
|
||||
{Line: 1, Text: `quoted "value" inside`},
|
||||
@@ -96,22 +114,72 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
input: []byte("An escaped \"newline\\\ninside\" quotes"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "An"},
|
||||
{Line: 1, Text: "escaped"},
|
||||
{Line: 1, Text: "newline\\\ninside"},
|
||||
{Line: 2, Text: "quotes"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("An escaped newline\\\noutside quotes"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "An"},
|
||||
{Line: 1, Text: "escaped"},
|
||||
{Line: 1, Text: "newline"},
|
||||
{Line: 1, Text: "outside"},
|
||||
{Line: 1, Text: "quotes"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("line1\\\nescaped\nline2\nline3"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "line1"},
|
||||
{Line: 1, Text: "escaped"},
|
||||
{Line: 3, Text: "line2"},
|
||||
{Line: 4, Text: "line3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("line1\\\nescaped1\\\nescaped2\nline4\nline5"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "line1"},
|
||||
{Line: 1, Text: "escaped1"},
|
||||
{Line: 1, Text: "escaped2"},
|
||||
{Line: 4, Text: "line4"},
|
||||
{Line: 5, Text: "line5"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`"unescapable\ in quotes"`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `unescapable\ in quotes`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`"don't\escape"`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `don't\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\\escape"`,
|
||||
input: []byte(`"don't\\escape"`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
input: []byte(`un\escapable`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `un\escapable`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`A "quoted value with line
|
||||
break inside" {
|
||||
foobar
|
||||
}`,
|
||||
}`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "A"},
|
||||
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||
@@ -121,13 +189,13 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"C:\php\php-cgi.exe"`,
|
||||
input: []byte(`"C:\php\php-cgi.exe"`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `C:\php\php-cgi.exe`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `empty "" string`,
|
||||
input: []byte(`empty "" string`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `empty`},
|
||||
{Line: 1, Text: ``},
|
||||
@@ -135,7 +203,7 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "skip those\r\nCR characters",
|
||||
input: []byte("skip those\r\nCR characters"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "skip"},
|
||||
{Line: 1, Text: "those"},
|
||||
@@ -144,30 +212,54 @@ func TestLexer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
|
||||
input: []byte("\xEF\xBB\xBF:8080"), // test with leading byte order mark
|
||||
expected: []Token{
|
||||
{Line: 1, Text: ":8080"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("simple `backtick quoted` string"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `simple`},
|
||||
{Line: 1, Text: `backtick quoted`},
|
||||
{Line: 1, Text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("multiline `backtick\nquoted\n` string"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `multiline`},
|
||||
{Line: 1, Text: "backtick\nquoted\n"},
|
||||
{Line: 3, Text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("nested `\"quotes inside\" backticks` string"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `nested`},
|
||||
{Line: 1, Text: `"quotes inside" backticks`},
|
||||
{Line: 1, Text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte("reverse-nested \"`backticks` inside\" quotes"),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `reverse-nested`},
|
||||
{Line: 1, Text: "`backticks` inside"},
|
||||
{Line: 1, Text: `quotes`},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := tokenize(testCase.input)
|
||||
actual, err := Tokenize(testCase.input, "")
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
lexerCompare(t, i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(input string) (tokens []Token) {
|
||||
l := lexer{}
|
||||
if err := l.load(strings.NewReader(input)); err != nil {
|
||||
log.Printf("[ERROR] load failed: %v", err)
|
||||
}
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||
Regular → Executable
+160
-99
@@ -15,11 +15,15 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"io"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
@@ -28,33 +32,74 @@ import (
|
||||
// Directives that do not appear in validDirectives will cause
|
||||
// an error. If you do not want to check for valid directives,
|
||||
// pass in nil instead.
|
||||
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
|
||||
//
|
||||
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
||||
// will be replaced before parsing begins.
|
||||
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
||||
tokens, err := allTokens(filename, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := parser{Dispenser: NewDispenser(tokens)}
|
||||
return p.parseAll()
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces all occurrences of environment variables.
|
||||
func replaceEnvVars(input []byte) ([]byte, error) {
|
||||
var offset int
|
||||
for {
|
||||
begin := bytes.Index(input[offset:], spanOpen)
|
||||
if begin < 0 {
|
||||
break
|
||||
}
|
||||
begin += offset // make beginning relative to input, not offset
|
||||
end := bytes.Index(input[begin+len(spanOpen):], spanClose)
|
||||
if end < 0 {
|
||||
break
|
||||
}
|
||||
end += begin + len(spanOpen) // make end relative to input, not begin
|
||||
|
||||
// get the name; if there is no name, skip it
|
||||
envVarName := input[begin+len(spanOpen) : end]
|
||||
if len(envVarName) == 0 {
|
||||
offset = end + len(spanClose)
|
||||
continue
|
||||
}
|
||||
|
||||
// get the value of the environment variable
|
||||
envVarValue := []byte(os.ExpandEnv(os.Getenv(string(envVarName))))
|
||||
|
||||
// splice in the value
|
||||
input = append(input[:begin],
|
||||
append(envVarValue, input[end+len(spanClose):]...)...)
|
||||
|
||||
// continue at the end of the replacement
|
||||
offset = begin + len(envVarValue)
|
||||
}
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) ([]Token, error) {
|
||||
l := new(lexer)
|
||||
err := l.load(input)
|
||||
func allTokens(filename string, input []byte) ([]Token, error) {
|
||||
input, err := replaceEnvVars(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tokens []Token
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
tokens, err := Tokenize(input, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
Dispenser
|
||||
*Dispenser
|
||||
block ServerBlock // current server block being parsed
|
||||
validDirectives []string // a directive must be valid or it's an error
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
definedSnippets map[string][]Token
|
||||
nesting int
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
@@ -65,17 +110,19 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Keys) > 0 {
|
||||
if len(p.block.Keys) > 0 || len(p.block.Segments) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
if p.nesting > 0 {
|
||||
return blocks, p.EOFErr()
|
||||
}
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseOne() error {
|
||||
p.block = ServerBlock{Tokens: make(map[string][]Token)}
|
||||
|
||||
p.block = ServerBlock{}
|
||||
return p.begin()
|
||||
}
|
||||
|
||||
@@ -121,7 +168,7 @@ func (p *parser) addresses() error {
|
||||
var expectingAnother bool
|
||||
|
||||
for {
|
||||
tkn := replaceEnvVars(p.Val())
|
||||
tkn := p.Val()
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if tkn == "import" && p.isNewLine() {
|
||||
@@ -182,7 +229,7 @@ func (p *parser) blockContents() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only look for close curly brace if there was an opening
|
||||
// only look for close curly brace if there was an opening
|
||||
if errOpenCurlyBrace == nil {
|
||||
err = p.closeCurlyBrace()
|
||||
if err != nil {
|
||||
@@ -201,6 +248,7 @@ func (p *parser) directives() error {
|
||||
for p.Next() {
|
||||
// end of server block
|
||||
if p.Val() == "}" {
|
||||
// p.nesting has already been decremented
|
||||
break
|
||||
}
|
||||
|
||||
@@ -214,11 +262,15 @@ func (p *parser) directives() error {
|
||||
continue
|
||||
}
|
||||
|
||||
// normal case: parse a directive on this line
|
||||
// normal case: parse a directive as a new segment
|
||||
// (a "segment" is a line which starts with a directive
|
||||
// and which ends at the end of the line or at the end of
|
||||
// the block that is opened at the end of the line)
|
||||
if err := p.directive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -233,15 +285,23 @@ func (p *parser) doImport() error {
|
||||
if !p.NextArg() {
|
||||
return p.ArgErr()
|
||||
}
|
||||
importPattern := replaceEnvVars(p.Val())
|
||||
importPattern := p.Val()
|
||||
if importPattern == "" {
|
||||
return p.Err("Import requires a non-empty filepath")
|
||||
}
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
|
||||
// grab remaining args as placeholder replacements
|
||||
args := p.RemainingArgs()
|
||||
|
||||
// add args to the replacer
|
||||
repl := caddy.NewReplacer()
|
||||
for index, arg := range args {
|
||||
repl.Set("args."+strconv.Itoa(index), arg)
|
||||
}
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
|
||||
// splice out the import directive and its arguments
|
||||
// (2 tokens, plus the length of args)
|
||||
tokensBefore := p.tokens[:p.cursor-1-len(args)]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
var importedTokens []Token
|
||||
|
||||
@@ -254,7 +314,7 @@ func (p *parser) doImport() error {
|
||||
// list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.File())
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
@@ -293,10 +353,20 @@ func (p *parser) doImport() error {
|
||||
}
|
||||
}
|
||||
|
||||
// copy the tokens so we don't overwrite p.definedSnippets
|
||||
tokensCopy := make([]Token, len(importedTokens))
|
||||
copy(tokensCopy, importedTokens)
|
||||
|
||||
// run the argument replacer on the tokens
|
||||
for index, token := range tokensCopy {
|
||||
token.Text = repl.ReplaceKnown(token.Text, "")
|
||||
tokensCopy[index] = token
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
// and rewind cursor so Next() will land on first imported token
|
||||
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
|
||||
p.cursor--
|
||||
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
|
||||
p.cursor -= len(args) + 1
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -316,7 +386,12 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
||||
}
|
||||
|
||||
importedTokens, err := allTokens(file)
|
||||
input, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
||||
}
|
||||
|
||||
importedTokens, err := allTokens(importFile, input)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
||||
}
|
||||
@@ -325,7 +400,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
// (we use full, absolute path to avoid bugs: issue #1892)
|
||||
filename, err := filepath.Abs(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
|
||||
}
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].File = filename
|
||||
@@ -341,26 +416,22 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := replaceEnvVars(p.Val())
|
||||
nesting := 0
|
||||
|
||||
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
|
||||
if !p.validDirective(dir) {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
// a segment is a list of tokens associated with this directive
|
||||
var segment Segment
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
// the directive itself is appended as a relevant token
|
||||
segment = append(segment, p.Token())
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
nesting++
|
||||
} else if p.isNewLine() && nesting == 0 {
|
||||
p.nesting++
|
||||
} else if p.isNewLine() && p.nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
} else if p.Val() == "}" && nesting > 0 {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
} else if p.Val() == "}" && p.nesting > 0 {
|
||||
p.nesting--
|
||||
} else if p.Val() == "}" && p.nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
} else if p.Val() == "import" && p.isNewLine() {
|
||||
if err := p.doImport(); err != nil {
|
||||
@@ -369,13 +440,16 @@ func (p *parser) directive() error {
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
|
||||
segment = append(segment, p.Token())
|
||||
}
|
||||
|
||||
if nesting > 0 {
|
||||
p.block.Segments = append(p.block.Segments, segment)
|
||||
|
||||
if p.nesting > 0 {
|
||||
return p.EOFErr()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -401,56 +475,6 @@ func (p *parser) closeCurlyBrace() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validDirective returns true if dir is in p.validDirectives.
|
||||
func (p *parser) validDirective(dir string) bool {
|
||||
if p.validDirectives == nil {
|
||||
return true
|
||||
}
|
||||
for _, d := range p.validDirectives {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces environment variables that appear in the token
|
||||
// and understands both the $UNIX and %WINDOWS% syntaxes.
|
||||
func replaceEnvVars(s string) string {
|
||||
s = replaceEnvReferences(s, "{%", "%}")
|
||||
s = replaceEnvReferences(s, "{$", "}")
|
||||
return s
|
||||
}
|
||||
|
||||
// replaceEnvReferences performs the actual replacement of env variables
|
||||
// in s, given the placeholder start and placeholder end strings.
|
||||
func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
index := strings.Index(s, refStart)
|
||||
for index != -1 {
|
||||
endIndex := strings.Index(s[index:], refEnd)
|
||||
if endIndex == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
endIndex += index
|
||||
if endIndex > index+len(refStart) {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
index = strings.Index(s, refStart)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ServerBlock associates any number of keys (usually addresses
|
||||
// of some sort) with tokens (grouped by directive name).
|
||||
type ServerBlock struct {
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) isSnippet() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
@@ -462,29 +486,66 @@ func (p *parser) isSnippet() (bool, string) {
|
||||
|
||||
// read and store everything in a block for later replay.
|
||||
func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// TODO: disallow imports in snippets for simplicity at import time
|
||||
// snippet must have curlies.
|
||||
err := p.openCurlyBrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := 1
|
||||
nesting := 1 // count our own nesting in snippets
|
||||
tokens := []Token{}
|
||||
for p.Next() {
|
||||
if p.Val() == "}" {
|
||||
count--
|
||||
if count == 0 {
|
||||
nesting--
|
||||
if nesting == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.Val() == "{" {
|
||||
count++
|
||||
nesting++
|
||||
}
|
||||
tokens = append(tokens, p.tokens[p.cursor])
|
||||
}
|
||||
// make sure we're matched up
|
||||
if count != 0 {
|
||||
if nesting != 0 {
|
||||
return nil, p.SyntaxErr("}")
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// ServerBlock associates any number of keys from the
|
||||
// head of the server block with tokens, which are
|
||||
// grouped by segments.
|
||||
type ServerBlock struct {
|
||||
Keys []string
|
||||
Segments []Segment
|
||||
}
|
||||
|
||||
// DispenseDirective returns a dispenser that contains
|
||||
// all the tokens in the server block.
|
||||
func (sb ServerBlock) DispenseDirective(dir string) *Dispenser {
|
||||
var tokens []Token
|
||||
for _, seg := range sb.Segments {
|
||||
if len(seg) > 0 && seg[0].Text == dir {
|
||||
tokens = append(tokens, seg...)
|
||||
}
|
||||
}
|
||||
return NewDispenser(tokens)
|
||||
}
|
||||
|
||||
// Segment is a list of tokens which begins with a directive
|
||||
// and ends at the end of the directive (either at the end of
|
||||
// the line, or at the end of a block it opens).
|
||||
type Segment []Token
|
||||
|
||||
// Directive returns the directive name for the segment.
|
||||
// The directive name is the text of the first token.
|
||||
func (s Segment) Directive() string {
|
||||
if len(s) > 0 {
|
||||
return s[0].Text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// spanOpen and spanClose are used to bound spans that
|
||||
// contain the name of an environment variable.
|
||||
var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
|
||||
Regular → Executable
+145
-199
@@ -15,17 +15,17 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
input := []byte("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens, err := allTokens(input)
|
||||
tokens, err := allTokens("TestAllTokens", input)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
@@ -53,84 +53,67 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys []string
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
numTokens []int // number of tokens to expect in each segment
|
||||
}{
|
||||
{`localhost`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{}},
|
||||
}, []int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
}, []int{1}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{3},
|
||||
},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
}, []int{1}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
}, []int{3, 1}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{3}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{3}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{3}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []string{
|
||||
"http://localhost",
|
||||
}, map[string]int{}},
|
||||
}, []int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []string{
|
||||
"host1:80",
|
||||
"http://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
}, []int{3, 2}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
@@ -138,7 +121,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
"http://host1.com",
|
||||
"http://host2.com",
|
||||
"https://host3.com",
|
||||
}, map[string]int{}},
|
||||
}, []int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
@@ -147,10 +130,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
dir2`, false, []string{
|
||||
"http://host1.com:1234",
|
||||
"https://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
}, []int{6, 1}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
@@ -160,34 +140,25 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
foo bar
|
||||
}`, false, []string{
|
||||
"127.0.0.1",
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
}, []int{5, 5}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{3}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{3}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
}, []int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
@@ -197,48 +168,41 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
}
|
||||
dir2 foo bar`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
}, []int{7, 3}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
{``, false, []string{}, []int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import testdata/import_test1.txt`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
}, []int{2, 3, 1}},
|
||||
|
||||
{`import testdata/import_test2.txt`, false, []string{
|
||||
"host1",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
}, []int{1, 2}},
|
||||
|
||||
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
|
||||
{`import testdata/not_found.txt`, true, []string{}, []int{}},
|
||||
|
||||
{`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
|
||||
{`""`, false, []string{}, []int{}},
|
||||
|
||||
{`""`, false, []string{}, map[string]int{}},
|
||||
{``, false, []string{}, []int{}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
// import with args
|
||||
{`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_args*.txt a b`, false, []string{"a"}, []int{2}},
|
||||
|
||||
// test cases found by fuzzing!
|
||||
{`import }{$"`, true, []string{}, map[string]int{}},
|
||||
{`import /*/*.txt`, true, []string{}, map[string]int{}},
|
||||
{`import /???/?*?o`, true, []string{}, map[string]int{}},
|
||||
{`import /??`, true, []string{}, map[string]int{}},
|
||||
{`import /[a-z]`, true, []string{}, map[string]int{}},
|
||||
{`import {$}`, true, []string{}, map[string]int{}},
|
||||
{`import {%}`, true, []string{}, map[string]int{}},
|
||||
{`import {$$}`, true, []string{}, map[string]int{}},
|
||||
{`import {%%}`, true, []string{}, map[string]int{}},
|
||||
{`import }{$"`, true, []string{}, []int{}},
|
||||
{`import /*/*.txt`, true, []string{}, []int{}},
|
||||
{`import /???/?*?o`, true, []string{}, []int{}},
|
||||
{`import /??`, true, []string{}, []int{}},
|
||||
{`import /[a-z]`, true, []string{}, []int{}},
|
||||
{`import {$}`, true, []string{}, []int{}},
|
||||
{`import {%}`, true, []string{}, []int{}},
|
||||
{`import {$$}`, true, []string{}, []int{}},
|
||||
{`import {%%}`, true, []string{}, []int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
@@ -249,6 +213,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
// t.Logf("%+v\n", result)
|
||||
if len(result.Keys) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d keys, got %d",
|
||||
i, len(test.keys), len(result.Keys))
|
||||
@@ -261,15 +226,16 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
if len(result.Segments) != len(test.numTokens) {
|
||||
t.Errorf("Test %d: Expected %d segments, had %d",
|
||||
i, len(test.numTokens), len(result.Segments))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
|
||||
for j, seg := range result.Segments {
|
||||
if len(seg) != test.numTokens[j] {
|
||||
t.Errorf("Test %d, segment %d: Expected %d tokens, counted %d",
|
||||
i, j, test.numTokens[j], len(seg))
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -289,12 +255,12 @@ func TestRecursiveImport(t *testing.T) {
|
||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens) != 2 {
|
||||
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
|
||||
if len(got.Segments) != 2 {
|
||||
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["dir2"]) != 2 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Tokens)
|
||||
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 {
|
||||
t.Errorf("got unexpected tokens: %v", got.Segments)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -384,12 +350,12 @@ func TestDirectiveImport(t *testing.T) {
|
||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens) != 2 {
|
||||
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
|
||||
if len(got.Segments) != 2 {
|
||||
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["proxy"]) != 8 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Tokens)
|
||||
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 {
|
||||
t.Errorf("got unexpected tokens: %v", got.Segments)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -511,96 +477,85 @@ func TestParseAll(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
os.Setenv("PARTIAL_DIR", "r1")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: "foo",
|
||||
expect: "foo",
|
||||
},
|
||||
{
|
||||
input: "{$NOT_SET}",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: "foo{$NOT_SET}bar",
|
||||
expect: "foobar",
|
||||
},
|
||||
{
|
||||
input: "{$FOOBAR}",
|
||||
expect: "foobar",
|
||||
},
|
||||
{
|
||||
input: "foo {$FOOBAR} bar",
|
||||
expect: "foo foobar bar",
|
||||
},
|
||||
{
|
||||
input: "foo{$FOOBAR}bar",
|
||||
expect: "foofoobarbar",
|
||||
},
|
||||
{
|
||||
input: "foo\n{$FOOBAR}\nbar",
|
||||
expect: "foo\nfoobar\nbar",
|
||||
},
|
||||
{
|
||||
input: "{$FOOBAR} {$FOOBAR}",
|
||||
expect: "foobar foobar",
|
||||
},
|
||||
{
|
||||
input: "{$FOOBAR}{$FOOBAR}",
|
||||
expect: "foobarfoobar",
|
||||
},
|
||||
{
|
||||
input: "{$FOOBAR",
|
||||
expect: "{$FOOBAR",
|
||||
},
|
||||
{
|
||||
input: "{$LONGER_NAME $FOOBAR}",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: "{$}",
|
||||
expect: "{$}",
|
||||
},
|
||||
{
|
||||
input: "{$$}",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: "{$",
|
||||
expect: "{$",
|
||||
},
|
||||
{
|
||||
input: "}{$",
|
||||
expect: "}{$",
|
||||
},
|
||||
} {
|
||||
actual, err := replaceEnvVars([]byte(test.input))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(actual, []byte(test.expect)) {
|
||||
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p = testParser(`di{$PARTIAL_DIR}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// after end token
|
||||
p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["answer"][1].Text, "{{ .Name }} foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
@@ -617,26 +572,21 @@ func TestSnippets(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||
}
|
||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if len(blocks[0].Tokens) != 2 {
|
||||
t.Fatalf("Server block should have tokens from import")
|
||||
if len(blocks[0].Segments) != 2 {
|
||||
t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0])
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
|
||||
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
|
||||
if actual, expected := blocks[0].Segments[1][1].Text, "stderr"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
||||
@@ -666,11 +616,7 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
auth := blocks[0].Tokens["basicauth"]
|
||||
auth := blocks[0].Segments[0]
|
||||
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
|
||||
if line != "basicauth / import password" {
|
||||
// Previously, it would be changed to:
|
||||
@@ -701,20 +647,20 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||
}
|
||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if len(blocks[0].Tokens) != 1 {
|
||||
if len(blocks[0].Segments) != 1 {
|
||||
t.Fatalf("Server block should have tokens from import")
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
|
||||
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
return parser{Dispenser: NewTestDispenser(input)}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{args.0}
|
||||
@@ -0,0 +1 @@
|
||||
{args.0} {args.1}
|
||||
Vendored
Regular → Executable
Vendored
Regular → Executable
Vendored
Regular → Executable
Vendored
Regular → Executable
Vendored
Regular → Executable
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// Adapter is a type which can adapt a configuration to Caddy JSON.
|
||||
// It returns the results and any warnings, or an error.
|
||||
type Adapter interface {
|
||||
Adapt(body []byte, options map[string]interface{}) ([]byte, []Warning, error)
|
||||
}
|
||||
|
||||
// Warning represents a warning or notice related to conversion.
|
||||
type Warning struct {
|
||||
File string `json:"file,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
Directive string `json:"directive,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
|
||||
// marshaling errors (which are highly unlikely with correct code)
|
||||
// are converted to warnings. This is convenient when filling config
|
||||
// structs that require a json.RawMessage, without having to worry
|
||||
// about errors.
|
||||
func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
|
||||
b, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
if warnings != nil {
|
||||
*warnings = append(*warnings, Warning{Message: err.Error()})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// JSONModuleObject is like JSON(), except it marshals val into a JSON object
|
||||
// with an added key named fieldName with the value fieldVal. This is useful
|
||||
// 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
|
||||
// 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
|
||||
// a struct or map). Any errors are converted into warnings.
|
||||
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
|
||||
// encode to a JSON object first
|
||||
enc, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
if warnings != nil {
|
||||
*warnings = append(*warnings, Warning{Message: err.Error()})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// then decode the object
|
||||
var tmp map[string]interface{}
|
||||
err = json.Unmarshal(enc, &tmp)
|
||||
if err != nil {
|
||||
if warnings != nil {
|
||||
*warnings = append(*warnings, Warning{Message: err.Error()})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// so we can easily add the module's field with its appointed value
|
||||
tmp[fieldName] = fieldVal
|
||||
|
||||
// then re-marshal as JSON
|
||||
result, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
if warnings != nil {
|
||||
*warnings = append(*warnings, Warning{Message: err.Error()})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// JSONIndent is used to JSON-marshal the final resulting Caddy
|
||||
// configuration in a consistent, human-readable way.
|
||||
func JSONIndent(val interface{}) ([]byte, error) {
|
||||
return json.MarshalIndent(val, "", "\t")
|
||||
}
|
||||
|
||||
// RegisterAdapter registers a config adapter with the given name.
|
||||
// This should usually be done at init-time. It panics if the
|
||||
// adapter cannot be registered successfully.
|
||||
func RegisterAdapter(name string, adapter Adapter) {
|
||||
if _, ok := configAdapters[name]; ok {
|
||||
panic(fmt.Errorf("%s: already registered", name))
|
||||
}
|
||||
configAdapters[name] = adapter
|
||||
caddy.RegisterModule(adapterModule{name, adapter})
|
||||
}
|
||||
|
||||
// GetAdapter returns the adapter with the given name,
|
||||
// or nil if one with that name is not registered.
|
||||
func GetAdapter(name string) Adapter {
|
||||
return configAdapters[name]
|
||||
}
|
||||
|
||||
// adapterModule is a wrapper type that can turn any config
|
||||
// adapter into a Caddy module, which has the benefit of being
|
||||
// counted with other modules, even though they do not
|
||||
// technically extend the Caddy configuration structure.
|
||||
// See caddyserver/caddy#3132.
|
||||
type adapterModule struct {
|
||||
name string
|
||||
Adapter
|
||||
}
|
||||
|
||||
func (am adapterModule) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: caddy.ModuleID("caddy.adapters." + am.name),
|
||||
New: func() caddy.Module { return am },
|
||||
}
|
||||
}
|
||||
|
||||
var configAdapters = make(map[string]Adapter)
|
||||
@@ -0,0 +1,392 @@
|
||||
// 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 httpcaddyfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/certmagic"
|
||||
)
|
||||
|
||||
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||
// blocks that will be served on that address. To do this, each server block is
|
||||
// expanded so that each one is considered individually, although keys of a
|
||||
// server block that share the same address stay grouped together so the config
|
||||
// isn't repeated unnecessarily. For example, this Caddyfile:
|
||||
//
|
||||
// example.com {
|
||||
// bind 127.0.0.1
|
||||
// }
|
||||
// www.example.com, example.net/path, localhost:9999 {
|
||||
// bind 127.0.0.1 1.2.3.4
|
||||
// }
|
||||
//
|
||||
// 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,
|
||||
// and 127.0.0.1:9999. This is because the bind directive is applied to each
|
||||
// key of its server block (specifying the host part), and each key may have
|
||||
// a different port. And we definitely need to be sure that a site which is
|
||||
// bound to be served on a specific interface is not served on others just
|
||||
// because that is more convenient: it would be a potential security risk
|
||||
// if the difference between interfaces means private vs. public.
|
||||
//
|
||||
// So what this function does for the example above is iterate each server
|
||||
// block, and for each server block, iterate its keys. For the first, it
|
||||
// finds one key (example.com) and determines its listener address
|
||||
// (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds
|
||||
// the listener address to the map value returned by this function, with
|
||||
// the first server block as one of its associations.
|
||||
//
|
||||
// It then iterates each key on the second server block and associates them
|
||||
// with one or more listener addresses. Indeed, each key in this block has
|
||||
// two listener addresses because of the 'bind' directive. Once we know
|
||||
// which addresses serve which keys, we can create a new server block for
|
||||
// each address containing the contents of the server block and only those
|
||||
// specific keys of the server block which use that address.
|
||||
//
|
||||
// It is possible and even likely that some keys in the returned map have
|
||||
// the exact same list of server blocks (i.e. they are identical). This
|
||||
// happens when multiple hosts are declared with a 'bind' directive and
|
||||
// the resulting listener addresses are not shared by any other server
|
||||
// block (or the other server blocks are exactly identical in their token
|
||||
// contents). This happens with our example above because 1.2.3.4:443
|
||||
// and 1.2.3.4:9999 are used exclusively with the second server block. This
|
||||
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
||||
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||
// (Doing this is essentially a map-reduce technique.)
|
||||
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
||||
options map[string]interface{}) (map[string][]serverBlock, error) {
|
||||
sbmap := make(map[string][]serverBlock)
|
||||
|
||||
for i, sblock := range originalServerBlocks {
|
||||
// within a server block, we need to map all the listener addresses
|
||||
// implied by the server block to the keys of the server block which
|
||||
// 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
|
||||
// contents in cases where multiple keys really can be served together
|
||||
addrToKeys := make(map[string][]string)
|
||||
for j, key := range sblock.block.Keys {
|
||||
// a key can have multiple listener addresses if there are multiple
|
||||
// arguments to the 'bind' directive (although they will all have
|
||||
// the same port, since the port is defined by the key or is implicit
|
||||
// through automatic HTTPS)
|
||||
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key, options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err)
|
||||
}
|
||||
|
||||
// associate this key with each listener address it is served on
|
||||
for _, addr := range addrs {
|
||||
addrToKeys[addr] = append(addrToKeys[addr], key)
|
||||
}
|
||||
}
|
||||
|
||||
// now that we know which addresses serve which keys of this
|
||||
// server block, we iterate that mapping and create a list of
|
||||
// new server blocks for each address where the keys of the
|
||||
// server block are only the ones which use the address; but
|
||||
// the contents (tokens) are of course the same
|
||||
for addr, keys := range addrToKeys {
|
||||
// parse keys so that we only have to do it once
|
||||
parsedKeys := make([]Address, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
addr, err := ParseAddress(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing key '%s': %v", key, err)
|
||||
}
|
||||
parsedKeys = append(parsedKeys, addr.Normalize())
|
||||
}
|
||||
sbmap[addr] = append(sbmap[addr], serverBlock{
|
||||
block: caddyfile.ServerBlock{
|
||||
Keys: keys,
|
||||
Segments: sblock.block.Segments,
|
||||
},
|
||||
pile: sblock.pile,
|
||||
keys: parsedKeys,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return sbaddrs
|
||||
}
|
||||
|
||||
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
||||
options map[string]interface{}) ([]string, error) {
|
||||
addr, err := ParseAddress(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing key: %v", err)
|
||||
}
|
||||
addr = addr.Normalize()
|
||||
|
||||
// figure out the HTTP and HTTPS ports; either
|
||||
// use defaults, or override with user config
|
||||
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
||||
if hport, ok := options["http_port"]; ok {
|
||||
httpPort = strconv.Itoa(hport.(int))
|
||||
}
|
||||
if hsport, ok := options["https_port"]; ok {
|
||||
httpsPort = strconv.Itoa(hsport.(int))
|
||||
}
|
||||
|
||||
// default port is the HTTPS port
|
||||
lnPort := httpsPort
|
||||
if addr.Port != "" {
|
||||
// port explicitly defined
|
||||
lnPort = addr.Port
|
||||
} else if addr.Scheme == "http" {
|
||||
// port inferred from scheme
|
||||
lnPort = httpPort
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
|
||||
return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
|
||||
}
|
||||
|
||||
// the bind directive specifies hosts, but is optional
|
||||
lnHosts := make([]string, 0, len(sblock.pile))
|
||||
for _, cfgVal := range sblock.pile["bind"] {
|
||||
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
|
||||
}
|
||||
if len(lnHosts) == 0 {
|
||||
lnHosts = []string{""}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// now turn map into list
|
||||
listenersList := make([]string, 0, len(listeners))
|
||||
for lnStr := range listeners {
|
||||
listenersList = append(listenersList, lnStr)
|
||||
}
|
||||
|
||||
return listenersList, nil
|
||||
}
|
||||
|
||||
// Address represents a site address. It contains
|
||||
// the original input value, and the component
|
||||
// parts of an address. The component parts may be
|
||||
// updated to the correct values as setup proceeds,
|
||||
// but the original value should never be changed.
|
||||
//
|
||||
// The Host field must be in a normalized form.
|
||||
type Address struct {
|
||||
Original, Scheme, Host, Port, Path string
|
||||
}
|
||||
|
||||
// ParseAddress parses an address string into a structured format with separate
|
||||
// scheme, host, port, and path portions, as well as the original input string.
|
||||
func ParseAddress(str string) (Address, error) {
|
||||
const maxLen = 4096
|
||||
if len(str) > maxLen {
|
||||
str = str[:maxLen]
|
||||
}
|
||||
remaining := strings.TrimSpace(str)
|
||||
a := Address{Original: remaining}
|
||||
|
||||
// extract scheme
|
||||
splitScheme := strings.SplitN(remaining, "://", 2)
|
||||
switch len(splitScheme) {
|
||||
case 0:
|
||||
return a, nil
|
||||
case 1:
|
||||
remaining = splitScheme[0]
|
||||
case 2:
|
||||
a.Scheme = splitScheme[0]
|
||||
remaining = splitScheme[1]
|
||||
}
|
||||
|
||||
// extract host and port
|
||||
hostSplit := strings.SplitN(remaining, "/", 2)
|
||||
if len(hostSplit) > 0 {
|
||||
host, port, err := net.SplitHostPort(hostSplit[0])
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(hostSplit[0] + ":")
|
||||
if err != nil {
|
||||
host = hostSplit[0]
|
||||
}
|
||||
}
|
||||
a.Host = host
|
||||
a.Port = port
|
||||
}
|
||||
if len(hostSplit) == 2 {
|
||||
// all that remains is the path
|
||||
a.Path = "/" + hostSplit[1]
|
||||
}
|
||||
|
||||
// make sure port is valid
|
||||
if a.Port != "" {
|
||||
if portNum, err := strconv.Atoi(a.Port); err != nil {
|
||||
return Address{}, fmt.Errorf("invalid port '%s': %v", a.Port, err)
|
||||
} else if portNum < 0 || portNum > 65535 {
|
||||
return Address{}, fmt.Errorf("port %d is out of range", portNum)
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// String returns a human-readable form of a. It will
|
||||
// be a cleaned-up and filled-out URL string.
|
||||
func (a Address) String() string {
|
||||
if a.Host == "" && a.Port == "" {
|
||||
return ""
|
||||
}
|
||||
scheme := a.Scheme
|
||||
if scheme == "" {
|
||||
if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
}
|
||||
s := scheme
|
||||
if s != "" {
|
||||
s += "://"
|
||||
}
|
||||
if a.Port != "" &&
|
||||
((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) ||
|
||||
(scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) {
|
||||
s += net.JoinHostPort(a.Host, a.Port)
|
||||
} else {
|
||||
s += a.Host
|
||||
}
|
||||
if a.Path != "" {
|
||||
s += a.Path
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Normalize returns a normalized version of a.
|
||||
func (a Address) Normalize() Address {
|
||||
path := a.Path
|
||||
|
||||
// ensure host is normalized if it's an IP address
|
||||
host := strings.TrimSpace(a.Host)
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
host = ip.String()
|
||||
}
|
||||
|
||||
return Address{
|
||||
Original: a.Original,
|
||||
Scheme: lowerExceptPlaceholders(a.Scheme),
|
||||
Host: lowerExceptPlaceholders(host),
|
||||
Port: a.Port,
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// placeholders (substrings in non-escaped '{ }' spans).
|
||||
// See https://github.com/caddyserver/caddy/issues/3264
|
||||
func lowerExceptPlaceholders(s string) string {
|
||||
var sb strings.Builder
|
||||
var escaped, inPlaceholder bool
|
||||
for _, ch := range s {
|
||||
if ch == '\\' && !escaped {
|
||||
escaped = true
|
||||
sb.WriteRune(ch)
|
||||
continue
|
||||
}
|
||||
if ch == '{' && !escaped {
|
||||
inPlaceholder = true
|
||||
}
|
||||
if ch == '}' && inPlaceholder && !escaped {
|
||||
inPlaceholder = false
|
||||
}
|
||||
if inPlaceholder {
|
||||
sb.WriteRune(ch)
|
||||
} else {
|
||||
sb.WriteRune(unicode.ToLower(ch))
|
||||
}
|
||||
escaped = false
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,20 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
// +build gofuzz
|
||||
|
||||
import "testing"
|
||||
package httpcaddyfile
|
||||
|
||||
// This works because it does not have the same signature as the
|
||||
// conventional "TestMain" function described in the testing package
|
||||
// godoc.
|
||||
func TestMain(t *testing.T) {
|
||||
var ran bool
|
||||
run = func() {
|
||||
ran = true
|
||||
}
|
||||
main()
|
||||
if !ran {
|
||||
t.Error("Expected Run() to be called, but it wasn't")
|
||||
func FuzzParseAddress(data []byte) int {
|
||||
addr, err := ParseAddress(string(data))
|
||||
if err != nil {
|
||||
if addr == (Address{}) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port, path string
|
||||
shouldErr bool
|
||||
}{
|
||||
{``, "", "", "", "", false},
|
||||
{`localhost`, "", "localhost", "", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", "", false},
|
||||
{`localhost:`, "", "localhost", "", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false},
|
||||
{`:1234`, "", "", "1234", "", false},
|
||||
{`[::1]`, "", "::1", "", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", "", false},
|
||||
{`:`, "", "", "", "", false},
|
||||
{`:http`, "", "", "", "", true},
|
||||
{`:https`, "", "", "", "", true},
|
||||
{`localhost:http`, "", "", "", "", true}, // using service name in port is verboten, as of Go 1.12.8
|
||||
{`localhost:https`, "", "", "", "", true},
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
|
||||
{`host:https/path`, "", "", "", "", true},
|
||||
{`http://localhost:443`, "http", "localhost", "443", "", false}, // NOTE: not conventional
|
||||
{`https://localhost:80`, "https", "localhost", "80", "", false}, // NOTE: not conventional
|
||||
{`http://localhost`, "http", "localhost", "", "", false},
|
||||
{`https://localhost`, "https", "localhost", "", "", false},
|
||||
{`http://{env.APP_DOMAIN}`, "http", "{env.APP_DOMAIN}", "", "", false},
|
||||
{`{env.APP_DOMAIN}:80`, "", "{env.APP_DOMAIN}", "80", "", false},
|
||||
{`{env.APP_DOMAIN}/path`, "", "{env.APP_DOMAIN}", "", "/path", false},
|
||||
{`example.com/{env.APP_PATH}`, "", "example.com", "", "/{env.APP_PATH}", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "", "", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "", "", false},
|
||||
{`http://[::1]`, "http", "::1", "", "", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", "", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
|
||||
{``, "", "", "", "", false},
|
||||
{`::1`, "", "::1", "", "", false},
|
||||
{`localhost::`, "", "localhost::", "", "", false},
|
||||
{`#$%@`, "", "#$%@", "", "", false}, // don't want to presume what the hostname could be
|
||||
{`host/path`, "", "host", "", "/path", false},
|
||||
{`http://host/`, "http", "host", "", "/", false},
|
||||
{`//asdf`, "", "", "", "//asdf", false},
|
||||
{`:1234/asdf`, "", "", "1234", "/asdf", false},
|
||||
{`http://host/path`, "http", "host", "", "/path", false},
|
||||
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
|
||||
{`host:80/path`, "", "host", "80", "/path", false},
|
||||
{`/path`, "", "", "", "/path", false},
|
||||
} {
|
||||
actual, err := ParseAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected error, but had none (%#v)", i, test.input, actual)
|
||||
}
|
||||
|
||||
if !test.shouldErr && actual.Original != test.input {
|
||||
t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original)
|
||||
}
|
||||
if actual.Scheme != test.scheme {
|
||||
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != test.host {
|
||||
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
|
||||
}
|
||||
if actual.Port != test.port {
|
||||
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
|
||||
}
|
||||
if actual.Path != test.path {
|
||||
t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressString(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
addr Address
|
||||
expected string
|
||||
}{
|
||||
{Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"},
|
||||
{Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"},
|
||||
{Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"},
|
||||
{Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"},
|
||||
{Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"},
|
||||
{Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"},
|
||||
{Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"},
|
||||
{Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"},
|
||||
{Address{Scheme: "", Host: "", Port: "", Path: ""}, ""},
|
||||
} {
|
||||
actual := test.addr.String()
|
||||
if actual != test.expected {
|
||||
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyNormalization(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
input: "example.com",
|
||||
expect: "example.com",
|
||||
},
|
||||
{
|
||||
input: "http://host:1234/path",
|
||||
expect: "http://host:1234/path",
|
||||
},
|
||||
{
|
||||
input: "HTTP://A/ABCDEF",
|
||||
expect: "http://a/ABCDEF",
|
||||
},
|
||||
{
|
||||
input: "A/ABCDEF",
|
||||
expect: "a/ABCDEF",
|
||||
},
|
||||
{
|
||||
input: "A:2015/Path",
|
||||
expect: "a:2015/Path",
|
||||
},
|
||||
{
|
||||
input: "sub.{env.MY_DOMAIN}",
|
||||
expect: "sub.{env.MY_DOMAIN}",
|
||||
},
|
||||
{
|
||||
input: "sub.ExAmPle",
|
||||
expect: "sub.example",
|
||||
},
|
||||
{
|
||||
input: "sub.\\{env.MY_DOMAIN\\}",
|
||||
expect: "sub.\\{env.my_domain\\}",
|
||||
},
|
||||
{
|
||||
input: "sub.{env.MY_DOMAIN}.com",
|
||||
expect: "sub.{env.MY_DOMAIN}.com",
|
||||
},
|
||||
{
|
||||
input: ":80",
|
||||
expect: ":80",
|
||||
},
|
||||
{
|
||||
input: ":443",
|
||||
expect: ":443",
|
||||
},
|
||||
{
|
||||
input: ":1234",
|
||||
expect: ":1234",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: ":",
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: "[::]",
|
||||
expect: "::",
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
addr, err := ParseAddress(tc.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
|
||||
continue
|
||||
}
|
||||
if actual := addr.Normalize().Key(); actual != tc.expect {
|
||||
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
// 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 httpcaddyfile
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDirective("bind", parseBind)
|
||||
RegisterDirective("tls", parseTLS)
|
||||
RegisterHandlerDirective("root", parseRoot)
|
||||
RegisterHandlerDirective("redir", parseRedir)
|
||||
RegisterHandlerDirective("respond", parseRespond)
|
||||
RegisterHandlerDirective("route", parseRoute)
|
||||
RegisterHandlerDirective("handle", parseHandle)
|
||||
RegisterDirective("handle_errors", parseHandleErrors)
|
||||
RegisterDirective("log", parseLog)
|
||||
}
|
||||
|
||||
// parseBind parses the bind directive. Syntax:
|
||||
//
|
||||
// bind <addresses...>
|
||||
//
|
||||
func parseBind(h Helper) ([]ConfigValue, error) {
|
||||
var lnHosts []string
|
||||
for h.Next() {
|
||||
lnHosts = append(lnHosts, h.RemainingArgs()...)
|
||||
}
|
||||
return h.NewBindAddresses(lnHosts), nil
|
||||
}
|
||||
|
||||
// parseTLS parses the tls directive. Syntax:
|
||||
//
|
||||
// tls [<email>|internal]|[<cert_file> <key_file>] {
|
||||
// protocols <min> [<max>]
|
||||
// ciphers <cipher_suites...>
|
||||
// curves <curves...>
|
||||
// client_auth {
|
||||
// mode [request|require|verify_if_given|require_and_verify]
|
||||
// trusted_ca_cert <base64_der>
|
||||
// trusted_ca_cert_file <filename>
|
||||
// trusted_leaf_cert <base64_der>
|
||||
// trusted_leaf_cert_file <filename>
|
||||
// }
|
||||
// alpn <values...>
|
||||
// load <paths...>
|
||||
// ca <acme_ca_endpoint>
|
||||
// ca_root <pem_file>
|
||||
// dns <provider_name>
|
||||
// on_demand
|
||||
// eab <key_id> <mac_key>
|
||||
// issuer <module_name> ...
|
||||
// }
|
||||
//
|
||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
cp := new(caddytls.ConnectionPolicy)
|
||||
var fileLoader caddytls.FileLoader
|
||||
var folderLoader caddytls.FolderLoader
|
||||
var certSelector caddytls.CustomCertSelectionPolicy
|
||||
var acmeIssuer *caddytls.ACMEIssuer
|
||||
var internalIssuer *caddytls.InternalIssuer
|
||||
var issuer certmagic.Issuer
|
||||
var onDemand bool
|
||||
|
||||
for h.Next() {
|
||||
// file certificate loader
|
||||
firstLine := h.RemainingArgs()
|
||||
switch len(firstLine) {
|
||||
case 0:
|
||||
case 1:
|
||||
if firstLine[0] == "internal" {
|
||||
internalIssuer = new(caddytls.InternalIssuer)
|
||||
} else if !strings.Contains(firstLine[0], "@") {
|
||||
return nil, h.Err("single argument must either be 'internal' or an email address")
|
||||
} else {
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
acmeIssuer.Email = firstLine[0]
|
||||
}
|
||||
|
||||
case 2:
|
||||
certFilename := firstLine[0]
|
||||
keyFilename := firstLine[1]
|
||||
|
||||
// tag this certificate so if multiple certs match, specifically
|
||||
// this one that the user has provided will be used, see #2588:
|
||||
// https://github.com/caddyserver/caddy/issues/2588 ... but we
|
||||
// must be careful about how we do this; being careless will
|
||||
// lead to failed handshakes
|
||||
//
|
||||
// we need to remember which cert files we've seen, since we
|
||||
// must load each cert only once; otherwise, they each get a
|
||||
// different tag... since a cert loaded twice has the same
|
||||
// bytes, it will overwrite the first one in the cache, and
|
||||
// only the last cert (and its tag) will survive, so a any conn
|
||||
// policy that is looking for any tag but the last one to be
|
||||
// loaded won't find it, and TLS handshakes will fail (see end)
|
||||
// of issue #3004)
|
||||
//
|
||||
// tlsCertTags maps certificate filenames to their tag.
|
||||
// This is used to remember which tag is used for each
|
||||
// certificate files, since we need to avoid loading
|
||||
// the same certificate files more than once, overwriting
|
||||
// previous tags
|
||||
tlsCertTags, ok := h.State["tlsCertTags"].(map[string]string)
|
||||
if !ok {
|
||||
tlsCertTags = make(map[string]string)
|
||||
h.State["tlsCertTags"] = tlsCertTags
|
||||
}
|
||||
|
||||
tag, ok := tlsCertTags[certFilename]
|
||||
if !ok {
|
||||
// haven't seen this cert file yet, let's give it a tag
|
||||
// and add a loader for it
|
||||
tag = fmt.Sprintf("cert%d", len(tlsCertTags))
|
||||
fileLoader = append(fileLoader, caddytls.CertKeyFilePair{
|
||||
Certificate: certFilename,
|
||||
Key: keyFilename,
|
||||
Tags: []string{tag},
|
||||
})
|
||||
// remember this for next time we see this cert file
|
||||
tlsCertTags[certFilename] = tag
|
||||
}
|
||||
certSelector.AnyTag = append(certSelector.AnyTag, tag)
|
||||
|
||||
default:
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
var hasBlock bool
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
hasBlock = true
|
||||
|
||||
switch h.Val() {
|
||||
case "protocols":
|
||||
args := h.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, h.SyntaxErr("one or two protocols")
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
|
||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
}
|
||||
cp.ProtocolMin = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
|
||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
|
||||
}
|
||||
cp.ProtocolMax = args[1]
|
||||
}
|
||||
|
||||
case "ciphers":
|
||||
for h.NextArg() {
|
||||
if !caddytls.CipherSuiteNameSupported(h.Val()) {
|
||||
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||
}
|
||||
cp.CipherSuites = append(cp.CipherSuites, h.Val())
|
||||
}
|
||||
|
||||
case "curves":
|
||||
for h.NextArg() {
|
||||
if _, ok := caddytls.SupportedCurves[h.Val()]; !ok {
|
||||
return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val())
|
||||
}
|
||||
cp.Curves = append(cp.Curves, h.Val())
|
||||
}
|
||||
|
||||
case "client_auth":
|
||||
cp.ClientAuthentication = &caddytls.ClientAuthentication{}
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
subdir := h.Val()
|
||||
switch subdir {
|
||||
case "mode":
|
||||
if !h.Args(&cp.ClientAuthentication.Mode) {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
case "trusted_ca_cert",
|
||||
"trusted_leaf_cert":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if subdir == "trusted_ca_cert" {
|
||||
cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, h.Val())
|
||||
} else {
|
||||
cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, h.Val())
|
||||
}
|
||||
|
||||
case "trusted_ca_cert_file",
|
||||
"trusted_leaf_cert_file":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
filename := h.Val()
|
||||
certDataPEM, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(certDataPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return nil, h.Errf("no CERTIFICATE pem block found in %s", h.Val())
|
||||
}
|
||||
if subdir == "trusted_ca_cert_file" {
|
||||
cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts,
|
||||
base64.StdEncoding.EncodeToString(block.Bytes))
|
||||
} else {
|
||||
cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts,
|
||||
base64.StdEncoding.EncodeToString(block.Bytes))
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, h.Errf("unknown subdirective for client_auth: %s", subdir)
|
||||
}
|
||||
}
|
||||
|
||||
case "alpn":
|
||||
args := h.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
cp.ALPN = args
|
||||
|
||||
case "load":
|
||||
folderLoader = append(folderLoader, h.RemainingArgs()...)
|
||||
|
||||
case "ca":
|
||||
arg := h.RemainingArgs()
|
||||
if len(arg) != 1 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
acmeIssuer.CA = arg[0]
|
||||
|
||||
case "eab":
|
||||
arg := h.RemainingArgs()
|
||||
if len(arg) != 2 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
acmeIssuer.ExternalAccount = &acme.EAB{
|
||||
KeyID: arg[0],
|
||||
MACKey: arg[1],
|
||||
}
|
||||
|
||||
case "issuer":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
modName := h.Val()
|
||||
mod, err := caddy.GetModule("tls.issuance." + modName)
|
||||
if err != nil {
|
||||
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issuer, ok = unm.(certmagic.Issuer)
|
||||
if !ok {
|
||||
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
|
||||
}
|
||||
|
||||
case "dns":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
provName := h.Val()
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||
}
|
||||
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
|
||||
if err != nil {
|
||||
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
|
||||
}
|
||||
dnsProvModuleInstance := dnsProvModule.New()
|
||||
if unm, ok := dnsProvModuleInstance.(caddyfile.Unmarshaler); ok {
|
||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModuleInstance, "name", provName, h.warnings)
|
||||
|
||||
case "ca_root":
|
||||
arg := h.RemainingArgs()
|
||||
if len(arg) != 1 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0])
|
||||
|
||||
case "on_demand":
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
onDemand = true
|
||||
|
||||
default:
|
||||
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
||||
}
|
||||
}
|
||||
|
||||
// a naked tls directive is not allowed
|
||||
if len(firstLine) == 0 && !hasBlock {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
// begin building the final config values
|
||||
var configVals []ConfigValue
|
||||
|
||||
// certificate loaders
|
||||
if len(fileLoader) > 0 {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_loader",
|
||||
Value: fileLoader,
|
||||
})
|
||||
}
|
||||
if len(folderLoader) > 0 {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_loader",
|
||||
Value: folderLoader,
|
||||
})
|
||||
}
|
||||
|
||||
// issuer
|
||||
if acmeIssuer != nil && internalIssuer != nil {
|
||||
// the logic to support this would be complex
|
||||
return nil, h.Err("cannot use both ACME and internal issuers in same server block")
|
||||
}
|
||||
if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) {
|
||||
// similarly, the logic to support this would be complex
|
||||
return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives")
|
||||
}
|
||||
switch {
|
||||
case issuer != nil:
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_issuer",
|
||||
Value: issuer,
|
||||
})
|
||||
|
||||
case internalIssuer != nil:
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_issuer",
|
||||
Value: internalIssuer,
|
||||
})
|
||||
|
||||
case acmeIssuer != nil:
|
||||
// fill in global defaults, if configured
|
||||
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
|
||||
acmeIssuer.Email = email.(string)
|
||||
}
|
||||
if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
|
||||
acmeIssuer.CA = acmeCA.(string)
|
||||
}
|
||||
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
|
||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
|
||||
}
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_issuer",
|
||||
Value: disambiguateACMEIssuer(acmeIssuer),
|
||||
})
|
||||
}
|
||||
|
||||
// on-demand TLS
|
||||
if onDemand {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.on_demand",
|
||||
Value: true,
|
||||
})
|
||||
}
|
||||
|
||||
// custom certificate selection
|
||||
if len(certSelector.AnyTag) > 0 {
|
||||
cp.CertSelection = &certSelector
|
||||
}
|
||||
|
||||
// connection policy -- always add one, to ensure that TLS
|
||||
// is enabled, because this directive was used (this is
|
||||
// needed, for instance, when a site block has a key of
|
||||
// just ":5000" - i.e. no hostname, and only on-demand TLS
|
||||
// is enabled)
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.connection_policy",
|
||||
Value: cp,
|
||||
})
|
||||
|
||||
return configVals, nil
|
||||
}
|
||||
|
||||
// parseRoot parses the root directive. Syntax:
|
||||
//
|
||||
// root [<matcher>] <path>
|
||||
//
|
||||
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
var root string
|
||||
for h.Next() {
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
root = h.Val()
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
}
|
||||
return caddyhttp.VarsMiddleware{"root": root}, nil
|
||||
}
|
||||
|
||||
// parseRedir parses the redir directive. Syntax:
|
||||
//
|
||||
// redir [<matcher>] <to> [<code>]
|
||||
//
|
||||
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
to := h.Val()
|
||||
|
||||
var code string
|
||||
if h.NextArg() {
|
||||
code = h.Val()
|
||||
}
|
||||
if code == "permanent" {
|
||||
code = "301"
|
||||
}
|
||||
if code == "temporary" || code == "" {
|
||||
code = "302"
|
||||
}
|
||||
var body string
|
||||
if code == "html" {
|
||||
// Script tag comes first since that will better imitate a redirect in the browser's
|
||||
// history, but the meta tag is a fallback for most non-JS clients.
|
||||
const metaRedir = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirecting...</title>
|
||||
<script>window.location.replace("%s");</script>
|
||||
<meta http-equiv="refresh" content="0; URL='%s'">
|
||||
</head>
|
||||
<body>Redirecting to <a href="%s">%s</a>...</body>
|
||||
</html>
|
||||
`
|
||||
safeTo := html.EscapeString(to)
|
||||
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
|
||||
}
|
||||
|
||||
return caddyhttp.StaticResponse{
|
||||
StatusCode: caddyhttp.WeakString(code),
|
||||
Headers: http.Header{"Location": []string{to}},
|
||||
Body: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseRespond parses the respond directive.
|
||||
func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
sr := new(caddyhttp.StaticResponse)
|
||||
err := sr.UnmarshalCaddyfile(h.Dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
// parseRoute parses the route directive.
|
||||
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
sr := new(caddyhttp.Subroute)
|
||||
|
||||
allResults, err := parseSegmentAsConfig(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, result := range allResults {
|
||||
switch handler := result.Value.(type) {
|
||||
case caddyhttp.Route:
|
||||
sr.Routes = append(sr.Routes, handler)
|
||||
case caddyhttp.Subroute:
|
||||
// directives which return a literal subroute instead of a route
|
||||
// means they intend to keep those handlers together without
|
||||
// them being reordered; we're doing that anyway since we're in
|
||||
// the route directive, so just append its handlers
|
||||
sr.Routes = append(sr.Routes, handler.Routes...)
|
||||
default:
|
||||
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
return ParseSegmentAsSubroute(h)
|
||||
}
|
||||
|
||||
func parseHandleErrors(h Helper) ([]ConfigValue, error) {
|
||||
subroute, err := ParseSegmentAsSubroute(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []ConfigValue{
|
||||
{
|
||||
Class: "error_route",
|
||||
Value: subroute,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseLog parses the log directive. Syntax:
|
||||
//
|
||||
// log {
|
||||
// output <writer_module> ...
|
||||
// format <encoder_module> ...
|
||||
// level <level>
|
||||
// }
|
||||
//
|
||||
func parseLog(h Helper) ([]ConfigValue, error) {
|
||||
var configValues []ConfigValue
|
||||
for h.Next() {
|
||||
// log does not currently support any arguments
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
cl := new(caddy.CustomLog)
|
||||
|
||||
for h.NextBlock(0) {
|
||||
switch h.Val() {
|
||||
case "output":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
moduleName := h.Val()
|
||||
|
||||
// can't use the usual caddyfile.Unmarshaler flow with the
|
||||
// standard writers because they are in the caddy package
|
||||
// (because they are the default) and implementing that
|
||||
// interface there would unfortunately create circular import
|
||||
var wo caddy.WriterOpener
|
||||
switch moduleName {
|
||||
case "stdout":
|
||||
wo = caddy.StdoutWriter{}
|
||||
case "stderr":
|
||||
wo = caddy.StderrWriter{}
|
||||
case "discard":
|
||||
wo = caddy.DiscardWriter{}
|
||||
default:
|
||||
mod, err := caddy.GetModule("caddy.logging.writers." + moduleName)
|
||||
if err != nil {
|
||||
return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wo, ok = unm.(caddy.WriterOpener)
|
||||
if !ok {
|
||||
return nil, h.Errf("module %s is not a WriterOpener", mod)
|
||||
}
|
||||
}
|
||||
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
||||
|
||||
case "format":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
moduleName := h.Val()
|
||||
mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
|
||||
if err != nil {
|
||||
return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc, ok := unm.(zapcore.Encoder)
|
||||
if !ok {
|
||||
return nil, h.Errf("module %s is not a zapcore.Encoder", mod)
|
||||
}
|
||||
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
|
||||
|
||||
case "level":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
cl.Level = h.Val()
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
|
||||
}
|
||||
}
|
||||
|
||||
var val namedCustomLog
|
||||
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
||||
logCounter, ok := h.State["logCounter"].(int)
|
||||
if !ok {
|
||||
logCounter = 0
|
||||
}
|
||||
val.name = fmt.Sprintf("log%d", logCounter)
|
||||
cl.Include = []string{"http.log.access." + val.name}
|
||||
val.log = cl
|
||||
logCounter++
|
||||
h.State["logCounter"] = logCounter
|
||||
}
|
||||
configValues = append(configValues, ConfigValue{
|
||||
Class: "custom_log",
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
return configValues, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||
)
|
||||
|
||||
func TestLogDirectiveSyntax(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expectWarn bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
input: `:8080 {
|
||||
log
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
log {
|
||||
output file foo.log
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
log /foo {
|
||||
output file foo.log
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
|
||||
adapter := caddyfile.Adapter{
|
||||
ServerType: ServerType{},
|
||||
}
|
||||
|
||||
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
|
||||
|
||||
if len(warnings) > 0 != tc.expectWarn {
|
||||
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil != tc.expectError {
|
||||
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
// 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 httpcaddyfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
// directiveOrder specifies the order
|
||||
// to apply directives in HTTP routes.
|
||||
//
|
||||
// The root directive goes first in case rewrites or
|
||||
// redirects depend on existence of files, i.e. the
|
||||
// file matcher, which must know the root first.
|
||||
//
|
||||
// The header directive goes second so that headers
|
||||
// can be manipulated before doing redirects.
|
||||
var directiveOrder = []string{
|
||||
"map",
|
||||
"root",
|
||||
|
||||
"header",
|
||||
|
||||
"redir",
|
||||
"rewrite",
|
||||
|
||||
// URI manipulation
|
||||
"uri",
|
||||
"try_files",
|
||||
|
||||
// middleware handlers; some wrap responses
|
||||
"basicauth",
|
||||
"request_header",
|
||||
"encode",
|
||||
"templates",
|
||||
|
||||
// special routing & dispatching directives
|
||||
"handle",
|
||||
"handle_path",
|
||||
"route",
|
||||
"push",
|
||||
|
||||
// handlers that typically respond to requests
|
||||
"respond",
|
||||
"metrics",
|
||||
"reverse_proxy",
|
||||
"php_fastcgi",
|
||||
"file_server",
|
||||
"acme_server",
|
||||
}
|
||||
|
||||
// directiveIsOrdered returns true if dir is
|
||||
// a known, ordered (sorted) directive.
|
||||
func directiveIsOrdered(dir string) bool {
|
||||
for _, d := range directiveOrder {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RegisterDirective registers a unique directive dir with an
|
||||
// associated unmarshaling (setup) function. When directive dir
|
||||
// is encountered in a Caddyfile, setupFunc will be called to
|
||||
// unmarshal its tokens.
|
||||
func RegisterDirective(dir string, setupFunc UnmarshalFunc) {
|
||||
if _, ok := registeredDirectives[dir]; ok {
|
||||
panic("directive " + dir + " already registered")
|
||||
}
|
||||
registeredDirectives[dir] = setupFunc
|
||||
}
|
||||
|
||||
// RegisterHandlerDirective is like RegisterDirective, but for
|
||||
// directives which specifically output only an HTTP handler.
|
||||
// Directives registered with this function will always have
|
||||
// an optional matcher token as the first argument.
|
||||
func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
|
||||
RegisterDirective(dir, func(h Helper) ([]ConfigValue, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
matcherSet, ok, err := h.MatcherToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
// strip matcher token; we don't need to
|
||||
// use the return value here because a
|
||||
// new dispenser should have been made
|
||||
// solely for this directive's tokens,
|
||||
// with no other uses of same slice
|
||||
h.Dispenser.Delete()
|
||||
}
|
||||
|
||||
h.Dispenser.Reset() // pretend this lookahead never happened
|
||||
val, err := setupFunc(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h.NewRoute(matcherSet, val), nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterGlobalOption registers a unique global option opt with
|
||||
// an associated unmarshaling (setup) function. When the global
|
||||
// option opt is encountered in a Caddyfile, setupFunc will be
|
||||
// called to unmarshal its tokens.
|
||||
func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
|
||||
if _, ok := registeredGlobalOptions[opt]; ok {
|
||||
panic("global option " + opt + " already registered")
|
||||
}
|
||||
registeredGlobalOptions[opt] = setupFunc
|
||||
}
|
||||
|
||||
// Helper is a type which helps setup a value from
|
||||
// Caddyfile tokens.
|
||||
type Helper struct {
|
||||
*caddyfile.Dispenser
|
||||
// State stores intermediate variables during caddyfile adaptation.
|
||||
State map[string]interface{}
|
||||
options map[string]interface{}
|
||||
warnings *[]caddyconfig.Warning
|
||||
matcherDefs map[string]caddy.ModuleMap
|
||||
parentBlock caddyfile.ServerBlock
|
||||
groupCounter counter
|
||||
}
|
||||
|
||||
// Option gets the option keyed by name.
|
||||
func (h Helper) Option(name string) interface{} {
|
||||
return h.options[name]
|
||||
}
|
||||
|
||||
// Caddyfiles returns the list of config files from
|
||||
// which tokens in the current server block were loaded.
|
||||
func (h Helper) Caddyfiles() []string {
|
||||
// first obtain set of names of files involved
|
||||
// in this server block, without duplicates
|
||||
files := make(map[string]struct{})
|
||||
for _, segment := range h.parentBlock.Segments {
|
||||
for _, token := range segment {
|
||||
files[token.File] = struct{}{}
|
||||
}
|
||||
}
|
||||
// then convert the set into a slice
|
||||
filesSlice := make([]string, 0, len(files))
|
||||
for file := range files {
|
||||
filesSlice = append(filesSlice, file)
|
||||
}
|
||||
return filesSlice
|
||||
}
|
||||
|
||||
// JSON converts val into JSON. Any errors are added to warnings.
|
||||
func (h Helper) JSON(val interface{}) json.RawMessage {
|
||||
return caddyconfig.JSON(val, h.warnings)
|
||||
}
|
||||
|
||||
// MatcherToken assumes the next argument token is (possibly) a matcher,
|
||||
// and if so, returns the matcher set along with a true value. If the next
|
||||
// token is not a matcher, nil and false is returned. Note that a true
|
||||
// value may be returned with a nil matcher set if it is a catch-all.
|
||||
func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
|
||||
if !h.NextArg() {
|
||||
return nil, false, nil
|
||||
}
|
||||
return matcherSetFromMatcherToken(h.Dispenser.Token(), h.matcherDefs, h.warnings)
|
||||
}
|
||||
|
||||
// ExtractMatcherSet is like MatcherToken, except this is a higher-level
|
||||
// method that returns the matcher set described by the matcher token,
|
||||
// or nil if there is none, and deletes the matcher token from the
|
||||
// dispenser and resets it as if this look-ahead never happened. Useful
|
||||
// when wrapping a route (one or more handlers) in a user-defined matcher.
|
||||
func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
|
||||
matcherSet, hasMatcher, err := h.MatcherToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasMatcher {
|
||||
h.Dispenser.Delete() // strip matcher token
|
||||
}
|
||||
h.Dispenser.Reset() // pretend this lookahead never happened
|
||||
return matcherSet, nil
|
||||
}
|
||||
|
||||
// NewRoute returns config values relevant to creating a new HTTP route.
|
||||
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
||||
handler caddyhttp.MiddlewareHandler) []ConfigValue {
|
||||
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
||||
if err != nil {
|
||||
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
||||
File: h.File(),
|
||||
Line: h.Line(),
|
||||
Message: err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
var matcherSetsRaw []caddy.ModuleMap
|
||||
if matcherSet != nil {
|
||||
matcherSetsRaw = append(matcherSetsRaw, matcherSet)
|
||||
}
|
||||
return []ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: caddyhttp.Route{
|
||||
MatcherSetsRaw: matcherSetsRaw,
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID.Name(), h.warnings)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GroupRoutes adds the routes (caddyhttp.Route type) in vals to the
|
||||
// same group, if there is more than one route in vals.
|
||||
func (h Helper) GroupRoutes(vals []ConfigValue) {
|
||||
// ensure there's at least two routes; group of one is pointless
|
||||
var count int
|
||||
for _, v := range vals {
|
||||
if _, ok := v.Value.(caddyhttp.Route); ok {
|
||||
count++
|
||||
if count > 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if count < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
// now that we know the group will have some effect, do it
|
||||
groupName := h.groupCounter.nextGroup()
|
||||
for i := range vals {
|
||||
if route, ok := vals[i].Value.(caddyhttp.Route); ok {
|
||||
route.Group = groupName
|
||||
vals[i].Value = route
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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}}
|
||||
}
|
||||
|
||||
// ParseSegmentAsSubroute parses the segment such that its subdirectives
|
||||
// are themselves treated as directives, from which a subroute is built
|
||||
// and returned.
|
||||
func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
allResults, err := parseSegmentAsConfig(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildSubroute(allResults, h.groupCounter)
|
||||
}
|
||||
|
||||
// parseSegmentAsConfig parses the segment such that its subdirectives
|
||||
// are themselves treated as directives, including named matcher definitions,
|
||||
// and the raw Config structs are returned.
|
||||
func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
||||
var allResults []ConfigValue
|
||||
|
||||
for h.Next() {
|
||||
// slice the linear list of tokens into top-level segments
|
||||
var segments []caddyfile.Segment
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
segments = append(segments, h.NextSegment())
|
||||
}
|
||||
|
||||
// copy existing matcher definitions so we can augment
|
||||
// new ones that are defined only in this scope
|
||||
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
|
||||
for key, val := range h.matcherDefs {
|
||||
matcherDefs[key] = val
|
||||
}
|
||||
|
||||
// find and extract any embedded matcher definitions in this scope
|
||||
for i, seg := range segments {
|
||||
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
|
||||
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segments = append(segments[:i], segments[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// with matchers ready to go, evaluate each directive's segment
|
||||
for _, seg := range segments {
|
||||
dir := seg.Directive()
|
||||
dirFunc, ok := registeredDirectives[dir]
|
||||
if !ok {
|
||||
return nil, h.Errf("unrecognized directive: %s", dir)
|
||||
}
|
||||
|
||||
subHelper := h
|
||||
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
||||
subHelper.matcherDefs = matcherDefs
|
||||
|
||||
results, err := dirFunc(subHelper)
|
||||
if err != nil {
|
||||
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||
}
|
||||
for _, result := range results {
|
||||
result.directive = dir
|
||||
allResults = append(allResults, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
// ConfigValue represents a value to be added to the final
|
||||
// configuration, or a value to be consulted when building
|
||||
// the final configuration.
|
||||
type ConfigValue struct {
|
||||
// The kind of value this is. As the config is
|
||||
// being built, the adapter will look in the
|
||||
// "pile" for values belonging to a certain
|
||||
// class when it is setting up a certain part
|
||||
// of the config. The associated value will be
|
||||
// type-asserted and placed accordingly.
|
||||
Class string
|
||||
|
||||
// The value to be used when building the config.
|
||||
// Generally its type is associated with the
|
||||
// name of the Class.
|
||||
Value interface{}
|
||||
|
||||
directive string
|
||||
}
|
||||
|
||||
func sortRoutes(routes []ConfigValue) {
|
||||
dirPositions := make(map[string]int)
|
||||
for i, dir := range directiveOrder {
|
||||
dirPositions[dir] = i
|
||||
}
|
||||
|
||||
sort.SliceStable(routes, func(i, j int) bool {
|
||||
// if the directives are different, just use the established directive order
|
||||
iDir, jDir := routes[i].directive, routes[j].directive
|
||||
if iDir != jDir {
|
||||
return dirPositions[iDir] < dirPositions[jDir]
|
||||
}
|
||||
|
||||
// directives are the same; sub-sort by path matcher length if there's
|
||||
// only one matcher set and one path (this is a very common case and
|
||||
// usually -- but not always -- helpful/expected, oh well; user can
|
||||
// always take manual control of order using handler or route blocks)
|
||||
iRoute, ok := routes[i].Value.(caddyhttp.Route)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
jRoute, ok := routes[j].Value.(caddyhttp.Route)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// decode the path matchers, if there is just one of them
|
||||
var iPM, jPM caddyhttp.MatchPath
|
||||
if len(iRoute.MatcherSetsRaw) == 1 {
|
||||
_ = json.Unmarshal(iRoute.MatcherSetsRaw[0]["path"], &iPM)
|
||||
}
|
||||
if len(jRoute.MatcherSetsRaw) == 1 {
|
||||
_ = json.Unmarshal(jRoute.MatcherSetsRaw[0]["path"], &jPM)
|
||||
}
|
||||
|
||||
// sort by longer path (more specific) first; missing path
|
||||
// matchers or multi-matchers are treated as zero-length paths
|
||||
var iPathLen, jPathLen int
|
||||
if len(iPM) > 0 {
|
||||
iPathLen = len(iPM[0])
|
||||
}
|
||||
if len(jPM) > 0 {
|
||||
jPathLen = len(jPM[0])
|
||||
}
|
||||
|
||||
// if both directives have no path matcher, use whichever one
|
||||
// has any kind of matcher defined first.
|
||||
if iPathLen == 0 && jPathLen == 0 {
|
||||
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
||||
}
|
||||
|
||||
// sort with the most-specific (longest) path first
|
||||
return iPathLen > jPathLen
|
||||
})
|
||||
}
|
||||
|
||||
// serverBlock pairs a Caddyfile server block with
|
||||
// a "pile" of config values, keyed by class name,
|
||||
// as well as its parsed keys for convenience.
|
||||
type serverBlock struct {
|
||||
block caddyfile.ServerBlock
|
||||
pile map[string][]ConfigValue // config values obtained from directives
|
||||
keys []Address
|
||||
}
|
||||
|
||||
// hostsFromKeys returns a list of all the non-empty hostnames found in
|
||||
// the keys of the server block sb. If logger mode is false, a key with
|
||||
// an empty hostname portion will return an empty slice, since that
|
||||
// server block is interpreted to effectively match all hosts. An empty
|
||||
// string is never added to the slice.
|
||||
//
|
||||
// If loggerMode is true, then the non-standard ports of keys will be
|
||||
// joined to the hostnames. This is to effectively match the Host
|
||||
// header of requests that come in for that key.
|
||||
//
|
||||
// The resulting slice is not sorted but will never have duplicates.
|
||||
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
||||
// ensure each entry in our list is unique
|
||||
hostMap := make(map[string]struct{})
|
||||
for _, addr := range sb.keys {
|
||||
if addr.Host == "" {
|
||||
if !loggerMode {
|
||||
// server block contains a key like ":443", i.e. the host portion
|
||||
// is empty / catch-all, which means to match all hosts
|
||||
return []string{}
|
||||
}
|
||||
// never append an empty string
|
||||
continue
|
||||
}
|
||||
if loggerMode &&
|
||||
addr.Port != "" &&
|
||||
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort) &&
|
||||
addr.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort) {
|
||||
hostMap[net.JoinHostPort(addr.Host, addr.Port)] = struct{}{}
|
||||
} else {
|
||||
hostMap[addr.Host] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// convert map to slice
|
||||
sblockHosts := make([]string, 0, len(hostMap))
|
||||
for host := range hostMap {
|
||||
sblockHosts = append(sblockHosts, host)
|
||||
}
|
||||
|
||||
return sblockHosts
|
||||
}
|
||||
|
||||
// hasHostCatchAllKey returns true if sb has a key that
|
||||
// omits a host portion, i.e. it "catches all" hosts.
|
||||
func (sb serverBlock) hasHostCatchAllKey() bool {
|
||||
for _, addr := range sb.keys {
|
||||
if addr.Host == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type (
|
||||
// UnmarshalFunc is a function which can unmarshal Caddyfile
|
||||
// tokens into zero or more config values using a Helper type.
|
||||
// These are passed in a call to RegisterDirective.
|
||||
UnmarshalFunc func(h Helper) ([]ConfigValue, error)
|
||||
|
||||
// UnmarshalHandlerFunc is like UnmarshalFunc, except the
|
||||
// output of the unmarshaling is an HTTP handler. This
|
||||
// function does not need to deal with HTTP request matching
|
||||
// which is abstracted away. Since writing HTTP handlers
|
||||
// with Caddyfile support is very common, this is a more
|
||||
// convenient way to add a handler to the chain since a lot
|
||||
// of the details common to HTTP handlers are taken care of
|
||||
// for you. These are passed to a call to
|
||||
// RegisterHandlerDirective.
|
||||
UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
|
||||
// UnmarshalGlobalFunc is a function which can unmarshal Caddyfile
|
||||
// tokens into a global option config value using a Helper type.
|
||||
// These are passed in a call to RegisterGlobalOption.
|
||||
UnmarshalGlobalFunc func(d *caddyfile.Dispenser) (interface{}, error)
|
||||
)
|
||||
|
||||
var registeredDirectives = make(map[string]UnmarshalFunc)
|
||||
|
||||
var registeredGlobalOptions = make(map[string]UnmarshalGlobalFunc)
|
||||
@@ -0,0 +1,94 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostsFromKeys(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
keys []Address
|
||||
expectNormalMode []string
|
||||
expectLoggerMode []string
|
||||
}{
|
||||
{
|
||||
[]Address{
|
||||
{Original: "foo", Host: "foo"},
|
||||
},
|
||||
[]string{"foo"},
|
||||
[]string{"foo"},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: "foo", Host: "foo"},
|
||||
{Original: "bar", Host: "bar"},
|
||||
},
|
||||
[]string{"bar", "foo"},
|
||||
[]string{"bar", "foo"},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: ":2015", Port: "2015"},
|
||||
},
|
||||
[]string{}, []string{},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: ":443", Port: "443"},
|
||||
},
|
||||
[]string{}, []string{},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: "foo", Host: "foo"},
|
||||
{Original: ":2015", Port: "2015"},
|
||||
},
|
||||
[]string{}, []string{"foo"},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: "example.com:2015", Host: "example.com", Port: "2015"},
|
||||
},
|
||||
[]string{"example.com"},
|
||||
[]string{"example.com:2015"},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: "example.com:80", Host: "example.com", Port: "80"},
|
||||
},
|
||||
[]string{"example.com"},
|
||||
[]string{"example.com"},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
|
||||
},
|
||||
[]string{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
[]Address{
|
||||
{Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"},
|
||||
},
|
||||
[]string{"example.com"},
|
||||
[]string{"example.com:2015"},
|
||||
},
|
||||
} {
|
||||
sb := serverBlock{keys: tc.keys}
|
||||
|
||||
// test in normal mode
|
||||
actual := sb.hostsFromKeys(false)
|
||||
sort.Strings(actual)
|
||||
if !reflect.DeepEqual(tc.expectNormalMode, actual) {
|
||||
t.Errorf("Test %d (loggerMode=false): Expected: %v Actual: %v", i, tc.expectNormalMode, actual)
|
||||
}
|
||||
|
||||
// test in logger mode
|
||||
actual = sb.hostsFromKeys(true)
|
||||
sort.Strings(actual)
|
||||
if !reflect.DeepEqual(tc.expectLoggerMode, actual) {
|
||||
t.Errorf("Test %d (loggerMode=true): Expected: %v Actual: %v", i, tc.expectLoggerMode, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func TestMatcherSyntax(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expectWarn bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
input: `http://localhost
|
||||
@debug {
|
||||
query showdebug=1
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `http://localhost
|
||||
@debug {
|
||||
query bad format
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
input: `http://localhost
|
||||
@debug {
|
||||
not {
|
||||
path /somepath*
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `http://localhost
|
||||
@debug {
|
||||
not path /somepath*
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `http://localhost
|
||||
@debug not path /somepath*
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `@matcher {
|
||||
path /matcher-not-allowed/outside-of-site-block/*
|
||||
}
|
||||
http://localhost
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
|
||||
adapter := caddyfile.Adapter{
|
||||
ServerType: ServerType{},
|
||||
}
|
||||
|
||||
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
|
||||
|
||||
if len(warnings) > 0 != tc.expectWarn {
|
||||
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil != tc.expectError {
|
||||
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecificity(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expect int
|
||||
}{
|
||||
{"", 0},
|
||||
{"*", 0},
|
||||
{"*.*", 1},
|
||||
{"{placeholder}", 0},
|
||||
{"/{placeholder}", 1},
|
||||
{"foo", 3},
|
||||
{"example.com", 11},
|
||||
{"a.example.com", 13},
|
||||
{"*.example.com", 12},
|
||||
{"/foo", 4},
|
||||
{"/foo*", 4},
|
||||
{"{placeholder}.example.com", 12},
|
||||
{"{placeholder.example.com", 24},
|
||||
{"}.", 2},
|
||||
{"}{", 2},
|
||||
{"{}", 0},
|
||||
{"{{{}}", 1},
|
||||
} {
|
||||
actual := specificity(tc.input)
|
||||
if actual != tc.expect {
|
||||
t.Errorf("Test %d (%s): Expected %d but got %d", i, tc.input, tc.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalOptions(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expectWarn bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
input: `
|
||||
{
|
||||
email test@example.com
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin off
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin 127.0.0.1:2020
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin {
|
||||
disabled false
|
||||
}
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin {
|
||||
enforce_origin
|
||||
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||
}
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin 127.0.0.1:2020 {
|
||||
enforce_origin
|
||||
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||
}
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin 192.168.1.1:2020 127.0.0.1:2020 {
|
||||
enforce_origin
|
||||
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||
}
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
input: `
|
||||
{
|
||||
admin off {
|
||||
enforce_origin
|
||||
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||
}
|
||||
}
|
||||
:80
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
|
||||
adapter := caddyfile.Adapter{
|
||||
ServerType: ServerType{},
|
||||
}
|
||||
|
||||
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
|
||||
|
||||
if len(warnings) > 0 != tc.expectWarn {
|
||||
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil != tc.expectError {
|
||||
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
// 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 httpcaddyfile
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGlobalOption("debug", parseOptTrue)
|
||||
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
||||
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
||||
RegisterGlobalOption("default_sni", parseOptSingleString)
|
||||
RegisterGlobalOption("order", parseOptOrder)
|
||||
RegisterGlobalOption("experimental_http3", parseOptTrue)
|
||||
RegisterGlobalOption("storage", parseOptStorage)
|
||||
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
||||
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
||||
RegisterGlobalOption("acme_dns", parseOptSingleString)
|
||||
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
|
||||
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
|
||||
RegisterGlobalOption("email", parseOptSingleString)
|
||||
RegisterGlobalOption("admin", parseOptAdmin)
|
||||
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
|
||||
RegisterGlobalOption("local_certs", parseOptTrue)
|
||||
RegisterGlobalOption("key_type", parseOptSingleString)
|
||||
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
||||
}
|
||||
|
||||
func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func parseOptHTTPPort(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
var httpPort int
|
||||
for d.Next() {
|
||||
var httpPortStr string
|
||||
if !d.AllArgs(&httpPortStr) {
|
||||
return 0, d.ArgErr()
|
||||
}
|
||||
var err error
|
||||
httpPort, err = strconv.Atoi(httpPortStr)
|
||||
if err != nil {
|
||||
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
|
||||
}
|
||||
}
|
||||
return httpPort, nil
|
||||
}
|
||||
|
||||
func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
var httpsPort int
|
||||
for d.Next() {
|
||||
var httpsPortStr string
|
||||
if !d.AllArgs(&httpsPortStr) {
|
||||
return 0, d.ArgErr()
|
||||
}
|
||||
var err error
|
||||
httpsPort, err = strconv.Atoi(httpsPortStr)
|
||||
if err != nil {
|
||||
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
|
||||
}
|
||||
}
|
||||
return httpsPort, nil
|
||||
}
|
||||
|
||||
func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
newOrder := directiveOrder
|
||||
|
||||
for d.Next() {
|
||||
// get directive name
|
||||
if !d.Next() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
dirName := d.Val()
|
||||
if _, ok := registeredDirectives[dirName]; !ok {
|
||||
return nil, d.Errf("%s is not a registered directive", dirName)
|
||||
}
|
||||
|
||||
// get positional token
|
||||
if !d.Next() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pos := d.Val()
|
||||
|
||||
// if directive exists, first remove it
|
||||
for i, d := range newOrder {
|
||||
if d == dirName {
|
||||
newOrder = append(newOrder[:i], newOrder[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// act on the positional
|
||||
switch pos {
|
||||
case "first":
|
||||
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() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
|
||||
// insert directive into proper position
|
||||
for i, d := range newOrder {
|
||||
if d == otherDir {
|
||||
if pos == "before" {
|
||||
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
|
||||
}
|
||||
|
||||
func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
if !d.Next() { // consume option name
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
if !d.Next() { // get storage module name
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
modName := d.Val()
|
||||
mod, err := caddy.GetModule("caddy.storage." + modName)
|
||||
if err != nil {
|
||||
return nil, d.Errf("getting storage module '%s': %v", modName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage, ok := unm.(caddy.StorageConverter)
|
||||
if !ok {
|
||||
return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
eab := new(acme.EAB)
|
||||
for d.Next() {
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "key_id":
|
||||
if !d.NextArg() {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
return eab, nil
|
||||
}
|
||||
|
||||
func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
if !d.Next() { // consume option name
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
if !d.Next() { // get issuer module name
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
modName := d.Val()
|
||||
mod, err := caddy.GetModule("tls.issuance." + modName)
|
||||
if err != nil {
|
||||
return nil, d.Errf("getting issuer module '%s': %v", modName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iss, ok := unm.(certmagic.Issuer)
|
||||
if !ok {
|
||||
return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
|
||||
}
|
||||
return iss, nil
|
||||
}
|
||||
|
||||
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
d.Next() // consume parameter name
|
||||
if !d.Next() {
|
||||
return "", d.ArgErr()
|
||||
}
|
||||
val := d.Val()
|
||||
if d.Next() {
|
||||
return "", d.ArgErr()
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
adminCfg := new(caddy.AdminConfig)
|
||||
for d.Next() {
|
||||
if d.NextArg() {
|
||||
listenAddress := d.Val()
|
||||
if listenAddress == "off" {
|
||||
adminCfg.Disabled = true
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
adminCfg.Listen = listenAddress
|
||||
if d.NextArg() { // At most 1 arg is allowed
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "enforce_origin":
|
||||
adminCfg.EnforceOrigin = true
|
||||
|
||||
case "origins":
|
||||
adminCfg.Origins = d.RemainingArgs()
|
||||
|
||||
default:
|
||||
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
if adminCfg.Listen == "" && !adminCfg.Disabled {
|
||||
adminCfg.Listen = caddy.DefaultAdminListen
|
||||
}
|
||||
return adminCfg, nil
|
||||
}
|
||||
|
||||
func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
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":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(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.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 {
|
||||
return nil, d.Err("expected at least one config parameter for on_demand_tls")
|
||||
}
|
||||
return ond, nil
|
||||
}
|
||||
|
||||
func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
d.Next() // consume parameter name
|
||||
if !d.Next() {
|
||||
return "", d.ArgErr()
|
||||
}
|
||||
val := d.Val()
|
||||
if d.Next() {
|
||||
return "", d.ArgErr()
|
||||
}
|
||||
if val != "off" && val != "disable_redirects" {
|
||||
return "", d.Errf("auto_https must be either 'off' or 'disable_redirects'")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
// 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 httpcaddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
func (st ServerType) buildTLSApp(
|
||||
pairings []sbAddrAssociation,
|
||||
options map[string]interface{},
|
||||
warnings []caddyconfig.Warning,
|
||||
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
||||
|
||||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||
var certLoaders []caddytls.CertificateLoader
|
||||
|
||||
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
||||
if hsp, ok := options["https_port"].(int); ok {
|
||||
httpsPort = strconv.Itoa(hsp)
|
||||
}
|
||||
|
||||
// count how many server blocks have a TLS-enabled key with
|
||||
// no host, and find all hosts that share a server block with
|
||||
// a hostless key, so that they don't get forgotten/omitted
|
||||
// by auto-HTTPS (since they won't appear in route matchers)
|
||||
var serverBlocksWithTLSHostlessKey int
|
||||
hostsSharedWithHostlessKey := make(map[string]struct{})
|
||||
for _, pair := range pairings {
|
||||
for _, sb := range pair.serverBlocks {
|
||||
for _, addr := range sb.keys {
|
||||
if addr.Host == "" {
|
||||
// this address has no hostname, but if it's explicitly set
|
||||
// to HTTPS, then we need to count it as being TLS-enabled
|
||||
if addr.Scheme == "https" || addr.Port == httpsPort {
|
||||
serverBlocksWithTLSHostlessKey++
|
||||
}
|
||||
// this server block has a hostless key, now
|
||||
// go through and add all the hosts to the set
|
||||
for _, otherAddr := range sb.keys {
|
||||
if otherAddr.Original == addr.Original {
|
||||
continue
|
||||
}
|
||||
if otherAddr.Host != "" {
|
||||
hostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for _, p := range pairings {
|
||||
for _, sblock := range p.serverBlocks {
|
||||
// get values that populate an automation policy for this block
|
||||
var ap *caddytls.AutomationPolicy
|
||||
|
||||
sblockHosts := sblock.hostsFromKeys(false)
|
||||
if len(sblockHosts) == 0 {
|
||||
ap = catchAllAP
|
||||
}
|
||||
|
||||
// on-demand tls
|
||||
if _, ok := sblock.pile["tls.on_demand"]; ok {
|
||||
if ap == nil {
|
||||
var err error
|
||||
ap, err = newBaseAutomationPolicy(options, warnings, true)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
}
|
||||
ap.OnDemand = true
|
||||
}
|
||||
|
||||
// certificate issuers
|
||||
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
||||
for _, issuerVal := range issuerVals {
|
||||
issuer := issuerVal.Value.(certmagic.Issuer)
|
||||
if ap == nil {
|
||||
var err error
|
||||
ap, err = newBaseAutomationPolicy(options, warnings, true)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
}
|
||||
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuer, issuer) {
|
||||
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.Issuer, issuer)
|
||||
}
|
||||
ap.Issuer = issuer
|
||||
}
|
||||
}
|
||||
|
||||
// custom bind host
|
||||
for _, cfgVal := range sblock.pile["bind"] {
|
||||
// either an existing issuer is already configured (and thus, ap is not
|
||||
// nil), or we need to configure an issuer, so we need ap to be non-nil
|
||||
if ap == nil {
|
||||
ap, err = newBaseAutomationPolicy(options, warnings, true)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
}
|
||||
|
||||
// if an issuer was already configured and it is NOT an ACME
|
||||
// issuer, skip, since we intend to adjust only ACME issuers
|
||||
var acmeIssuer *caddytls.ACMEIssuer
|
||||
if ap.Issuer != nil {
|
||||
// ensure we include any issuer that embeds/wraps an underlying ACME issuer
|
||||
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
|
||||
if acmeWrapper, ok := ap.Issuer.(acmeCapable); ok {
|
||||
acmeIssuer = acmeWrapper.GetACMEIssuer()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// proceed to configure the ACME issuer's bind host, without
|
||||
// overwriting any existing settings
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
if acmeIssuer.Challenges.BindHost == "" {
|
||||
// only binding to one host is supported
|
||||
var bindHost string
|
||||
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
|
||||
bindHost = bindHosts[0]
|
||||
}
|
||||
acmeIssuer.Challenges.BindHost = bindHost
|
||||
}
|
||||
ap.Issuer = acmeIssuer // we'll encode it later
|
||||
}
|
||||
|
||||
if ap != nil {
|
||||
if ap.Issuer != nil {
|
||||
// encode issuer now that it's all set up
|
||||
issuerName := ap.Issuer.(caddy.Module).CaddyModule().ID.Name()
|
||||
ap.IssuerRaw = caddyconfig.JSONModuleObject(ap.Issuer, "module", issuerName, &warnings)
|
||||
}
|
||||
|
||||
// first make sure this block is allowed to create an automation policy;
|
||||
// doing so is 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
|
||||
// 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
|
||||
// host filter, which is indistinguishable between the two server blocks
|
||||
// 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
|
||||
// the least-leaky abstraction I could figure out
|
||||
if len(sblockHosts) == 0 {
|
||||
if serverBlocksWithTLSHostlessKey > 1 {
|
||||
// this server block and at least one other has a key with no host,
|
||||
// making the two indistinguishable; it is misleading to define such
|
||||
// a policy within one server block since it actually will apply to
|
||||
// others as well
|
||||
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")
|
||||
}
|
||||
if catchAllAP == nil {
|
||||
// this server block has a key with no hosts, but there is not yet
|
||||
// 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,
|
||||
// unless, of course, the server block has a key with no hosts, in which
|
||||
// case its automation policy becomes or blends with the default/global
|
||||
// automation policy because, of necessity, it applies to all hostnames
|
||||
// (i.e. it has no Subjects filter) -- in that case, we'll append it last
|
||||
if ap != catchAllAP {
|
||||
ap.Subjects = sblockHosts
|
||||
|
||||
// if a combination of public and internal names were given
|
||||
// for this same server block and no issuer was specified, we
|
||||
// need to separate them out in the automation policies so
|
||||
// that the internal names can use the internal issuer and
|
||||
// the other names can use the default/public/ACME issuer
|
||||
var ap2 *caddytls.AutomationPolicy
|
||||
if ap.Issuer == nil {
|
||||
var internal, external []string
|
||||
for _, s := range ap.Subjects {
|
||||
if certmagic.SubjectQualifiesForPublicCert(s) {
|
||||
external = append(external, s)
|
||||
} else {
|
||||
internal = append(internal, s)
|
||||
}
|
||||
}
|
||||
if len(external) > 0 && len(internal) > 0 {
|
||||
ap.Subjects = external
|
||||
apCopy := *ap
|
||||
ap2 = &apCopy
|
||||
ap2.Subjects = internal
|
||||
ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
|
||||
}
|
||||
}
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
|
||||
if ap2 != nil {
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// certificate loaders
|
||||
if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
|
||||
for _, clVal := range clVals {
|
||||
certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// group certificate loaders by module name, then add to config
|
||||
if len(certLoaders) > 0 {
|
||||
loadersByName := make(map[string]caddytls.CertificateLoader)
|
||||
for _, cl := range certLoaders {
|
||||
name := caddy.GetModuleName(cl)
|
||||
// ugh... technically, we may have multiple FileLoader and FolderLoader
|
||||
// modules (because the tls directive returns one per occurrence), but
|
||||
// the config structure expects only one instance of each kind of loader
|
||||
// module, so we have to combine them... instead of enumerating each
|
||||
// possible cert loader module in a type switch, we can use reflection,
|
||||
// which works on any cert loaders that are slice types
|
||||
if reflect.TypeOf(cl).Kind() == reflect.Slice {
|
||||
combined := reflect.ValueOf(loadersByName[name])
|
||||
if !combined.IsValid() {
|
||||
combined = reflect.New(reflect.TypeOf(cl)).Elem()
|
||||
}
|
||||
clVal := reflect.ValueOf(cl)
|
||||
for i := 0; i < clVal.Len(); i++ {
|
||||
combined = reflect.Append(combined, clVal.Index(i))
|
||||
}
|
||||
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
|
||||
}
|
||||
}
|
||||
for certLoaderName, loaders := range loadersByName {
|
||||
tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
|
||||
}
|
||||
}
|
||||
|
||||
// set any of the on-demand options, for if/when on-demand TLS is enabled
|
||||
if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok {
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.OnDemand = onDemand
|
||||
}
|
||||
|
||||
// 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
|
||||
// hostless key matches all hosts, therefore, it wouldn't be
|
||||
// considered for auto-HTTPS, so we need to make sure those hosts
|
||||
// are manually considered for managed certificates; we also need
|
||||
// to make sure that any of these names which are internal-only
|
||||
// get internal certificates by default rather than ACME
|
||||
var al caddytls.AutomateLoader
|
||||
internalAP := &caddytls.AutomationPolicy{
|
||||
IssuerRaw: json.RawMessage(`{"module":"internal"}`),
|
||||
}
|
||||
for h := range hostsSharedWithHostlessKey {
|
||||
al = append(al, h)
|
||||
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
||||
internalAP.Subjects = append(internalAP.Subjects, h)
|
||||
}
|
||||
}
|
||||
if len(al) > 0 {
|
||||
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
|
||||
}
|
||||
if len(internalAP.Subjects) > 0 {
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
|
||||
}
|
||||
|
||||
// if there is a global/catch-all automation policy, ensure it goes last
|
||||
if catchAllAP != nil {
|
||||
// first, encode its issuer, if there is one
|
||||
if catchAllAP.Issuer != nil {
|
||||
issuerName := catchAllAP.Issuer.(caddy.Module).CaddyModule().ID.Name()
|
||||
catchAllAP.IssuerRaw = caddyconfig.JSONModuleObject(catchAllAP.Issuer, "module", issuerName, &warnings)
|
||||
}
|
||||
|
||||
// then append it to the end of the policies list
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
||||
}
|
||||
|
||||
// do a little verification & cleanup
|
||||
if tlsApp.Automation != nil {
|
||||
// consolidate automation policies that are the exact same
|
||||
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
|
||||
|
||||
// ensure automation policies don't overlap subjects (this should be
|
||||
// an error at provision-time as well, but catch it in the adapt phase
|
||||
// for convenience)
|
||||
automationHostSet := make(map[string]struct{})
|
||||
for _, ap := range tlsApp.Automation.Policies {
|
||||
for _, s := range ap.Subjects {
|
||||
if _, ok := automationHostSet[s]; ok {
|
||||
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
|
||||
}
|
||||
automationHostSet[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tlsApp, warnings, nil
|
||||
}
|
||||
|
||||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||
// its values from the global options map. It should be used as the base
|
||||
// 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
|
||||
// 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) {
|
||||
issuer, hasIssuer := options["cert_issuer"]
|
||||
|
||||
acmeCA, hasACMECA := options["acme_ca"]
|
||||
acmeCARoot, hasACMECARoot := options["acme_ca_root"]
|
||||
acmeDNS, hasACMEDNS := options["acme_dns"]
|
||||
acmeEAB, hasACMEEAB := options["acme_eab"]
|
||||
|
||||
email, hasEmail := options["email"]
|
||||
localCerts, hasLocalCerts := options["local_certs"]
|
||||
keyType, hasKeyType := options["key_type"]
|
||||
|
||||
hasGlobalAutomationOpts := hasIssuer || hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType
|
||||
|
||||
// if there are no global options related to automation policies
|
||||
// set, then we can just return right away
|
||||
if !hasGlobalAutomationOpts {
|
||||
if always {
|
||||
return new(caddytls.AutomationPolicy), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ap := new(caddytls.AutomationPolicy)
|
||||
if keyType != nil {
|
||||
ap.KeyType = keyType.(string)
|
||||
}
|
||||
|
||||
if hasIssuer {
|
||||
if hasACMECA || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts {
|
||||
return nil, fmt.Errorf("global options are ambiguous: cert_issuer is confusing when combined with acme_*, email, or local_certs options")
|
||||
}
|
||||
ap.Issuer = issuer.(certmagic.Issuer)
|
||||
} else if localCerts != nil {
|
||||
// internal issuer enabled trumps any ACME configurations; useful in testing
|
||||
ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later
|
||||
} else {
|
||||
if acmeCA == nil {
|
||||
acmeCA = ""
|
||||
}
|
||||
if email == nil {
|
||||
email = ""
|
||||
}
|
||||
mgr := &caddytls.ACMEIssuer{
|
||||
CA: acmeCA.(string),
|
||||
Email: email.(string),
|
||||
}
|
||||
if acmeDNS != nil {
|
||||
provName := acmeDNS.(string)
|
||||
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
|
||||
}
|
||||
mgr.Challenges = &caddytls.ChallengesConfig{
|
||||
DNS: &caddytls.DNSChallengeConfig{
|
||||
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
|
||||
},
|
||||
}
|
||||
}
|
||||
if acmeCARoot != nil {
|
||||
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
|
||||
}
|
||||
if acmeEAB != nil {
|
||||
mgr.ExternalAccount = acmeEAB.(*acme.EAB)
|
||||
}
|
||||
ap.Issuer = disambiguateACMEIssuer(mgr) // we'll encode it later
|
||||
}
|
||||
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
// disambiguateACMEIssuer returns an issuer based on the properties of acmeIssuer.
|
||||
// If acmeIssuer implicitly configures a certain kind of ACMEIssuer (for example,
|
||||
// ZeroSSL), the proper wrapper over acmeIssuer will be returned instead.
|
||||
func disambiguateACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) certmagic.Issuer {
|
||||
// as a special case, we integrate with ZeroSSL's ACME endpoint if it looks like an
|
||||
// implicit ZeroSSL configuration (this requires a wrapper type over ACMEIssuer
|
||||
// because of the EAB generation; if EAB is provided, we can use plain ACMEIssuer)
|
||||
if strings.Contains(acmeIssuer.CA, "acme.zerossl.com") && acmeIssuer.ExternalAccount == nil {
|
||||
return &caddytls.ZeroSSLIssuer{ACMEIssuer: acmeIssuer}
|
||||
}
|
||||
return acmeIssuer
|
||||
}
|
||||
|
||||
// consolidateAutomationPolicies combines automation policies that are the same,
|
||||
// for a cleaner overall output.
|
||||
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
|
||||
for i := 0; i < len(aps); i++ {
|
||||
for j := 0; j < len(aps); j++ {
|
||||
if j == i {
|
||||
continue
|
||||
}
|
||||
|
||||
// if they're exactly equal in every way, just keep one of them
|
||||
if reflect.DeepEqual(aps[i], aps[j]) {
|
||||
aps = append(aps[:j], aps[j+1:]...)
|
||||
i--
|
||||
break
|
||||
}
|
||||
|
||||
// if the policy is the same, we can keep just one, but we have
|
||||
// to be careful which one we keep; if only one has any hostnames
|
||||
// defined, then we need to keep the one without any hostnames,
|
||||
// otherwise the one without any subjects (a catch-all) would be
|
||||
// eaten up by the one with subjects; and if both have subjects, we
|
||||
// need to combine their lists
|
||||
if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
|
||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||
aps[i].MustStaple == aps[j].MustStaple &&
|
||||
aps[i].KeyType == aps[j].KeyType &&
|
||||
aps[i].OnDemand == aps[j].OnDemand &&
|
||||
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
|
||||
if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 {
|
||||
aps = append(aps[:j], aps[j+1:]...)
|
||||
} else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
|
||||
aps = append(aps[:i], aps[i+1:]...)
|
||||
} else {
|
||||
// avoid repeated subjects
|
||||
for _, subj := range aps[j].Subjects {
|
||||
if !sliceContains(aps[i].Subjects, subj) {
|
||||
aps[i].Subjects = append(aps[i].Subjects, subj)
|
||||
}
|
||||
}
|
||||
aps = append(aps[:j], aps[j+1:]...)
|
||||
}
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure any catch-all policies go last
|
||||
sort.SliceStable(aps, func(i, j int) bool {
|
||||
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
||||
})
|
||||
|
||||
return aps
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(adminLoad{})
|
||||
}
|
||||
|
||||
// adminLoad is a module that provides the /load endpoint
|
||||
// for the Caddy admin API. The only reason it's not baked
|
||||
// into the caddy package directly is because of the import
|
||||
// of the caddyconfig package for its GetAdapter function.
|
||||
// If the caddy package depends on the caddyconfig package,
|
||||
// then the caddyconfig package will not be able to import
|
||||
// the caddy package, and it can more easily cause backward
|
||||
// edges in the dependency tree (i.e. import cycle).
|
||||
// Fortunately, the admin API has first-class support for
|
||||
// adding endpoints from modules.
|
||||
type adminLoad struct{}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (adminLoad) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "admin.api.load",
|
||||
New: func() caddy.Module { return new(adminLoad) },
|
||||
}
|
||||
}
|
||||
|
||||
// Routes returns a route for the /load endpoint.
|
||||
func (al adminLoad) Routes() []caddy.AdminRoute {
|
||||
return []caddy.AdminRoute{
|
||||
{
|
||||
Pattern: "/load",
|
||||
Handler: caddy.AdminHandlerFunc(al.handleLoad),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// handleLoad replaces the entire current configuration with
|
||||
// a new one provided in the response body. It supports config
|
||||
// adapters through the use of the Content-Type header. A
|
||||
// config that is identical to the currently-running config
|
||||
// will be a no-op unless Cache-Control: must-revalidate is set.
|
||||
func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodPost {
|
||||
return caddy.APIError{
|
||||
Code: 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{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("reading request body: %v", err),
|
||||
}
|
||||
}
|
||||
body := buf.Bytes()
|
||||
|
||||
// if the config is formatted other than Caddy's native
|
||||
// JSON, we need to adapt it before loading it
|
||||
if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
|
||||
ct, _, err := mime.ParseMediaType(ctHeader)
|
||||
if err != nil {
|
||||
return caddy.APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("invalid Content-Type: %v", err),
|
||||
}
|
||||
}
|
||||
if !strings.HasSuffix(ct, "/json") {
|
||||
slashIdx := strings.Index(ct, "/")
|
||||
if slashIdx < 0 {
|
||||
return caddy.APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("malformed Content-Type"),
|
||||
}
|
||||
}
|
||||
adapterName := ct[slashIdx+1:]
|
||||
cfgAdapter := GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
return caddy.APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
|
||||
}
|
||||
}
|
||||
result, warnings, err := cfgAdapter.Adapt(body, nil)
|
||||
if err != nil {
|
||||
return caddy.APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
|
||||
}
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
respBody, err := json.Marshal(warnings)
|
||||
if err != nil {
|
||||
caddy.Log().Named("admin.api.load").Error(err.Error())
|
||||
}
|
||||
_, _ = w.Write(respBody)
|
||||
}
|
||||
body = result
|
||||
}
|
||||
}
|
||||
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err = caddy.Load(body, forceReload)
|
||||
if err != nil {
|
||||
return caddy.APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("loading config: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
caddy.Log().Named("admin.api").Info("load complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure and has
|
||||
// some really convenient methods.
|
||||
type Dispenser struct {
|
||||
filename string
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
|
||||
func NewDispenser(filename string, input io.Reader) Dispenser {
|
||||
tokens, _ := allTokens(input) // ignoring error because nothing to do with it
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDispenserTokens returns a Dispenser filled with the given tokens.
|
||||
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line. Returns true if a token was loaded; false
|
||||
// otherwise. If false, all tokens on the line have
|
||||
// been consumed. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block. It returns true if a token was
|
||||
// loaded, or false when the block's closing curly brace
|
||||
// was loaded and thus the block ended. Nested blocks are
|
||||
// not supported.
|
||||
func (d *Dispenser) NextBlock() bool {
|
||||
if d.nesting > 0 {
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
d.nesting--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !d.NextArg() { // block must open on same line
|
||||
return false
|
||||
}
|
||||
if d.Val() != "{" {
|
||||
d.cursor-- // roll back if not opening brace
|
||||
return false
|
||||
}
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
// Open and then closed right away
|
||||
return false
|
||||
}
|
||||
d.nesting++
|
||||
return true
|
||||
}
|
||||
|
||||
// Val gets the text of the current token. If there is no token
|
||||
// loaded, it returns empty string.
|
||||
func (d *Dispenser) Val() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token. If there is no token
|
||||
// loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].Line
|
||||
}
|
||||
|
||||
// File gets the filename of the current token. If there is no token loaded,
|
||||
// it returns the filename originally given when parsing started.
|
||||
func (d *Dispenser) File() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return d.filename
|
||||
}
|
||||
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
|
||||
return tokenFilename
|
||||
}
|
||||
return d.filename
|
||||
}
|
||||
|
||||
// Args is a convenience function that loads the next arguments
|
||||
// (tokens on the same line) into an arbitrary number of strings
|
||||
// pointed to in targets. If there are fewer tokens available
|
||||
// than string pointers, the remaining strings will not be changed
|
||||
// and false will be returned. If there were enough tokens available
|
||||
// to fill the arguments, then true will be returned.
|
||||
func (d *Dispenser) Args(targets ...*string) bool {
|
||||
enough := true
|
||||
for i := 0; i < len(targets); i++ {
|
||||
if !d.NextArg() {
|
||||
enough = false
|
||||
break
|
||||
}
|
||||
*targets[i] = d.Val()
|
||||
}
|
||||
return enough
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
d.cursor--
|
||||
break
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ArgErr returns an argument error, meaning that another
|
||||
// argument was expected but not found. In other words,
|
||||
// a line break or open curly brace was encountered instead of
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const filename = "Caddyfile"
|
||||
|
||||
// ToJSON converts caddyfile to its JSON representation.
|
||||
func ToJSON(caddyfile []byte) ([]byte, error) {
|
||||
var j EncodedCaddyfile
|
||||
|
||||
serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
block := EncodedServerBlock{
|
||||
Keys: sb.Keys,
|
||||
Body: [][]interface{}{},
|
||||
}
|
||||
|
||||
// Extract directives deterministically by sorting them
|
||||
var directives = make([]string, len(sb.Tokens))
|
||||
for dir := range sb.Tokens {
|
||||
directives = append(directives, dir)
|
||||
}
|
||||
sort.Strings(directives)
|
||||
|
||||
// Convert each directive's tokens into our JSON structure
|
||||
for _, dir := range directives {
|
||||
disp := NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
for disp.Next() {
|
||||
block.Body = append(block.Body, constructLine(&disp))
|
||||
}
|
||||
}
|
||||
|
||||
// tack this block onto the end of the list
|
||||
j = append(j, block)
|
||||
}
|
||||
|
||||
result, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// constructLine transforms tokens into a JSON-encodable structure;
|
||||
// but only one line at a time, to be used at the top-level of
|
||||
// a server block only (where the first token on each line is a
|
||||
// directive) - not to be used at any other nesting level.
|
||||
func constructLine(d *Dispenser) []interface{} {
|
||||
var args []interface{}
|
||||
|
||||
args = append(args, d.Val())
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
args = append(args, constructBlock(d))
|
||||
continue
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// constructBlock recursively processes tokens into a
|
||||
// JSON-encodable structure. To be used in a directive's
|
||||
// block. Goes to end of block.
|
||||
func constructBlock(d *Dispenser) [][]interface{} {
|
||||
block := [][]interface{}{}
|
||||
|
||||
for d.Next() {
|
||||
if d.Val() == "}" {
|
||||
break
|
||||
}
|
||||
block = append(block, constructLine(d))
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
|
||||
func FromJSON(jsonBytes []byte) ([]byte, error) {
|
||||
var j EncodedCaddyfile
|
||||
var result string
|
||||
|
||||
err := json.Unmarshal(jsonBytes, &j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for sbPos, sb := range j {
|
||||
if sbPos > 0 {
|
||||
result += "\n\n"
|
||||
}
|
||||
for i, key := range sb.Keys {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
//result += standardizeScheme(key)
|
||||
result += key
|
||||
}
|
||||
result += jsonToText(sb.Body, 1)
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
// jsonToText recursively transforms a scope of JSON into plain
|
||||
// Caddyfile text.
|
||||
func jsonToText(scope interface{}, depth int) string {
|
||||
var result string
|
||||
|
||||
switch val := scope.(type) {
|
||||
case string:
|
||||
if strings.ContainsAny(val, "\" \n\t\r") {
|
||||
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
|
||||
} else {
|
||||
result += val
|
||||
}
|
||||
case int:
|
||||
result += strconv.Itoa(val)
|
||||
case float64:
|
||||
result += fmt.Sprintf("%v", val)
|
||||
case bool:
|
||||
result += fmt.Sprintf("%t", val)
|
||||
case [][]interface{}:
|
||||
result += " {\n"
|
||||
for _, arg := range val {
|
||||
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
case []interface{}:
|
||||
for i, v := range val {
|
||||
if block, ok := v.([]interface{}); ok {
|
||||
result += "{\n"
|
||||
for _, arg := range block {
|
||||
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
continue
|
||||
}
|
||||
result += jsonToText(v, depth)
|
||||
if i < len(val)-1 {
|
||||
result += " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO: Will this function come in handy somewhere else?
|
||||
/*
|
||||
// standardizeScheme turns an address like host:https into https://host,
|
||||
// or "host:" into "host".
|
||||
func standardizeScheme(addr string) string {
|
||||
if hostname, port, err := net.SplitHostPort(addr); err == nil {
|
||||
if port == "http" || port == "https" {
|
||||
addr = port + "://" + hostname
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(addr, ":")
|
||||
}
|
||||
*/
|
||||
|
||||
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
|
||||
type EncodedCaddyfile []EncodedServerBlock
|
||||
|
||||
// EncodedServerBlock represents a server block ripe for encoding.
|
||||
type EncodedServerBlock struct {
|
||||
Keys []string `json:"keys"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 "testing"
|
||||
|
||||
var tests = []struct {
|
||||
caddyfile, json string
|
||||
}{
|
||||
{ // 0
|
||||
caddyfile: `foo {
|
||||
root /bar
|
||||
}`,
|
||||
json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
|
||||
},
|
||||
{ // 1
|
||||
caddyfile: `host1, host2 {
|
||||
dir {
|
||||
def
|
||||
}
|
||||
}`,
|
||||
json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
|
||||
},
|
||||
{ // 2
|
||||
caddyfile: `host1, host2 {
|
||||
dir abc {
|
||||
def ghi
|
||||
jkl
|
||||
}
|
||||
}`,
|
||||
json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
|
||||
},
|
||||
{ // 3
|
||||
caddyfile: `host1:1234, host2:5678 {
|
||||
dir abc {
|
||||
}
|
||||
}`,
|
||||
json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
|
||||
},
|
||||
{ // 4
|
||||
caddyfile: `host {
|
||||
foo "bar baz"
|
||||
}`,
|
||||
json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
|
||||
},
|
||||
{ // 5
|
||||
caddyfile: `host, host:80 {
|
||||
foo "bar \"baz\""
|
||||
}`,
|
||||
json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
|
||||
},
|
||||
{ // 6
|
||||
caddyfile: `host {
|
||||
foo "bar
|
||||
baz"
|
||||
}`,
|
||||
json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`,
|
||||
},
|
||||
{ // 7
|
||||
caddyfile: `host {
|
||||
dir 123 4.56 true
|
||||
}`,
|
||||
json: `[{"keys":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
},
|
||||
{ // 8
|
||||
caddyfile: `http://host, https://host {
|
||||
}`,
|
||||
json: `[{"keys":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
},
|
||||
{ // 9
|
||||
caddyfile: `host {
|
||||
dir1 a b
|
||||
dir2 c d
|
||||
}`,
|
||||
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
|
||||
},
|
||||
{ // 10
|
||||
caddyfile: `host {
|
||||
dir a b
|
||||
dir c d
|
||||
}`,
|
||||
json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
|
||||
},
|
||||
{ // 11
|
||||
caddyfile: `host {
|
||||
dir1 a b
|
||||
dir2 {
|
||||
c
|
||||
d
|
||||
}
|
||||
}`,
|
||||
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
|
||||
},
|
||||
{ // 12
|
||||
caddyfile: `host1 {
|
||||
dir1
|
||||
}
|
||||
|
||||
host2 {
|
||||
dir2
|
||||
}`,
|
||||
json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
output, err := ToJSON([]byte(test.caddyfile))
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
if string(output) != test.json {
|
||||
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromJSON(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
output, err := FromJSON([]byte(test.json))
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
if string(output) != test.caddyfile {
|
||||
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Will these tests come in handy somewhere else?
|
||||
/*
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
// host:https should be converted to https://host
|
||||
output, err := ToJSON([]byte(`host:https`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
|
||||
output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := "https://host {\n}", string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
|
||||
// host: should be converted to just host
|
||||
output, err = ToJSON([]byte(`host:`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := "host {\n}", string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,209 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 basicauth implements HTTP Basic Authentication for Caddy.
|
||||
//
|
||||
// This is useful for simple protections on a website, like requiring
|
||||
// a password to access an admin interface. This package assumes a
|
||||
// fairly small threat model.
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
)
|
||||
|
||||
// BasicAuth is middleware to protect resources with a username and password.
|
||||
// Note that HTTP Basic Authentication is not secure by itself and should
|
||||
// not be used to protect important assets without HTTPS. Even then, the
|
||||
// security of HTTP Basic Auth is disputed. Use discretion when deciding
|
||||
// what to protect with BasicAuth.
|
||||
type BasicAuth struct {
|
||||
Next httpserver.Handler
|
||||
SiteRoot string
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var protected, isAuthenticated bool
|
||||
var realm string
|
||||
var username string
|
||||
var password string
|
||||
var ok bool
|
||||
|
||||
// do not check for basic auth on OPTIONS call
|
||||
if r.Method == http.MethodOptions {
|
||||
// Pass-through when no paths match
|
||||
return a.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
for _, rule := range a.Rules {
|
||||
for _, res := range rule.Resources {
|
||||
if !httpserver.Path(r.URL.Path).Matches(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
// path matches; this endpoint is protected
|
||||
protected = true
|
||||
realm = rule.Realm
|
||||
|
||||
// parse auth header
|
||||
username, password, ok = r.BasicAuth()
|
||||
|
||||
// check credentials
|
||||
if !ok ||
|
||||
username != rule.Username ||
|
||||
!rule.Password(password) {
|
||||
continue
|
||||
}
|
||||
|
||||
// by this point, authentication was successful
|
||||
isAuthenticated = true
|
||||
|
||||
// let upstream middleware (e.g. fastcgi and cgi) know about authenticated
|
||||
// user; this replaces the request with a wrapped instance
|
||||
r = r.WithContext(context.WithValue(r.Context(),
|
||||
httpserver.RemoteUserCtxKey, username))
|
||||
|
||||
// Provide username to be used in log by replacer
|
||||
repl := httpserver.NewReplacer(r, nil, "-")
|
||||
repl.Set("user", username)
|
||||
}
|
||||
}
|
||||
|
||||
if protected && !isAuthenticated {
|
||||
// browsers show a message that says something like:
|
||||
// "The website says: <realm>"
|
||||
// which is kinda dumb, but whatever.
|
||||
if realm == "" {
|
||||
realm = "Restricted"
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
|
||||
|
||||
// Get a replacer so we can provide basic info for the authentication error.
|
||||
repl := httpserver.NewReplacer(r, nil, "-")
|
||||
repl.Set("user", username)
|
||||
errstr := repl.Replace("BasicAuth: user \"{user}\" was not found or password was incorrect. {remote} {host} {uri} {proto}")
|
||||
err := fmt.Errorf("%s", errstr)
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
|
||||
// Pass-through when no paths match
|
||||
return a.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Rule represents a BasicAuth rule. A username and password
|
||||
// combination protect the associated resources, which are
|
||||
// file or directory paths.
|
||||
type Rule struct {
|
||||
Username string
|
||||
Password func(string) bool
|
||||
Resources []string
|
||||
Realm string // See RFC 1945 and RFC 2617, default: "Restricted"
|
||||
}
|
||||
|
||||
// PasswordMatcher determines whether a password matches a rule.
|
||||
type PasswordMatcher func(pw string) bool
|
||||
|
||||
var (
|
||||
htpasswords map[string]map[string]PasswordMatcher
|
||||
htpasswordsMu sync.Mutex
|
||||
)
|
||||
|
||||
// GetHtpasswdMatcher matches password rules.
|
||||
func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) {
|
||||
filename = filepath.Join(siteRoot, filename)
|
||||
htpasswordsMu.Lock()
|
||||
if htpasswords == nil {
|
||||
htpasswords = make(map[string]map[string]PasswordMatcher)
|
||||
}
|
||||
pm := htpasswords[filename]
|
||||
if pm == nil {
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %q: %v", filename, err)
|
||||
}
|
||||
defer fh.Close()
|
||||
pm = make(map[string]PasswordMatcher)
|
||||
if err = parseHtpasswd(pm, fh); err != nil {
|
||||
return nil, fmt.Errorf("parsing htpasswd %q: %v", fh.Name(), err)
|
||||
}
|
||||
htpasswords[filename] = pm
|
||||
}
|
||||
htpasswordsMu.Unlock()
|
||||
if pm[username] == nil {
|
||||
return nil, fmt.Errorf("username %q not found in %q", username, filename)
|
||||
}
|
||||
return pm[username], nil
|
||||
}
|
||||
|
||||
func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.IndexByte(line, '#') == 0 {
|
||||
continue
|
||||
}
|
||||
i := strings.IndexByte(line, ':')
|
||||
if i <= 0 {
|
||||
return fmt.Errorf("malformed line, no color: %q", line)
|
||||
}
|
||||
user, encoded := line[:i], line[i+1:]
|
||||
for _, p := range basic.DefaultSystems {
|
||||
matcher, err := p(encoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matcher != nil {
|
||||
pm[user] = matcher.MatchesPassword
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// PlainMatcher returns a PasswordMatcher that does a constant-time
|
||||
// byte comparison against the password passw.
|
||||
func PlainMatcher(passw string) PasswordMatcher {
|
||||
// compare hashes of equal length instead of actual password
|
||||
// to avoid leaking password length
|
||||
passwHash := sha1.New()
|
||||
if _, err := passwHash.Write([]byte(passw)); err != nil {
|
||||
log.Printf("[ERROR] unable to write password hash: %v", err)
|
||||
}
|
||||
passwSum := passwHash.Sum(nil)
|
||||
return func(pw string) bool {
|
||||
pwHash := sha1.New()
|
||||
if _, err := pwHash.Write([]byte(pw)); err != nil {
|
||||
log.Printf("[ERROR] unable to write password hash: %v", err)
|
||||
}
|
||||
pwSum := pwHash.Sum(nil)
|
||||
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 basicauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
var i int
|
||||
// This handler is registered for tests in which the only authorized user is
|
||||
// "okuser"
|
||||
upstreamHandler := func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
|
||||
if remoteUser != "okuser" {
|
||||
t.Errorf("Test %d: expecting remote user 'okuser', got '%s'", i, remoteUser)
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
rws := []BasicAuth{
|
||||
{
|
||||
Next: httpserver.HandlerFunc(upstreamHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "okuser", Password: PlainMatcher("okpass"),
|
||||
Resources: []string{"/testing"}, Realm: "Resources"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Next: httpserver.HandlerFunc(upstreamHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "okuser", Password: PlainMatcher("okpass"),
|
||||
Resources: []string{"/testing"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type testType struct {
|
||||
from string
|
||||
result int
|
||||
user string
|
||||
password string
|
||||
haserror bool
|
||||
}
|
||||
|
||||
tests := []testType{
|
||||
{"/testing", http.StatusOK, "okuser", "okpass", false},
|
||||
{"/testing", http.StatusUnauthorized, "baduser", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "badpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS", true},
|
||||
{"/testing", http.StatusUnauthorized, "", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "", true},
|
||||
{"/testing", http.StatusUnauthorized, "", "", true},
|
||||
}
|
||||
|
||||
var test testType
|
||||
for _, rw := range rws {
|
||||
expectRealm := rw.Rules[0].Realm
|
||||
if expectRealm == "" {
|
||||
expectRealm = "Restricted" // Default if Realm not specified in rule
|
||||
}
|
||||
for i, test = range tests {
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
|
||||
}
|
||||
req.SetBasicAuth(test.user, test.password)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
|
||||
}
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected status code %d but was %d",
|
||||
i, test.result, result)
|
||||
}
|
||||
if test.result == http.StatusUnauthorized {
|
||||
headers := rec.Header()
|
||||
if val, ok := headers["Www-Authenticate"]; ok {
|
||||
if got, want := val[0], "Basic realm=\""+expectRealm+"\""; got != want {
|
||||
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
|
||||
}
|
||||
} else {
|
||||
if req.Header.Get("Authorization") == "" {
|
||||
// see issue #1508: https://github.com/caddyserver/caddy/issues/1508
|
||||
t.Errorf("Test %d: Expected Authorization header to be retained after successful auth, but was empty", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleOverlappingRules(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
|
||||
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
haserror bool
|
||||
}{
|
||||
{"/t", http.StatusOK, "t:p1", false},
|
||||
{"/t/t", http.StatusOK, "t:p1", false},
|
||||
{"/t/t", http.StatusOK, "t1:p2", false},
|
||||
{"/a", http.StatusOK, "t1:p2", false},
|
||||
{"/t/t", http.StatusUnauthorized, "t1:p3", true},
|
||||
{"/t", http.StatusUnauthorized, "t1:p2", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
|
||||
}
|
||||
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.cred))
|
||||
req.Header.Set("Authorization", auth)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
}
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
|
||||
i, test.result, result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprintf(w, r.URL.String())
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func TestHtpasswd(t *testing.T) {
|
||||
htpasswdPasswd := "IedFOuGmTpT8"
|
||||
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
htfh, err := ioutil.TempFile("", "basicauth-")
|
||||
if err != nil {
|
||||
t.Skip("Error creating temp file, will skip htpassword test")
|
||||
return
|
||||
}
|
||||
defer os.Remove(htfh.Name())
|
||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||
}
|
||||
htfh.Close()
|
||||
|
||||
for i, username := range []string{"sha1", "md5"} {
|
||||
rule := Rule{Username: username, Resources: []string{"/testing"}}
|
||||
|
||||
siteRoot := filepath.Dir(htfh.Name())
|
||||
filename := filepath.Base(htfh.Name())
|
||||
if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil {
|
||||
t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
|
||||
}
|
||||
t.Logf("%d. username=%q", i, rule.Username)
|
||||
if !rule.Password(htpasswdPasswd) || rule.Password(htpasswdPasswd+"!") {
|
||||
t.Errorf("%d (%s) password does not match.", i, rule.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsMethod(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "username", Password: PlainMatcher("password"), Resources: []string{"/testing"}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// add basic auth with invalid username
|
||||
// and password to make sure basic auth is ignored
|
||||
req.SetBasicAuth("invaliduser", "invalidpassword")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not ServeHTTP: %v", err)
|
||||
}
|
||||
if result != http.StatusOK {
|
||||
t.Errorf("Expected status code %d but was %d", http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 basicauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("basicauth", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new BasicAuth middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
cfg := httpserver.GetConfig(c)
|
||||
root := cfg.Root
|
||||
|
||||
rules, err := basicAuthParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
basic := BasicAuth{Rules: rules}
|
||||
|
||||
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
basic.Next = next
|
||||
basic.SiteRoot = root
|
||||
return basic
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
var err error
|
||||
for c.Next() {
|
||||
var rule Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule.Username = args[0]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
case 3:
|
||||
rule.Resources = append(rule.Resources, args[0])
|
||||
rule.Username = args[1]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
default:
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
// If nested block is present, process it here
|
||||
for c.NextBlock() {
|
||||
val := c.Val()
|
||||
args = c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// Assume single argument is path resource
|
||||
rule.Resources = append(rule.Resources, val)
|
||||
case 1:
|
||||
if val == "realm" {
|
||||
if rule.Realm == "" {
|
||||
rule.Realm = strings.Replace(args[0], `"`, `\"`, -1)
|
||||
} else {
|
||||
return rules, c.Errf("\"realm\" subdirective can only be specified once")
|
||||
}
|
||||
} else {
|
||||
return rules, c.Errf("expecting \"realm\", got \"%s\"", val)
|
||||
}
|
||||
default:
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
|
||||
htpasswdPrefix := "htpasswd="
|
||||
if !strings.HasPrefix(passw, htpasswdPrefix) {
|
||||
return PlainMatcher(passw), nil
|
||||
}
|
||||
return GetHtpasswdMatcher(passw[len(htpasswdPrefix):], username, siteRoot)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 basicauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `basicauth user pwd`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(BasicAuth)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthParse(t *testing.T) {
|
||||
htpasswdPasswd := "IedFOuGmTpT8"
|
||||
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
var skipHtpassword bool
|
||||
htfh, err := ioutil.TempFile(".", "basicauth-")
|
||||
if err != nil {
|
||||
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
||||
skipHtpassword = true
|
||||
} else {
|
||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||
}
|
||||
htfh.Close()
|
||||
defer os.Remove(htfh.Name())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
password string
|
||||
expected []Rule
|
||||
}{
|
||||
{`basicauth user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth /resource1 user pwd {
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1"}},
|
||||
}},
|
||||
{`basicauth /resource1 user pwd {
|
||||
realm Resources
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1"}, Realm: "Resources"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
realm "Secure resources"
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}, Realm: "Secure resources"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
realm "Secure resources"
|
||||
realm Extra
|
||||
/resource2
|
||||
}`, true, "pwd", []Rule{}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
foo "Resources"
|
||||
/resource2
|
||||
}`, true, "pwd", []Rule{}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
{`basicauth /res1 user1 pwd1
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []Rule{
|
||||
{Username: "user1", Resources: []string{"/res1"}},
|
||||
{Username: "user2", Resources: []string{"/res2"}},
|
||||
}},
|
||||
{`basicauth user`, true, "", []Rule{}},
|
||||
{`basicauth`, true, "", []Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []Rule{}},
|
||||
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
|
||||
{Username: "sha1"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actual, err := basicAuthParse(caddy.NewTestController("http", test.input))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Username != expectedRule.Username {
|
||||
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
|
||||
i, j, expectedRule.Username, actualRule.Username)
|
||||
}
|
||||
|
||||
if actualRule.Realm != expectedRule.Realm {
|
||||
t.Errorf("Test %d, rule %d: Expected realm '%s', got '%s'",
|
||||
i, j, expectedRule.Realm, actualRule.Realm)
|
||||
}
|
||||
|
||||
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
|
||||
continue
|
||||
}
|
||||
pwd := test.password
|
||||
if len(actual) > 1 {
|
||||
pwd = fmt.Sprintf("%s%d", pwd, j+1)
|
||||
}
|
||||
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
|
||||
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
|
||||
i, j, test.password, actualRule.Password(""))
|
||||
}
|
||||
|
||||
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
|
||||
actualRes := fmt.Sprintf("%v", actualRule.Resources)
|
||||
if actualRes != expectedRes {
|
||||
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
|
||||
i, j, expectedRes, actualRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 bind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetupBind(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `bind 1.2.3.4`)
|
||||
err := setupBind(c)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
if got, want := cfg.TLS.Manager.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 browse provides middleware for listing files in a directory
|
||||
// when directory path is requested instead of a specific file.
|
||||
package browse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
const (
|
||||
sortByName = "name"
|
||||
sortByNameDirFirst = "namedirfirst"
|
||||
sortBySize = "size"
|
||||
sortByTime = "time"
|
||||
)
|
||||
|
||||
// Browse is an http.Handler that can show a file listing when
|
||||
// directories in the given paths are specified.
|
||||
type Browse struct {
|
||||
Next httpserver.Handler
|
||||
Configs []Config
|
||||
IgnoreIndexes bool
|
||||
}
|
||||
|
||||
// Config is a configuration for browsing in a particular path.
|
||||
type Config struct {
|
||||
PathScope string // the base path the URL must match to enable browsing
|
||||
Fs staticfiles.FileServer
|
||||
Variables interface{}
|
||||
Template *template.Template
|
||||
}
|
||||
|
||||
// A Listing is the context used to fill out a template.
|
||||
type Listing struct {
|
||||
// The name of the directory (the last element of the path).
|
||||
Name string
|
||||
|
||||
// The full path of the request.
|
||||
Path string
|
||||
|
||||
// Whether the parent directory is browse-able.
|
||||
CanGoUp bool
|
||||
|
||||
// The items (files and folders) in the path.
|
||||
Items []FileInfo
|
||||
|
||||
// The number of directories in the listing.
|
||||
NumDirs int
|
||||
|
||||
// The number of files (items that aren't directories) in the listing.
|
||||
NumFiles int
|
||||
|
||||
// Which sorting order is used.
|
||||
Sort string
|
||||
|
||||
// And which order.
|
||||
Order string
|
||||
|
||||
// If ≠0 then Items have been limited to that many elements.
|
||||
ItemsLimitedTo int
|
||||
|
||||
// Optional custom variables for use in browse templates.
|
||||
User interface{}
|
||||
|
||||
httpserver.Context
|
||||
}
|
||||
|
||||
// Crumb represents part of a breadcrumb menu.
|
||||
type Crumb struct {
|
||||
Link, Text string
|
||||
}
|
||||
|
||||
// Breadcrumbs returns l.Path where every element maps
|
||||
// the link to the text to display.
|
||||
func (l Listing) Breadcrumbs() []Crumb {
|
||||
var result []Crumb
|
||||
|
||||
if len(l.Path) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// skip trailing slash
|
||||
lpath := l.Path
|
||||
if lpath[len(lpath)-1] == '/' {
|
||||
lpath = lpath[:len(lpath)-1]
|
||||
}
|
||||
|
||||
parts := strings.Split(lpath, "/")
|
||||
for i := range parts {
|
||||
txt := parts[i]
|
||||
if i == 0 && parts[i] == "" {
|
||||
txt = "/"
|
||||
}
|
||||
result = append(result, Crumb{Link: strings.Repeat("../", len(parts)-i-1), Text: txt})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FileInfo is the info about a particular file or directory
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Size int64
|
||||
URL string
|
||||
ModTime time.Time
|
||||
Mode os.FileMode
|
||||
IsDir bool
|
||||
IsSymlink bool
|
||||
}
|
||||
|
||||
// HumanSize returns the size of the file as a human-readable string
|
||||
// in IEC format (i.e. power of 2 or base 1024).
|
||||
func (fi FileInfo) HumanSize() string {
|
||||
return humanize.IBytes(uint64(fi.Size))
|
||||
}
|
||||
|
||||
// HumanModTime returns the modified time of the file as a human-readable string.
|
||||
func (fi FileInfo) HumanModTime(format string) string {
|
||||
return fi.ModTime.Format(format)
|
||||
}
|
||||
|
||||
// Implement sorting for Listing
|
||||
type byName Listing
|
||||
type byNameDirFirst Listing
|
||||
type bySize Listing
|
||||
type byTime Listing
|
||||
|
||||
// By Name
|
||||
func (l byName) Len() int { return len(l.Items) }
|
||||
func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
|
||||
// Treat upper and lower case equally
|
||||
func (l byName) Less(i, j int) bool {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
// By Name Dir First
|
||||
func (l byNameDirFirst) Len() int { return len(l.Items) }
|
||||
func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
|
||||
// Treat upper and lower case equally
|
||||
func (l byNameDirFirst) Less(i, j int) bool {
|
||||
|
||||
// if both are dir or file sort normally
|
||||
if l.Items[i].IsDir == l.Items[j].IsDir {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
// always sort dir ahead of file
|
||||
return l.Items[i].IsDir
|
||||
}
|
||||
|
||||
// By Size
|
||||
func (l bySize) Len() int { return len(l.Items) }
|
||||
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
|
||||
const directoryOffset = -1 << 31 // = math.MinInt32
|
||||
func (l bySize) Less(i, j int) bool {
|
||||
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
||||
|
||||
// Directory sizes depend on the filesystem implementation,
|
||||
// which is opaque to a visitor, and should indeed does not change if the operator chooses to change the fs.
|
||||
// For a consistent user experience directories are pulled to the front…
|
||||
if l.Items[i].IsDir {
|
||||
iSize = directoryOffset
|
||||
}
|
||||
if l.Items[j].IsDir {
|
||||
jSize = directoryOffset
|
||||
}
|
||||
// … and sorted by name.
|
||||
if l.Items[i].IsDir && l.Items[j].IsDir {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
return iSize < jSize
|
||||
}
|
||||
|
||||
// By Time
|
||||
func (l byTime) Len() int { return len(l.Items) }
|
||||
func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
|
||||
|
||||
// Add sorting method to "Listing"
|
||||
// it will apply what's in ".Sort" and ".Order"
|
||||
func (l Listing) applySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
if l.Order == "desc" {
|
||||
switch l.Sort {
|
||||
case sortByName:
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case sortByNameDirFirst:
|
||||
sort.Sort(sort.Reverse(byNameDirFirst(l)))
|
||||
case sortBySize:
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case sortByTime:
|
||||
sort.Sort(sort.Reverse(byTime(l)))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
} else { // If we had more Orderings we could add them here
|
||||
switch l.Sort {
|
||||
case sortByName:
|
||||
sort.Sort(byName(l))
|
||||
case sortByNameDirFirst:
|
||||
sort.Sort(byNameDirFirst(l))
|
||||
case sortBySize:
|
||||
sort.Sort(bySize(l))
|
||||
case sortByTime:
|
||||
sort.Sort(byTime(l))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config *Config) (Listing, bool) {
|
||||
var (
|
||||
fileInfos []FileInfo
|
||||
dirCount, fileCount int
|
||||
hasIndexFile bool
|
||||
)
|
||||
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
for _, indexName := range config.Fs.IndexPages {
|
||||
if name == indexName {
|
||||
hasIndexFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath, config)
|
||||
|
||||
if isDir {
|
||||
name += "/"
|
||||
dirCount++
|
||||
} else {
|
||||
fileCount++
|
||||
}
|
||||
|
||||
if config.Fs.IsHidden(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
|
||||
fileInfos = append(fileInfos, FileInfo{
|
||||
IsDir: isDir,
|
||||
IsSymlink: isSymlink(f),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: u.String(),
|
||||
ModTime: f.ModTime().UTC(),
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
}
|
||||
|
||||
return Listing{
|
||||
Name: path.Base(urlPath),
|
||||
Path: urlPath,
|
||||
CanGoUp: canGoUp,
|
||||
Items: fileInfos,
|
||||
NumDirs: dirCount,
|
||||
NumFiles: fileCount,
|
||||
}, hasIndexFile
|
||||
}
|
||||
|
||||
// isSymlink return true if f is a symbolic link
|
||||
func isSymlink(f os.FileInfo) bool {
|
||||
return f.Mode()&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
// isSymlinkTargetDir return true if f's symbolic link target
|
||||
// is a directory. Return false if not a symbolic link.
|
||||
func isSymlinkTargetDir(f os.FileInfo, urlPath string, config *Config) bool {
|
||||
if !isSymlink(f) {
|
||||
return false
|
||||
}
|
||||
|
||||
// a bit strange, but we want Stat thru the jailed filesystem to be safe
|
||||
target, err := config.Fs.Root.Open(path.Join(urlPath, f.Name()))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer target.Close()
|
||||
targetInfo, err := target.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return targetInfo.IsDir()
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
// If so, control is handed over to ServeListing.
|
||||
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// See if there's a browse configuration to match the path
|
||||
var bc *Config
|
||||
for i := range b.Configs {
|
||||
if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
|
||||
bc = &b.Configs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if bc == nil {
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Browse works on existing directories; delegate everything else
|
||||
requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path)
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusNotFound, err
|
||||
default:
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
defer requestedFilepath.Close()
|
||||
|
||||
info, err := requestedFilepath.Stat()
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone, err
|
||||
default:
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Do not reply to anything else because it might be nonsensical
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
// proceed, noop
|
||||
case "PROPFIND", http.MethodOptions:
|
||||
return http.StatusNotImplemented, nil
|
||||
default:
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Browsing navigation gets messed up if browsing a directory
|
||||
// that doesn't end in "/" (which it should, anyway)
|
||||
u := *r.URL
|
||||
if u.Path == "" {
|
||||
u.Path = "/"
|
||||
}
|
||||
if u.Path[len(u.Path)-1] != '/' {
|
||||
u.Path += "/"
|
||||
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
|
||||
return http.StatusMovedPermanently, nil
|
||||
}
|
||||
|
||||
return b.ServeListing(w, r, requestedFilepath, bc)
|
||||
}
|
||||
|
||||
func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) {
|
||||
files, err := requestedFilepath.Readdir(-1)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Determine if user can browse up another folder
|
||||
var canGoUp bool
|
||||
curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
|
||||
for _, other := range b.Configs {
|
||||
if strings.HasPrefix(curPathDir, other.PathScope) {
|
||||
canGoUp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble listing of directory contents
|
||||
listing, hasIndex := directoryListing(files, canGoUp, urlPath, config)
|
||||
|
||||
return &listing, hasIndex, nil
|
||||
}
|
||||
|
||||
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||
// and reads 'limit' if given. The latter is 0 if not given.
|
||||
//
|
||||
// This sets Cookies.
|
||||
func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
|
||||
sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
|
||||
|
||||
// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
|
||||
switch sort {
|
||||
case "":
|
||||
sort = sortByNameDirFirst
|
||||
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
||||
sort = sortCookie.Value
|
||||
}
|
||||
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
|
||||
}
|
||||
|
||||
switch order {
|
||||
case "":
|
||||
order = "asc"
|
||||
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
||||
order = orderCookie.Value
|
||||
}
|
||||
case "asc", "desc":
|
||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
|
||||
}
|
||||
|
||||
if limitQuery != "" {
|
||||
limit, err = strconv.Atoi(limitQuery)
|
||||
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ServeListing returns a formatted view of 'requestedFilepath' contents'.
|
||||
func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
|
||||
listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc)
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone, err
|
||||
default:
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
listing.Context = httpserver.Context{
|
||||
Root: bc.Fs.Root,
|
||||
Req: r,
|
||||
URL: r.URL,
|
||||
}
|
||||
listing.User = bc.Variables
|
||||
|
||||
// Copy the query values into the Listing struct
|
||||
var limit int
|
||||
listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
listing.applySort()
|
||||
|
||||
if limit > 0 && limit <= len(listing.Items) {
|
||||
listing.Items = listing.Items[:limit]
|
||||
listing.ItemsLimitedTo = limit
|
||||
}
|
||||
|
||||
var buf *bytes.Buffer
|
||||
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
|
||||
switch {
|
||||
case strings.Contains(acceptHeader, "application/json"):
|
||||
if buf, err = b.formatAsJSON(listing, bc); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
default: // There's no 'application/json' in the 'Accept' header; browse normally
|
||||
if buf, err = b.formatAsHTML(listing, bc); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(w)
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||
marsh, err := json.Marshal(listing.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = buf.Write(marsh)
|
||||
return buf, err
|
||||
}
|
||||
|
||||
func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := bc.Template.Execute(buf, listing)
|
||||
return buf, err
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 browse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
const testDirPrefix = "caddy_browse_test"
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
// making up []fileInfo with bogus values;
|
||||
// to be used to make up our "listing"
|
||||
fileInfos := []FileInfo{
|
||||
{
|
||||
Name: "fizz",
|
||||
Size: 4,
|
||||
ModTime: time.Now().AddDate(-1, 1, 0),
|
||||
},
|
||||
{
|
||||
Name: "buzz",
|
||||
Size: 2,
|
||||
ModTime: time.Now().AddDate(0, -3, 3),
|
||||
},
|
||||
{
|
||||
Name: "bazz",
|
||||
Size: 1,
|
||||
ModTime: time.Now().AddDate(0, -2, -23),
|
||||
},
|
||||
{
|
||||
Name: "jazz",
|
||||
Size: 3,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
}
|
||||
listing := Listing{
|
||||
Name: "foobar",
|
||||
Path: "/fizz/buzz",
|
||||
CanGoUp: false,
|
||||
Items: fileInfos,
|
||||
}
|
||||
|
||||
// sort by name
|
||||
listing.Sort = "name"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(byName(listing)) {
|
||||
t.Errorf("The listing isn't name sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// sort by size
|
||||
listing.Sort = "size"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(bySize(listing)) {
|
||||
t.Errorf("The listing isn't size sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// sort by Time
|
||||
listing.Sort = "time"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(byTime(listing)) {
|
||||
t.Errorf("The listing isn't time sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// sort by name dir first
|
||||
listing.Sort = "namedirfirst"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(byNameDirFirst(listing)) {
|
||||
t.Errorf("The listing isn't namedirfirst sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by name
|
||||
listing.Sort = "name"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(byName(listing)) {
|
||||
t.Errorf("The listing isn't reversed by name: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by size
|
||||
listing.Sort = "size"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(bySize(listing)) {
|
||||
t.Errorf("The listing isn't reversed by size: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by time
|
||||
listing.Sort = "time"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(byTime(listing)) {
|
||||
t.Errorf("The listing isn't reversed by time: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by name dir first
|
||||
listing.Sort = "namedirfirst"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(byNameDirFirst(listing)) {
|
||||
t.Errorf("The listing isn't reversed by namedirfirst: %v", listing.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseHTTPMethods(t *testing.T) {
|
||||
tmpl, err := template.ParseFiles("testdata/photos.tpl")
|
||||
if err != nil {
|
||||
t.Fatalf("An error occurred while parsing the template: %v", err)
|
||||
}
|
||||
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos",
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir("./testdata"),
|
||||
},
|
||||
Template: tmpl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
for method, expected := range map[string]int{
|
||||
http.MethodGet: http.StatusOK,
|
||||
http.MethodHead: http.StatusOK,
|
||||
http.MethodOptions: http.StatusNotImplemented,
|
||||
"PROPFIND": http.StatusNotImplemented,
|
||||
} {
|
||||
req, err := http.NewRequest(method, "/photos/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test: Could not create HTTP request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
code, _ := b.ServeHTTP(rec, req)
|
||||
if code != expected {
|
||||
t.Errorf("Wrong status with HTTP Method %s: expected %d, got %d", method, expected, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseTemplate(t *testing.T) {
|
||||
tmpl, err := template.ParseFiles("testdata/photos.tpl")
|
||||
if err != nil {
|
||||
t.Fatalf("An error occurred while parsing the template: %v", err)
|
||||
}
|
||||
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos",
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir("./testdata"),
|
||||
Hide: []string{"photos/hidden.html"},
|
||||
},
|
||||
Template: tmpl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/photos/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test: Could not create HTTP request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
code, _ := b.ServeHTTP(rec, req)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
|
||||
}
|
||||
|
||||
respBody := rec.Body.String()
|
||||
expectedBody := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Template</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Header</h1>
|
||||
|
||||
<h1>/photos/</h1>
|
||||
|
||||
<a href="./test1/">test1</a><br>
|
||||
|
||||
<a href="./test.html">test.html</a><br>
|
||||
|
||||
<a href="./test2.html">test2.html</a><br>
|
||||
|
||||
<a href="./test3.html">test3.html</a><br>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
if respBody != expectedBody {
|
||||
t.Fatalf("Expected body: '%v' got: '%v'", expectedBody, respBody)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBrowseJson(t *testing.T) {
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called: %s", r.URL)
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos/",
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir("./testdata"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
|
||||
testDataPath := filepath.Join("./testdata", "photos")
|
||||
file, err := os.Open(testDataPath)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
t.Fatalf("Os Permission Error")
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
files, err := file.Readdir(-1)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to Read Contents of the directory")
|
||||
}
|
||||
var fileinfos []FileInfo
|
||||
|
||||
for i, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
// Tests fail in CI environment because all file mod times are the same for
|
||||
// some reason, making the sorting unpredictable. To hack around this,
|
||||
// we ensure here that each file has a different mod time.
|
||||
chTime := f.ModTime().UTC().Add(-(time.Duration(i) * time.Second))
|
||||
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
|
||||
url := url.URL{Path: "./" + name}
|
||||
|
||||
fileinfos = append(fileinfos, FileInfo{
|
||||
IsDir: f.IsDir(),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: url.String(),
|
||||
ModTime: chTime,
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
}
|
||||
|
||||
// Test that sort=name returns correct listing.
|
||||
|
||||
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
|
||||
|
||||
tests := []struct {
|
||||
QueryURL string
|
||||
SortBy string
|
||||
OrderBy string
|
||||
Limit int
|
||||
shouldErr bool
|
||||
expectedResult []FileInfo
|
||||
}{
|
||||
//test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
|
||||
//without the limit query entire listing will be produced
|
||||
{"/?sort=name", "", "", -1, false, listing.Items},
|
||||
//test case 2: limit is set to 1, orderBy and sortBy is default
|
||||
{"/?limit=1&sort=name", "", "", 1, false, listing.Items[:1]},
|
||||
//test case 3 : if the listing request is bigger than total size of listing then it should return everything
|
||||
{"/?limit=100000000&sort=name", "", "", 100000000, false, listing.Items},
|
||||
//test case 4 : testing for negative limit
|
||||
{"/?limit=-1&sort=name", "", "", -1, false, listing.Items},
|
||||
//test case 5 : testing with limit set to -1 and order set to descending
|
||||
{"/?limit=-1&order=desc&sort=name", "", "desc", -1, false, listing.Items},
|
||||
//test case 6 : testing with limit set to 2 and order set to descending
|
||||
{"/?limit=2&order=desc&sort=name", "", "desc", 2, false, listing.Items},
|
||||
//test case 7 : testing with limit set to 3 and order set to descending
|
||||
{"/?limit=3&order=desc&sort=name", "", "desc", 3, false, listing.Items},
|
||||
//test case 8 : testing with limit set to 3 and order set to ascending
|
||||
{"/?limit=3&order=asc&sort=name", "", "asc", 3, false, listing.Items},
|
||||
//test case 9 : testing with limit set to 1111111 and order set to ascending
|
||||
{"/?limit=1111111&order=asc&sort=name", "", "asc", 1111111, false, listing.Items},
|
||||
//test case 10 : testing with limit set to default and order set to ascending and sorting by size
|
||||
{"/?order=asc&sort=size&sort=name", "size", "asc", -1, false, listing.Items},
|
||||
//test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&sort=name", "time", "asc", -1, false, listing.Items},
|
||||
//test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&limit=1&sort=name", "time", "asc", 1, false, listing.Items},
|
||||
//test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&limit=-100&sort=name", "time", "asc", -100, false, listing.Items},
|
||||
//test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
|
||||
{"/?order=asc&sort=size&limit=-100&sort=name", "size", "asc", -100, false, listing.Items},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var marsh []byte
|
||||
req, err := http.NewRequest("GET", "/photos"+test.QueryURL, nil)
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored when making request, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
code, err := b.ServeHTTP(rec, req)
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("In test %d: Wrong status, expected %d, got %d", i, http.StatusOK, code)
|
||||
}
|
||||
if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
|
||||
t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
|
||||
actualJSONResponse := rec.Body.String()
|
||||
copyOfListing := listing
|
||||
if test.SortBy == "" {
|
||||
copyOfListing.Sort = "name"
|
||||
} else {
|
||||
copyOfListing.Sort = test.SortBy
|
||||
}
|
||||
if test.OrderBy == "" {
|
||||
copyOfListing.Order = "asc"
|
||||
} else {
|
||||
copyOfListing.Order = test.OrderBy
|
||||
}
|
||||
|
||||
copyOfListing.applySort()
|
||||
|
||||
limit := test.Limit
|
||||
if limit <= len(copyOfListing.Items) && limit > 0 {
|
||||
marsh, err = json.Marshal(copyOfListing.Items[:limit])
|
||||
} else { // if the 'limit' query is empty, or has the wrong value, list everything
|
||||
marsh, err = json.Marshal(copyOfListing.Items)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to Marshal the listing ")
|
||||
}
|
||||
expectedJSON := string(marsh)
|
||||
|
||||
if actualJSONResponse != expectedJSON {
|
||||
t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n",
|
||||
i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "sort" package has "IsSorted" function, but no "IsReversed";
|
||||
func isReversed(data sort.Interface) bool {
|
||||
n := data.Len()
|
||||
for i := n - 1; i > 0; i-- {
|
||||
if !data.Less(i, i-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestBrowseRedirect(t *testing.T) {
|
||||
testCases := []struct {
|
||||
url string
|
||||
statusCode int
|
||||
returnCode int
|
||||
location string
|
||||
}{
|
||||
{
|
||||
"http://www.example.com/photos",
|
||||
http.StatusMovedPermanently,
|
||||
http.StatusMovedPermanently,
|
||||
"http://www.example.com/photos/",
|
||||
},
|
||||
{
|
||||
"/photos",
|
||||
http.StatusMovedPermanently,
|
||||
http.StatusMovedPermanently,
|
||||
"/photos/",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Test %d - Next shouldn't be called", i)
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos",
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir("./testdata"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", tc.url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
returnCode, _ := b.ServeHTTP(rec, req)
|
||||
if returnCode != tc.returnCode {
|
||||
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
|
||||
i, tc.returnCode, returnCode)
|
||||
}
|
||||
|
||||
if got := rec.Code; got != tc.statusCode {
|
||||
t.Errorf("Test %d - wrong status, expected %d, got %d",
|
||||
i, tc.statusCode, got)
|
||||
}
|
||||
|
||||
if got := rec.Header().Get("Location"); got != tc.location {
|
||||
t.Errorf("Test %d - wrong Location header, expected %s, got %s",
|
||||
i, tc.location, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows support for symlinks is limited, and we had a hard time getting
|
||||
// all these tests to pass with the permissions of CI; so just skip them
|
||||
fmt.Println("Skipping browse symlink tests on Windows...")
|
||||
return
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
source string
|
||||
target string
|
||||
pathScope string
|
||||
url string
|
||||
expectedName string
|
||||
expectedURL string
|
||||
}{
|
||||
// test case can expect a directory "dir" and a symlink to it called "symlink"
|
||||
|
||||
{"dir", "$TMP/rel_symlink_to_dir", "/", "/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/abs_symlink_to_dir", "/", "/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"../../dir", "$TMP/sub/dir/rel_symlink_to_dir", "/", "/sub/dir/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/sub/dir/abs_symlink_to_dir", "/", "/sub/dir/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"../../dir", "$TMP/with/scope/rel_symlink_to_dir", "/with/scope", "/with/scope/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/with/scope/abs_symlink_to_dir", "/with/scope", "/with/scope/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"../../../../dir", "$TMP/with/scope/sub/dir/rel_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/with/scope/sub/dir/abs_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"symlink", "$TMP/rel_symlink_to_symlink", "/", "/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/abs_symlink_to_symlink", "/", "/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
|
||||
{"../../symlink", "$TMP/sub/dir/rel_symlink_to_symlink", "/", "/sub/dir/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/sub/dir/abs_symlink_to_symlink", "/", "/sub/dir/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
|
||||
{"../../symlink", "$TMP/with/scope/rel_symlink_to_symlink", "/with/scope", "/with/scope/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/with/scope/abs_symlink_to_symlink", "/with/scope", "/with/scope/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
|
||||
{"../../../../symlink", "$TMP/with/scope/sub/dir/rel_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/with/scope/sub/dir/abs_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
func() {
|
||||
tmpdir, err := ioutil.TempDir("", testDirPrefix)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(tmpdir, "dir"), 0755); err != nil {
|
||||
t.Fatalf("failed to create test dir 'dir': %v", err)
|
||||
}
|
||||
if err := os.Symlink("dir", filepath.Join(tmpdir, "symlink")); err != nil {
|
||||
t.Fatalf("failed to create test symlink 'symlink': %v", err)
|
||||
}
|
||||
|
||||
sourceResolved := strings.Replace(tc.source, "$TMP", tmpdir, -1)
|
||||
targetResolved := strings.Replace(tc.target, "$TMP", tmpdir, -1)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(sourceResolved), 0755); err != nil {
|
||||
t.Fatalf("failed to create source symlink dir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetResolved), 0755); err != nil {
|
||||
t.Fatalf("failed to create target symlink dir: %v", err)
|
||||
}
|
||||
if err := os.Symlink(sourceResolved, targetResolved); err != nil {
|
||||
t.Fatalf("failed to create test symlink: %v", err)
|
||||
}
|
||||
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Test %d - Next shouldn't be called", i)
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: tc.pathScope,
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir(tmpdir),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", tc.url, nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
returnCode, _ := b.ServeHTTP(rec, req)
|
||||
if returnCode != http.StatusOK {
|
||||
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
|
||||
i, http.StatusOK, returnCode)
|
||||
}
|
||||
|
||||
type jsonEntry struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
IsSymlink bool
|
||||
URL string
|
||||
}
|
||||
var entries []jsonEntry
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &entries); err != nil {
|
||||
t.Fatalf("Test %d - failed to parse json: %v", i, err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, e := range entries {
|
||||
if e.Name != tc.expectedName {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if !e.IsDir {
|
||||
t.Errorf("Test %d - expected to be a dir, got %v", i, e.IsDir)
|
||||
}
|
||||
if !e.IsSymlink {
|
||||
t.Errorf("Test %d - expected to be a symlink, got %v", i, e.IsSymlink)
|
||||
}
|
||||
if e.URL != tc.expectedURL {
|
||||
t.Errorf("Test %d - wrong URL, expected %v, got %v", i, tc.expectedURL, e.URL)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Test %d - failed, could not find name %v", i, tc.expectedName)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 browse
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
tempDirPath := os.TempDir()
|
||||
_, err := os.Stat(tempDirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
nonExistentDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
|
||||
|
||||
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
|
||||
}
|
||||
defer os.Remove(tempTemplate.Name())
|
||||
|
||||
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expectedPathScope []string
|
||||
shouldErr bool
|
||||
}{
|
||||
// test case #0 tests handling of multiple pathscopes
|
||||
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
|
||||
|
||||
// test case #1 tests instantiation of Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detection of custom template
|
||||
{"browse . " + tempTemplatePath, []string{"."}, false},
|
||||
|
||||
// test case #3 tests detection of non-existent template
|
||||
{"browse . " + nonExistentDirPath, nil, true},
|
||||
|
||||
// test case #4 tests detection of duplicate pathscopes
|
||||
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
|
||||
} {
|
||||
|
||||
c := caddy.NewTestController("http", test.input)
|
||||
err := setup(c)
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d received an error of %v", i, err)
|
||||
}
|
||||
if test.expectedPathScope == nil {
|
||||
continue
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
mid := mids[len(mids)-1]
|
||||
receivedConfigs := mid(nil).(Browse).Configs
|
||||
for j, config := range receivedConfigs {
|
||||
if config.PathScope != test.expectedPathScope[j] {
|
||||
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test case #6 tests startup with missing root directory in combination with default browse settings
|
||||
controller := caddy.NewTestController("http", "browse")
|
||||
cfg := httpserver.GetConfig(controller)
|
||||
|
||||
// Make sure non-existent root path doesn't return error
|
||||
cfg.Root = nonExistentDirPath
|
||||
err = setup(controller)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Test for non-existent browse path received an error, but shouldn't have: %v", err)
|
||||
}
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
<h1>Header</h1>
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Template</title>
|
||||
</head>
|
||||
<body>
|
||||
{{.Include "header.html"}}
|
||||
<h1>{{.Path}}</h1>
|
||||
{{range .Items}}
|
||||
<a href="{{.URL}}">{{.Name}}</a><br>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
Should be hidden
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test 2</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</html>
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 caddyhttp
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
|
||||
// plug in the standard directives
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/basicauth"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/bind"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/browse"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/errors"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/expvar"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/extensions"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/fastcgi"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/gzip"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/header"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/index"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/internalsrv"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/limits"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/log"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/markdown"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/mime"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/pprof"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/proxy"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/push"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/requestid"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/root"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/status"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/templates"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/websocket"
|
||||
_ "github.com/caddyserver/caddy/onevent"
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 caddyhttp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
// TODO: this test could be improved; the purpose is to
|
||||
// ensure that the standard plugins are in fact plugged in
|
||||
// and registered properly; this is a quick/naive way to do it.
|
||||
func TestStandardPlugins(t *testing.T) {
|
||||
numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins
|
||||
s := caddy.DescribePlugins()
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+4; got != want {
|
||||
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 errors implements an HTTP error handling middleware.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("errors", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorHandler handles HTTP errors (and errors from other middleware).
|
||||
type ErrorHandler struct {
|
||||
Next httpserver.Handler
|
||||
GenericErrorPage string // default error page filename
|
||||
ErrorPages map[int]string // map of status code to filename
|
||||
Log *httpserver.Logger
|
||||
Debug bool // if true, errors are written out to client rather than to a log
|
||||
}
|
||||
|
||||
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
defer h.recovery(w, r)
|
||||
|
||||
status, err := h.Next.ServeHTTP(w, r)
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("[ERROR %d %s] %v", status, r.URL.Path, err)
|
||||
if h.Debug {
|
||||
// Write error to response instead of to log
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintln(w, errMsg)
|
||||
return 0, err // returning 0 signals that a response has been written
|
||||
}
|
||||
h.Log.Println(errMsg)
|
||||
}
|
||||
|
||||
if status >= 400 {
|
||||
h.errorPage(w, r, status)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// errorPage serves a static error page to w according to the status
|
||||
// code. If there is an error serving the error page, a plaintext error
|
||||
// message is written instead, and the extra error is logged.
|
||||
func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) {
|
||||
// See if an error page for this status code was specified
|
||||
if pagePath, ok := h.findErrorPage(code); ok {
|
||||
// Try to open it
|
||||
errorPage, err := os.Open(pagePath)
|
||||
if err != nil {
|
||||
// An additional error handling an error... <insert grumpy cat here>
|
||||
h.Log.Printf("[NOTICE %d %s] could not load error page: %v", code, r.URL.String(), err)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
return
|
||||
}
|
||||
defer errorPage.Close()
|
||||
|
||||
// Copy the page body into the response
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
_, err = io.Copy(w, errorPage)
|
||||
|
||||
if err != nil {
|
||||
// Epic fail... sigh.
|
||||
h.Log.Printf("[NOTICE %d %s] could not respond with %s: %v", code, r.URL.String(), pagePath, err)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Default error response
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
func (h ErrorHandler) findErrorPage(code int) (string, bool) {
|
||||
if pagePath, ok := h.ErrorPages[code]; ok {
|
||||
return pagePath, true
|
||||
}
|
||||
|
||||
if h.GenericErrorPage != "" {
|
||||
return h.GenericErrorPage, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
rec := recover()
|
||||
if rec == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain source of panic
|
||||
// From: https://gist.github.com/swdunlop/9629168
|
||||
var name, file string // function name, file name
|
||||
var line int
|
||||
var pc [16]uintptr
|
||||
n := runtime.Callers(3, pc[:])
|
||||
for _, pc := range pc[:n] {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
file, line = fn.FileLine(pc)
|
||||
name = fn.Name()
|
||||
if !strings.HasPrefix(name, "runtime.") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Trim file path
|
||||
delim := "/github.com/caddyserver/caddy/"
|
||||
pkgPathPos := strings.Index(file, delim)
|
||||
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
|
||||
file = file[pkgPathPos+len(delim):]
|
||||
}
|
||||
|
||||
panicMsg := fmt.Sprintf("[PANIC %s] %s:%d - %v", r.URL.String(), file, line, rec)
|
||||
if h.Debug {
|
||||
// Write error and stack trace to the response rather than to a log
|
||||
var stackBuf [4096]byte
|
||||
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
|
||||
httpserver.WriteTextResponse(w, http.StatusInternalServerError, fmt.Sprintf("%s\n\n%s", panicMsg, stack))
|
||||
} else {
|
||||
// Currently we don't use the function name, since file:line is more conventional
|
||||
h.Log.Printf(panicMsg)
|
||||
h.errorPage(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
// create a temporary page
|
||||
const content = "This is a error page"
|
||||
|
||||
path, err := createErrorPageFile("errors_test.html", content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(path)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
em := ErrorHandler{
|
||||
ErrorPages: map[int]string{
|
||||
http.StatusNotFound: path,
|
||||
http.StatusForbidden: "not_exist_file",
|
||||
},
|
||||
Log: httpserver.NewTestLogger(&buf),
|
||||
}
|
||||
_, notExistErr := os.Open("not_exist_file")
|
||||
|
||||
testErr := errors.New("test error")
|
||||
tests := []struct {
|
||||
next httpserver.Handler
|
||||
expectedCode int
|
||||
expectedBody string
|
||||
expectedLog string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
next: genErrorHandler(http.StatusOK, nil, "normal"),
|
||||
expectedCode: http.StatusOK,
|
||||
expectedBody: "normal",
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusMovedPermanently, testErr, ""),
|
||||
expectedCode: http.StatusMovedPermanently,
|
||||
expectedBody: "",
|
||||
expectedLog: fmt.Sprintf("[ERROR %d %s] %v\n", http.StatusMovedPermanently, "/", testErr),
|
||||
expectedErr: testErr,
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusBadRequest, nil, ""),
|
||||
expectedCode: 0,
|
||||
expectedBody: fmt.Sprintf("%d %s\n", http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)),
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusNotFound, nil, ""),
|
||||
expectedCode: 0,
|
||||
expectedBody: content,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusForbidden, nil, ""),
|
||||
expectedCode: 0,
|
||||
expectedBody: fmt.Sprintf("%d %s\n", http.StatusForbidden,
|
||||
http.StatusText(http.StatusForbidden)),
|
||||
expectedLog: fmt.Sprintf("[NOTICE %d /] could not load error page: %v\n",
|
||||
http.StatusForbidden, notExistErr),
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, test := range tests {
|
||||
em.Next = test.next
|
||||
buf.Reset()
|
||||
rec := httptest.NewRecorder()
|
||||
code, err := em.ServeHTTP(rec, req)
|
||||
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v, but got %v",
|
||||
i, test.expectedErr, err)
|
||||
}
|
||||
if code != test.expectedCode {
|
||||
t.Errorf("Test %d: Expected status code %d, but got %d",
|
||||
i, test.expectedCode, code)
|
||||
}
|
||||
if body := rec.Body.String(); body != test.expectedBody {
|
||||
t.Errorf("Test %d: Expected body %q, but got %q",
|
||||
i, test.expectedBody, body)
|
||||
}
|
||||
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
|
||||
t.Errorf("Test %d: Expected log %q, but got %q",
|
||||
i, test.expectedLog, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
const panicMsg = "I'm a panic"
|
||||
eh := ErrorHandler{
|
||||
ErrorPages: make(map[int]string),
|
||||
Debug: true,
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
panic(panicMsg)
|
||||
}),
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
code, err := eh.ServeHTTP(rec, req)
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("Expected error handler to return 0 (it should write to response), got status %d", code)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected error handler to return nil error (it should panic!), but got '%v'", err)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") {
|
||||
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, panicMsg) {
|
||||
t.Errorf("Expected response body to contain panic message, but it didn't:\n%s", body)
|
||||
}
|
||||
if len(body) < 500 {
|
||||
t.Errorf("Expected response body to contain stack trace, but it was too short: len=%d", len(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericErrorPage(t *testing.T) {
|
||||
// create temporary generic error page
|
||||
const genericErrorContent = "This is a generic error page"
|
||||
|
||||
genericErrorPagePath, err := createErrorPageFile("generic_error_test.html", genericErrorContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(genericErrorPagePath)
|
||||
|
||||
// create temporary error page
|
||||
const notFoundErrorContent = "This is a error page"
|
||||
|
||||
notFoundErrorPagePath, err := createErrorPageFile("not_found.html", notFoundErrorContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(notFoundErrorPagePath)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
em := ErrorHandler{
|
||||
GenericErrorPage: genericErrorPagePath,
|
||||
ErrorPages: map[int]string{
|
||||
http.StatusNotFound: notFoundErrorPagePath,
|
||||
},
|
||||
Log: httpserver.NewTestLogger(&buf),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
next httpserver.Handler
|
||||
expectedCode int
|
||||
expectedBody string
|
||||
expectedLog string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
next: genErrorHandler(http.StatusNotFound, nil, ""),
|
||||
expectedCode: 0,
|
||||
expectedBody: notFoundErrorContent,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusInternalServerError, nil, ""),
|
||||
expectedCode: 0,
|
||||
expectedBody: genericErrorContent,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
em.Next = test.next
|
||||
buf.Reset()
|
||||
rec := httptest.NewRecorder()
|
||||
code, err := em.ServeHTTP(rec, req)
|
||||
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v, but got %v",
|
||||
i, test.expectedErr, err)
|
||||
}
|
||||
if code != test.expectedCode {
|
||||
t.Errorf("Test %d: Expected status code %d, but got %d",
|
||||
i, test.expectedCode, code)
|
||||
}
|
||||
if body := rec.Body.String(); body != test.expectedBody {
|
||||
t.Errorf("Test %d: Expected body %q, but got %q",
|
||||
i, test.expectedBody, body)
|
||||
}
|
||||
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
|
||||
t.Errorf("Test %d: Expected log %q, but got %q",
|
||||
i, test.expectedLog, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func genErrorHandler(status int, err error, body string) httpserver.Handler {
|
||||
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if len(body) > 0 {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
fmt.Fprint(w, body)
|
||||
}
|
||||
return status, err
|
||||
})
|
||||
}
|
||||
|
||||
func createErrorPageFile(name string, content string) (string, error) {
|
||||
errorPageFilePath := filepath.Join(os.TempDir(), name)
|
||||
f, err := os.Create(errorPageFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
return errorPageFilePath, nil
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 errors
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new errors middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
handler, err := errorsParse(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler.Log.Attach(c)
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
|
||||
|
||||
// Very important that we make a pointer because the startup
|
||||
// function that opens the log file must have access to the
|
||||
// same instance of the handler, not a copy.
|
||||
handler := &ErrorHandler{
|
||||
ErrorPages: make(map[int]string),
|
||||
Log: &httpserver.Logger{},
|
||||
}
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
optionalBlock := func() error {
|
||||
for c.NextBlock() {
|
||||
|
||||
what := c.Val()
|
||||
where := c.RemainingArgs()
|
||||
|
||||
if httpserver.IsLogRollerSubdirective(what) {
|
||||
var err error
|
||||
err = httpserver.ParseRoller(handler.Log.Roller, what, where...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if len(where) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
where := where[0]
|
||||
|
||||
// Error page; ensure it exists
|
||||
if !filepath.IsAbs(where) {
|
||||
where = filepath.Join(cfg.Root, where)
|
||||
}
|
||||
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if what == "*" {
|
||||
if handler.GenericErrorPage != "" {
|
||||
return c.Errf("Duplicate status code entry: %s", what)
|
||||
}
|
||||
handler.GenericErrorPage = where
|
||||
} else {
|
||||
whatInt, err := strconv.Atoi(what)
|
||||
if err != nil {
|
||||
return c.Err("Expecting a numeric status code or '*', got '" + what + "'")
|
||||
}
|
||||
|
||||
if _, exists := handler.ErrorPages[whatInt]; exists {
|
||||
return c.Errf("Duplicate status code entry: %s", what)
|
||||
}
|
||||
|
||||
handler.ErrorPages[whatInt] = where
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
// weird hack to avoid having the handler values overwritten.
|
||||
if c.Val() == "}" {
|
||||
continue
|
||||
}
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) == 1 {
|
||||
switch args[0] {
|
||||
case "visible":
|
||||
handler.Debug = true
|
||||
default:
|
||||
handler.Log.Output = args[0]
|
||||
handler.Log.Roller = httpserver.DefaultLogRoller()
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
return handler, c.Errf("Only 1 Argument expected for errors directive")
|
||||
}
|
||||
|
||||
// Configuration may be in a block
|
||||
err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 errors
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `errors`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middlewares, was nil instead")
|
||||
}
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(*ErrorHandler)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
|
||||
expectedLogger := &httpserver.Logger{}
|
||||
|
||||
if !reflect.DeepEqual(expectedLogger, myHandler.Log) {
|
||||
t.Errorf("Expected '%v' as the default Log, got: '%v'", expectedLogger, myHandler.Log)
|
||||
}
|
||||
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
// Test Startup function -- TODO
|
||||
// if len(c.Startup) == 0 {
|
||||
// t.Fatal("Expected 1 startup function, had 0")
|
||||
// }
|
||||
// c.Startup[0]()
|
||||
// if myHandler.Log == nil {
|
||||
// t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
// }
|
||||
}
|
||||
|
||||
func TestErrorsParse(t *testing.T) {
|
||||
testAbs, err := filepath.Abs("./404.html")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler ErrorHandler
|
||||
}{
|
||||
{`errors`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Log: &httpserver.Logger{},
|
||||
}},
|
||||
{`errors errors.txt`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: httpserver.DefaultLogRoller(),
|
||||
},
|
||||
}},
|
||||
{`errors visible`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Debug: true,
|
||||
Log: &httpserver.Logger{},
|
||||
}},
|
||||
{`errors errors.txt {
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
500: "500.html",
|
||||
},
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: httpserver.DefaultLogRoller(),
|
||||
},
|
||||
}},
|
||||
{`errors errors.txt {
|
||||
rotate_size 2
|
||||
rotate_age 10
|
||||
rotate_keep 3
|
||||
rotate_compress
|
||||
}`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt", Roller: &httpserver.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
Compress: true,
|
||||
LocalTime: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{`errors errors.txt {
|
||||
rotate_size 3
|
||||
rotate_age 11
|
||||
rotate_keep 5
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: &httpserver.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
Compress: false,
|
||||
LocalTime: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{`errors errors.txt {
|
||||
* generic_error.html
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, ErrorHandler{
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: httpserver.DefaultLogRoller(),
|
||||
},
|
||||
GenericErrorPage: "generic_error.html",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
}},
|
||||
// test absolute file path
|
||||
{`errors {
|
||||
404 ` + testAbs + `
|
||||
}`,
|
||||
false, ErrorHandler{
|
||||
ErrorPages: map[int]string{
|
||||
404: testAbs,
|
||||
},
|
||||
Log: &httpserver.Logger{},
|
||||
}},
|
||||
{`errors errors.txt { rotate_size 2 rotate_age 10 rotate_keep 3 rotate_compress }`,
|
||||
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
{`errors errors.txt {
|
||||
rotate_compress invalid
|
||||
}`,
|
||||
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
// Next two test cases is the detection of duplicate status codes
|
||||
{`errors {
|
||||
503 503.html
|
||||
503 503.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
|
||||
{`errors {
|
||||
* generic_error.html
|
||||
* generic_error.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
{`errors /path error.txt {
|
||||
404
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
|
||||
{`errors /path error.txt`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actualErrorsRule, err := errorsParse(caddy.NewTestController("http", test.inputErrorsRules))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(actualErrorsRule, &test.expectedErrorHandler) {
|
||||
t.Errorf("Test %d expect %v, but got %v", i,
|
||||
test.expectedErrorHandler, actualErrorsRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 expvar
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// ExpVar is a simple struct to hold expvar's configuration
|
||||
type ExpVar struct {
|
||||
Next httpserver.Handler
|
||||
Resource Resource
|
||||
}
|
||||
|
||||
// ServeHTTP handles requests to expvar's configured entry point with
|
||||
// expvar, or passes all other requests up the chain.
|
||||
func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) {
|
||||
expvarHandler(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
return e.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// expvarHandler returns a JSON object will all the published variables.
|
||||
//
|
||||
// This is lifted straight from the expvar package.
|
||||
func expvarHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprintf(w, "{\n")
|
||||
first := true
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
if !first {
|
||||
fmt.Fprintf(w, ",\n")
|
||||
}
|
||||
first = false
|
||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
||||
})
|
||||
fmt.Fprintf(w, "\n}\n")
|
||||
}
|
||||
|
||||
// Resource contains the path to the expvar entry point
|
||||
type Resource string
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 expvar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestExpVar(t *testing.T) {
|
||||
rw := ExpVar{
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Resource: "/d/v",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
from string
|
||||
result int
|
||||
}{
|
||||
{"/d/v", 0},
|
||||
{"/x/y", http.StatusOK},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
|
||||
i, test.result, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprintf(w, r.URL.String())
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 expvar
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("expvar", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new ExpVar middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
resource, err := expVarParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// publish any extra information/metrics we may want to capture
|
||||
publishExtraVars()
|
||||
|
||||
ev := ExpVar{Resource: resource}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
ev.Next = next
|
||||
return ev
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expVarParse(c *caddy.Controller) (Resource, error) {
|
||||
var resource Resource
|
||||
var err error
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
resource = Resource(defaultExpvarPath)
|
||||
case 1:
|
||||
resource = Resource(args[0])
|
||||
default:
|
||||
return resource, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
return resource, err
|
||||
}
|
||||
|
||||
func publishExtraVars() {
|
||||
// By using sync.Once instead of an init() function, we don't clutter
|
||||
// the app's expvar export unnecessarily, or risk colliding with it.
|
||||
publishOnce.Do(func() {
|
||||
expvar.Publish("Goroutines", expvar.Func(func() interface{} {
|
||||
return runtime.NumGoroutine()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
var publishOnce sync.Once // publishing variables should only be done once
|
||||
var defaultExpvarPath = "/debug/vars"
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 expvar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `expvar`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
c = caddy.NewTestController("http", `expvar /d/v`)
|
||||
err = setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
mids = httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(ExpVar)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler)
|
||||
}
|
||||
if myHandler.Resource != "/d/v" {
|
||||
t.Errorf("Expected /d/v as expvar resource")
|
||||
}
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 extensions contains middleware for clean URLs.
|
||||
//
|
||||
// The root path of the site is passed in as well as possible extensions
|
||||
// to try internally for paths requested that don't match an existing
|
||||
// resource. The first path+ext combination that matches a valid file
|
||||
// will be used.
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Ext can assume an extension from clean URLs.
|
||||
// It tries extensions in the order listed in Extensions.
|
||||
type Ext struct {
|
||||
// Next handler in the chain
|
||||
Next httpserver.Handler
|
||||
|
||||
// Path to site root
|
||||
Root string
|
||||
|
||||
// List of extensions to try
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
urlpath := strings.TrimSuffix(r.URL.Path, "/")
|
||||
if len(r.URL.Path) > 0 && path.Ext(urlpath) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
|
||||
for _, ext := range e.Extensions {
|
||||
_, err := os.Stat(httpserver.SafePath(e.Root, urlpath) + ext)
|
||||
if err == nil {
|
||||
r.URL.Path = urlpath + ext
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return e.Next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 extensions
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("ext", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new instance of 'extensions' middleware for clean URLs.
|
||||
func setup(c *caddy.Controller) error {
|
||||
cfg := httpserver.GetConfig(c)
|
||||
root := cfg.Root
|
||||
|
||||
exts, err := extParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Ext{
|
||||
Next: next,
|
||||
Extensions: exts,
|
||||
Root: root,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extParse sets up an instance of extension middleware
|
||||
// from a middleware controller and returns a list of extensions.
|
||||
func extParse(c *caddy.Controller) ([]string, error) {
|
||||
var exts []string
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
if !c.NextArg() {
|
||||
return exts, c.ArgErr()
|
||||
}
|
||||
exts = append(exts, c.Val())
|
||||
|
||||
// Tack on any other extensions that may have been listed
|
||||
exts = append(exts, c.RemainingArgs()...)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 extensions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `ext .html .htm .php`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, had 0 instead")
|
||||
}
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Ext)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Extensions[0] != ".html" {
|
||||
t.Errorf("Expected .html in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[1] != ".htm" {
|
||||
t.Errorf("Expected .htm in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[2] != ".php" {
|
||||
t.Errorf("Expected .php in the list of Extensions")
|
||||
}
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputExts string
|
||||
shouldErr bool
|
||||
expectedExts []string
|
||||
}{
|
||||
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
|
||||
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
|
||||
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
actualExts, err := extParse(caddy.NewTestController("http", test.inputExts))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualExts) != len(test.expectedExts) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expectedExts), len(actualExts))
|
||||
}
|
||||
for j, actualExt := range actualExts {
|
||||
if actualExt != test.expectedExts[j] {
|
||||
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
|
||||
i, j, test.expectedExts[j], actualExt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 fastcgi has middleware that acts as a FastCGI client. Requests
|
||||
// that get forwarded to FastCGI stop the middleware execution chain.
|
||||
// The most common use for this package is to serve PHP websites via php-fpm.
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// Handler is a middleware type that can handle requests as a FastCGI client.
|
||||
type Handler struct {
|
||||
Next httpserver.Handler
|
||||
Rules []Rule
|
||||
Root string
|
||||
FileSys http.FileSystem
|
||||
|
||||
// These are sent to CGI scripts in env variables
|
||||
SoftwareName string
|
||||
SoftwareVersion string
|
||||
ServerName string
|
||||
ServerPort string
|
||||
}
|
||||
|
||||
// ServeHTTP satisfies the httpserver.Handler interface.
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for _, rule := range h.Rules {
|
||||
// First requirement: Base path must match request path. If it doesn't,
|
||||
// we check to make sure the leading slash is not missing, and if so,
|
||||
// we check again with it prepended. This is in case people forget
|
||||
// a leading slash when performing rewrites, and we don't want to expose
|
||||
// the contents of the (likely PHP) script. See issue #1645.
|
||||
hpath := httpserver.Path(r.URL.Path)
|
||||
if !hpath.Matches(rule.Path) {
|
||||
if strings.HasPrefix(string(hpath), "/") {
|
||||
// this is a normal-looking path, and it doesn't match; try next rule
|
||||
continue
|
||||
}
|
||||
hpath = httpserver.Path("/" + string(hpath)) // prepend leading slash
|
||||
if !hpath.Matches(rule.Path) {
|
||||
// even after fixing the request path, it still doesn't match; try next rule
|
||||
continue
|
||||
}
|
||||
}
|
||||
// The path must also be allowed (not ignored).
|
||||
if !rule.AllowedPath(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
// In addition to matching the path, a request must meet some
|
||||
// other criteria before being proxied as FastCGI. For example,
|
||||
// we probably want to exclude static assets (CSS, JS, images...)
|
||||
// but we also want to be flexible for the script we proxy to.
|
||||
|
||||
fpath := r.URL.Path
|
||||
|
||||
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
|
||||
fpath = idx
|
||||
// Index file present.
|
||||
// If request path cannot be split, return error.
|
||||
if !rule.canSplit(fpath) {
|
||||
return http.StatusInternalServerError, ErrIndexMissingSplit
|
||||
}
|
||||
} else {
|
||||
// No index file present.
|
||||
// If request path cannot be split, ignore request.
|
||||
if !rule.canSplit(fpath) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// These criteria work well in this order for PHP sites
|
||||
// We lower path and Ext as on Windows, the system is case insensitive, so .PHP is served as .php
|
||||
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(strings.ToLower(fpath), strings.ToLower(rule.Ext)) {
|
||||
|
||||
// Create environment for CGI script
|
||||
env, err := h.buildEnv(r, rule, fpath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Connect to FastCGI gateway
|
||||
address, err := rule.Address()
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
network, address := parseAddress(address)
|
||||
|
||||
ctx := context.Background()
|
||||
if rule.ConnectTimeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, rule.ConnectTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
fcgiBackend, err := DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
defer fcgiBackend.Close()
|
||||
|
||||
// read/write timeouts
|
||||
if err := fcgiBackend.SetReadTimeout(rule.ReadTimeout); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if err := fcgiBackend.SetSendTimeout(rule.SendTimeout); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
|
||||
var contentLength int64
|
||||
// if ContentLength is already set
|
||||
if r.ContentLength > 0 {
|
||||
contentLength = r.ContentLength
|
||||
} else {
|
||||
contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
|
||||
}
|
||||
switch r.Method {
|
||||
case "HEAD":
|
||||
resp, err = fcgiBackend.Head(env)
|
||||
case "GET":
|
||||
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
|
||||
case "OPTIONS":
|
||||
resp, err = fcgiBackend.Options(env)
|
||||
default:
|
||||
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
}
|
||||
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
return http.StatusGatewayTimeout, err
|
||||
} else if err != io.EOF {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
}
|
||||
|
||||
// Write response header
|
||||
writeHeader(w, resp)
|
||||
|
||||
// Write the response body
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
|
||||
// Log any stderr output from upstream
|
||||
if fcgiBackend.stderr.Len() != 0 {
|
||||
// Remove trailing newline, error logger already does this.
|
||||
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
|
||||
}
|
||||
|
||||
// Normally we would return the status code if it is an error status (>= 400),
|
||||
// however, upstream FastCGI apps don't know about our contract and have
|
||||
// probably already written an error page. So we just return 0, indicating
|
||||
// that the response body is already written. However, we do return any
|
||||
// error value so it can be logged.
|
||||
// Note that the proxy middleware works the same way, returning status=0.
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return h.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// parseAddress returns the network and address of fcgiAddress.
|
||||
// The first string is the network, "tcp" or "unix", implied from the scheme and address.
|
||||
// The second string is fcgiAddress, with scheme prefixes removed.
|
||||
// The two returned strings can be used as parameters to the Dial() function.
|
||||
func parseAddress(fcgiAddress string) (string, string) {
|
||||
// check if address has tcp scheme explicitly set
|
||||
if strings.HasPrefix(fcgiAddress, "tcp://") {
|
||||
return "tcp", fcgiAddress[len("tcp://"):]
|
||||
}
|
||||
// check if address has fastcgi scheme explicitly set
|
||||
if strings.HasPrefix(fcgiAddress, "fastcgi://") {
|
||||
return "tcp", fcgiAddress[len("fastcgi://"):]
|
||||
}
|
||||
// check if unix socket
|
||||
if trim := strings.HasPrefix(fcgiAddress, "unix"); strings.HasPrefix(fcgiAddress, "/") || trim {
|
||||
if trim {
|
||||
return "unix", fcgiAddress[len("unix:"):]
|
||||
}
|
||||
return "unix", fcgiAddress
|
||||
}
|
||||
// default case, a plain tcp address with no scheme
|
||||
return "tcp", fcgiAddress
|
||||
}
|
||||
|
||||
func writeHeader(w http.ResponseWriter, r *http.Response) {
|
||||
for key, vals := range r.Header {
|
||||
for _, val := range vals {
|
||||
w.Header().Add(key, val)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(r.StatusCode)
|
||||
}
|
||||
|
||||
func (h Handler) exists(path string) bool {
|
||||
if _, err := os.Stat(h.Root + path); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// buildEnv returns a set of CGI environment variables for the request.
|
||||
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
|
||||
var env map[string]string
|
||||
|
||||
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
var ip, port string
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
|
||||
ip = r.RemoteAddr[:idx]
|
||||
port = r.RemoteAddr[idx+1:]
|
||||
} else {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
|
||||
// Remove [] from IPv6 addresses
|
||||
ip = strings.Replace(ip, "[", "", 1)
|
||||
ip = strings.Replace(ip, "]", "", 1)
|
||||
|
||||
// Split path in preparation for env variables.
|
||||
// Previous rule.canSplit checks ensure this can never be -1.
|
||||
splitPos := rule.splitPos(fpath)
|
||||
|
||||
// Request has the extension; path was split successfully
|
||||
docURI := fpath[:splitPos+len(rule.SplitPath)]
|
||||
pathInfo := fpath[splitPos+len(rule.SplitPath):]
|
||||
scriptName := fpath
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
scriptName = strings.TrimSuffix(scriptName, pathInfo)
|
||||
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
scriptFilename := filepath.Join(rule.Root, scriptName)
|
||||
|
||||
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
|
||||
// have difficulty discovering its URL.
|
||||
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
|
||||
scriptName = path.Join(pathPrefix, scriptName)
|
||||
|
||||
// Get the request URI from context. The context stores the original URI in case
|
||||
// it was changed by a middleware such as rewrite. By default, we pass the
|
||||
// original URI in as the value of REQUEST_URI (the user can overwrite this
|
||||
// if desired). Most PHP apps seem to want the original URI. Besides, this is
|
||||
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
|
||||
reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
|
||||
|
||||
// Retrieve name of remote user that was set by some downstream middleware such as basicauth.
|
||||
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
|
||||
|
||||
requestScheme := "http"
|
||||
if r.TLS != nil {
|
||||
requestScheme = "https"
|
||||
}
|
||||
|
||||
// Some variables are unused but cleared explicitly to prevent
|
||||
// the parent environment from interfering.
|
||||
env = map[string]string{
|
||||
// Variables defined in CGI 1.1 spec
|
||||
"AUTH_TYPE": "", // Not used
|
||||
"CONTENT_LENGTH": r.Header.Get("Content-Length"),
|
||||
"CONTENT_TYPE": r.Header.Get("Content-Type"),
|
||||
"GATEWAY_INTERFACE": "CGI/1.1",
|
||||
"PATH_INFO": pathInfo,
|
||||
"QUERY_STRING": r.URL.RawQuery,
|
||||
"REMOTE_ADDR": ip,
|
||||
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
|
||||
"REMOTE_PORT": port,
|
||||
"REMOTE_IDENT": "", // Not used
|
||||
"REMOTE_USER": remoteUser,
|
||||
"REQUEST_METHOD": r.Method,
|
||||
"REQUEST_SCHEME": requestScheme,
|
||||
"SERVER_NAME": h.ServerName,
|
||||
"SERVER_PORT": h.ServerPort,
|
||||
"SERVER_PROTOCOL": r.Proto,
|
||||
"SERVER_SOFTWARE": h.SoftwareName + "/" + h.SoftwareVersion,
|
||||
|
||||
// Other variables
|
||||
"DOCUMENT_ROOT": rule.Root,
|
||||
"DOCUMENT_URI": docURI,
|
||||
"HTTP_HOST": r.Host, // added here, since not always part of headers
|
||||
"REQUEST_URI": reqURL.RequestURI(),
|
||||
"SCRIPT_FILENAME": scriptFilename,
|
||||
"SCRIPT_NAME": scriptName,
|
||||
}
|
||||
|
||||
// compliance with the CGI specification requires that
|
||||
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
|
||||
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
|
||||
if env["PATH_INFO"] != "" {
|
||||
env["PATH_TRANSLATED"] = filepath.Join(rule.Root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
||||
}
|
||||
|
||||
// Some web apps rely on knowing HTTPS or not
|
||||
if r.TLS != nil {
|
||||
env["HTTPS"] = "on"
|
||||
// and pass the protocol details in a manner compatible with apache's mod_ssl
|
||||
// (which is why they have a SSL_ prefix and not TLS_).
|
||||
v, ok := tlsProtocolStringToMap[r.TLS.Version]
|
||||
if ok {
|
||||
env["SSL_PROTOCOL"] = v
|
||||
}
|
||||
// and pass the cipher suite in a manner compatible with apache's mod_ssl
|
||||
for k, v := range caddytls.SupportedCiphersMap {
|
||||
if v == r.TLS.CipherSuite {
|
||||
env["SSL_CIPHER"] = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add env variables from config (with support for placeholders in values)
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
for _, envVar := range rule.EnvVars {
|
||||
env[envVar[0]] = replacer.Replace(envVar[1])
|
||||
}
|
||||
|
||||
// Add all HTTP headers to env variables
|
||||
for field, val := range r.Header {
|
||||
header := strings.ToUpper(field)
|
||||
header = headerNameReplacer.Replace(header)
|
||||
env["HTTP_"+header] = strings.Join(val, ", ")
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// Rule represents a FastCGI handling rule.
|
||||
// It is parsed from the fastcgi directive in the Caddyfile, see setup.go.
|
||||
type Rule struct {
|
||||
// The base path to match. Required.
|
||||
Path string
|
||||
|
||||
// upstream load balancer
|
||||
balancer
|
||||
|
||||
// Always process files with this extension with fastcgi.
|
||||
Ext string
|
||||
|
||||
// Use this directory as the fastcgi root directory. Defaults to the root
|
||||
// directory of the parent virtual host.
|
||||
Root string
|
||||
|
||||
// The path in the URL will be split into two, with the first piece ending
|
||||
// with the value of SplitPath. The first piece will be assumed as the
|
||||
// actual resource (CGI script) name, and the second piece will be set to
|
||||
// PATH_INFO for the CGI script to use.
|
||||
SplitPath string
|
||||
|
||||
// If the URL ends with '/' (which indicates a directory), these index
|
||||
// files will be tried instead.
|
||||
IndexFiles []string
|
||||
|
||||
// Environment Variables
|
||||
EnvVars [][2]string
|
||||
|
||||
// Ignored paths
|
||||
IgnoredSubPaths []string
|
||||
|
||||
// The duration used to set a deadline when connecting to an upstream.
|
||||
ConnectTimeout time.Duration
|
||||
|
||||
// The duration used to set a deadline when reading from the FastCGI server.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// The duration used to set a deadline when sending to the FastCGI server.
|
||||
SendTimeout time.Duration
|
||||
}
|
||||
|
||||
// balancer is a fastcgi upstream load balancer.
|
||||
type balancer interface {
|
||||
// Address picks an upstream address from the
|
||||
// underlying load balancer.
|
||||
Address() (string, error)
|
||||
}
|
||||
|
||||
// roundRobin is a round robin balancer for fastcgi upstreams.
|
||||
type roundRobin struct {
|
||||
// Known Go bug: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
// must be first field for 64 bit alignment
|
||||
// on x86 and arm.
|
||||
index int64
|
||||
addresses []string
|
||||
}
|
||||
|
||||
func (r *roundRobin) Address() (string, error) {
|
||||
index := atomic.AddInt64(&r.index, 1) % int64(len(r.addresses))
|
||||
return r.addresses[index], nil
|
||||
}
|
||||
|
||||
// srvResolver is a private interface used to abstract
|
||||
// the DNS resolver. It is mainly used to facilitate testing.
|
||||
type srvResolver interface {
|
||||
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
|
||||
}
|
||||
|
||||
// srv is a service locator for fastcgi upstreams
|
||||
type srv struct {
|
||||
resolver srvResolver
|
||||
service string
|
||||
}
|
||||
|
||||
// Address looks up the service and returns the address:port
|
||||
// from first result in resolved list.
|
||||
// No explicit balancing is required because net.LookupSRV
|
||||
// sorts the results by priority and randomizes within priority.
|
||||
func (s *srv) Address() (string, error) {
|
||||
_, addrs, err := s.resolver.LookupSRV(context.Background(), "", "", s.service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", strings.TrimRight(addrs[0].Target, "."), addrs[0].Port), nil
|
||||
}
|
||||
|
||||
// canSplit checks if path can split into two based on rule.SplitPath.
|
||||
func (r Rule) canSplit(path string) bool {
|
||||
return r.splitPos(path) >= 0
|
||||
}
|
||||
|
||||
// splitPos returns the index where path should be split
|
||||
// based on rule.SplitPath.
|
||||
func (r Rule) splitPos(path string) int {
|
||||
if httpserver.CaseSensitivePath {
|
||||
return strings.Index(path, r.SplitPath)
|
||||
}
|
||||
return strings.Index(strings.ToLower(path), strings.ToLower(r.SplitPath))
|
||||
}
|
||||
|
||||
// AllowedPath checks if requestPath is not an ignored path.
|
||||
func (r Rule) AllowedPath(requestPath string) bool {
|
||||
for _, ignoredSubPath := range r.IgnoredSubPaths {
|
||||
if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
|
||||
// ErrIndexMissingSplit describes an index configuration error.
|
||||
ErrIndexMissingSplit = errors.New("configured index file(s) must include split value")
|
||||
)
|
||||
|
||||
// LogError is a non fatal error that allows requests to go through.
|
||||
type LogError string
|
||||
|
||||
// Error satisfies error interface.
|
||||
func (l LogError) Error() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
// Map of supported protocols to Apache ssl_mod format
|
||||
// Note that these are slightly different from SupportedProtocols in caddytls/config.go's
|
||||
var tlsProtocolStringToMap = map[uint16]string{
|
||||
tls.VersionTLS10: "TLSv1",
|
||||
tls.VersionTLS11: "TLSv1.1",
|
||||
tls.VersionTLS12: "TLSv1.2",
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
body := "This is some test body content"
|
||||
|
||||
bodyLenStr := strconv.Itoa(len(body))
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create listener for test: %v", err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
_, err := w.Write([]byte(body))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to write header: %v", err)
|
||||
}
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{{Path: "/", balancer: address(listener.Addr().String())}},
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create request: %v", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
status, err := handler.ServeHTTP(w, r)
|
||||
|
||||
if got, want := status, 0; got != want {
|
||||
t.Errorf("Expected returned status code to be %d, got %d", want, got)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got: %v", err)
|
||||
}
|
||||
if got, want := w.Header().Get("Content-Length"), bodyLenStr; got != want {
|
||||
t.Errorf("Expected Content-Length to be '%s', got: '%s'", want, got)
|
||||
}
|
||||
if got, want := w.Body.String(), body; got != want {
|
||||
t.Errorf("Expected response body to be '%s', got: '%s'", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleParseAddress(t *testing.T) {
|
||||
getClientTestTable := []struct {
|
||||
rule *Rule
|
||||
expectednetwork string
|
||||
expectedaddress string
|
||||
}{
|
||||
{&Rule{balancer: address("tcp://172.17.0.1:9000")}, "tcp", "172.17.0.1:9000"},
|
||||
{&Rule{balancer: address("fastcgi://localhost:9000")}, "tcp", "localhost:9000"},
|
||||
{&Rule{balancer: address("172.17.0.15")}, "tcp", "172.17.0.15"},
|
||||
{&Rule{balancer: address("/my/unix/socket")}, "unix", "/my/unix/socket"},
|
||||
{&Rule{balancer: address("unix:/second/unix/socket")}, "unix", "/second/unix/socket"},
|
||||
}
|
||||
|
||||
for _, entry := range getClientTestTable {
|
||||
addr, err := entry.rule.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
|
||||
}
|
||||
if actualnetwork, _ := parseAddress(addr); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", addr, actualnetwork, entry.expectednetwork)
|
||||
}
|
||||
if _, actualaddress := parseAddress(addr); actualaddress != entry.expectedaddress {
|
||||
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", addr, actualaddress, entry.expectedaddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleIgnoredPath(t *testing.T) {
|
||||
rule := &Rule{
|
||||
Path: "/fastcgi",
|
||||
IgnoredSubPaths: []string{"/download", "/static"},
|
||||
}
|
||||
tests := []struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{"/fastcgi", true},
|
||||
{"/fastcgi/dl", true},
|
||||
{"/fastcgi/download", false},
|
||||
{"/fastcgi/download/static", false},
|
||||
{"/fastcgi/static", false},
|
||||
{"/fastcgi/static/download", false},
|
||||
{"/fastcgi/something/download", true},
|
||||
{"/fastcgi/something/static", true},
|
||||
{"/fastcgi//static", false},
|
||||
{"/fastcgi//static//download", false},
|
||||
{"/fastcgi//download", false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
allowed := rule.AllowedPath(test.url)
|
||||
if test.expected != allowed {
|
||||
t.Errorf("Test %d: expected %v found %v", i, test.expected, allowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEnv(t *testing.T) {
|
||||
testBuildEnv := func(r *http.Request, rule Rule, fpath string, envExpected map[string]string) {
|
||||
var h Handler
|
||||
env, err := h.buildEnv(r, rule, fpath)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error:", err.Error())
|
||||
}
|
||||
for k, v := range envExpected {
|
||||
if env[k] != v {
|
||||
t.Errorf("Unexpected %v. Got %v, expected %v", k, env[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
}
|
||||
u, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error:", err.Error())
|
||||
}
|
||||
|
||||
var newReq = func() *http.Request {
|
||||
r := http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Host: "localhost:2015",
|
||||
RemoteAddr: "[2b02:1810:4f2d:9400:70ab:f822:be8a:9093]:51688",
|
||||
RequestURI: "/fgci_test.php",
|
||||
Header: map[string][]string{
|
||||
"Foo": {"Bar", "two"},
|
||||
},
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), httpserver.OriginalURLCtxKey, *r.URL)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
fpath := "/fgci_test.php"
|
||||
|
||||
var newEnv = func() map[string]string {
|
||||
return map[string]string{
|
||||
"REMOTE_ADDR": "2b02:1810:4f2d:9400:70ab:f822:be8a:9093",
|
||||
"REMOTE_PORT": "51688",
|
||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||||
"QUERY_STRING": "test=foobar",
|
||||
"REQUEST_METHOD": "GET",
|
||||
"HTTP_HOST": "localhost:2015",
|
||||
"SCRIPT_NAME": "/fgci_test.php",
|
||||
}
|
||||
}
|
||||
|
||||
// request
|
||||
var r *http.Request
|
||||
|
||||
// expected environment variables
|
||||
var envExpected map[string]string
|
||||
|
||||
// 1. Test for full canonical IPv6 address
|
||||
r = newReq()
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 2. Test for shorthand notation of IPv6 address
|
||||
r = newReq()
|
||||
r.RemoteAddr = "[::1]:51688"
|
||||
envExpected = newEnv()
|
||||
envExpected["REMOTE_ADDR"] = "::1"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 3. Test for IPv4 address
|
||||
r = newReq()
|
||||
r.RemoteAddr = "192.168.0.10:51688"
|
||||
envExpected = newEnv()
|
||||
envExpected["REMOTE_ADDR"] = "192.168.0.10"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 4. Test for environment variable
|
||||
r = newReq()
|
||||
rule.EnvVars = [][2]string{
|
||||
{"HTTP_HOST", "localhost:2016"},
|
||||
{"REQUEST_METHOD", "POST"},
|
||||
}
|
||||
envExpected = newEnv()
|
||||
envExpected["HTTP_HOST"] = "localhost:2016"
|
||||
envExpected["REQUEST_METHOD"] = "POST"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 5. Test for environment variable placeholders
|
||||
r = newReq()
|
||||
rule.EnvVars = [][2]string{
|
||||
{"HTTP_HOST", "{host}"},
|
||||
{"CUSTOM_URI", "custom_uri{uri}"},
|
||||
{"CUSTOM_QUERY", "custom=true&{query}"},
|
||||
}
|
||||
envExpected = newEnv()
|
||||
envExpected["HTTP_HOST"] = "localhost:2015"
|
||||
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=foobar"
|
||||
envExpected["CUSTOM_QUERY"] = "custom=true&test=foobar"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 6. Test SCRIPT_NAME includes path prefix
|
||||
r = newReq()
|
||||
ctx := context.WithValue(r.Context(), caddy.CtxKey("path_prefix"), "/test")
|
||||
r = r.WithContext(ctx)
|
||||
envExpected = newEnv()
|
||||
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 7. Test SCRIPT_NAME,SCRIPT_FILENAME do not include PATH_INFO
|
||||
fpath = "/fgci_test.php/extra/paths"
|
||||
r = newReq()
|
||||
envExpected = newEnv()
|
||||
envExpected["PATH_INFO"] = "/extra/paths"
|
||||
envExpected["SCRIPT_NAME"] = "/fgci_test.php"
|
||||
envExpected["SCRIPT_FILENAME"] = filepath.FromSlash("/fgci_test.php")
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 8. Test REQUEST_SCHEME in env
|
||||
r = newReq()
|
||||
envExpected = newEnv()
|
||||
envExpected["REQUEST_SCHEME"] = "http"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
}
|
||||
|
||||
func TestReadTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
sleep time.Duration
|
||||
readTimeout time.Duration
|
||||
shouldErr bool
|
||||
}{
|
||||
{75 * time.Millisecond, 50 * time.Millisecond, true},
|
||||
{0, -1 * time.Second, true},
|
||||
{0, time.Minute, false},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i, test := range tests {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
balancer: address(listener.Addr().String()),
|
||||
ReadTimeout: test.readTimeout,
|
||||
},
|
||||
},
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create request: %v", i, err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(test.sleep)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
wg.Done()
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := handler.ServeHTTP(w, r)
|
||||
if test.shouldErr {
|
||||
if err == nil {
|
||||
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
|
||||
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
|
||||
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
|
||||
}
|
||||
|
||||
want := http.StatusGatewayTimeout
|
||||
if got != want {
|
||||
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
|
||||
i, want, got)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
sendTimeout time.Duration
|
||||
shouldErr bool
|
||||
}{
|
||||
{-1 * time.Second, true},
|
||||
{time.Minute, false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
balancer: address(listener.Addr().String()),
|
||||
SendTimeout: test.sendTimeout,
|
||||
},
|
||||
},
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create request: %v", i, err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := handler.ServeHTTP(w, r)
|
||||
if test.shouldErr {
|
||||
if err == nil {
|
||||
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
|
||||
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
|
||||
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
|
||||
}
|
||||
|
||||
want := http.StatusGatewayTimeout
|
||||
if got != want {
|
||||
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
|
||||
i, want, got)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalancer(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"localhost", "host.local"},
|
||||
{"localhost"},
|
||||
{"localhost", "host.local", "example.com"},
|
||||
{"localhost", "host.local", "example.com", "127.0.0.1"},
|
||||
}
|
||||
for i, test := range tests {
|
||||
b := address(test...)
|
||||
for _, host := range test {
|
||||
a, err := b.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
|
||||
}
|
||||
if a != host {
|
||||
t.Errorf("Test %d: expected %s, found %s", i, host, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func address(addresses ...string) balancer {
|
||||
return &roundRobin{
|
||||
addresses: addresses,
|
||||
index: -1,
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
ini_set("display_errors",1);
|
||||
|
||||
echo "resp: start\n";//.print_r($GLOBALS,1)."\n".print_r($_SERVER,1)."\n";
|
||||
|
||||
//echo print_r($_SERVER,1)."\n";
|
||||
|
||||
$length = 0;
|
||||
$stat = "PASSED";
|
||||
|
||||
$ret = "[";
|
||||
|
||||
if (count($_POST) || count($_FILES)) {
|
||||
foreach($_POST as $key => $val) {
|
||||
$md5 = md5($val);
|
||||
|
||||
if ($key != $md5) {
|
||||
$stat = "FAILED";
|
||||
echo "server:err ".$md5." != ".$key."\n";
|
||||
}
|
||||
|
||||
$length += strlen($key) + strlen($val);
|
||||
|
||||
$ret .= $key."(".strlen($key).") ";
|
||||
}
|
||||
$ret .= "] [";
|
||||
foreach ($_FILES as $k0 => $val) {
|
||||
|
||||
$error = $val["error"];
|
||||
if ($error == UPLOAD_ERR_OK) {
|
||||
$tmp_name = $val["tmp_name"];
|
||||
$name = $val["name"];
|
||||
$datafile = "/tmp/test.go";
|
||||
move_uploaded_file($tmp_name, $datafile);
|
||||
$md5 = md5_file($datafile);
|
||||
|
||||
if ($k0 != $md5) {
|
||||
$stat = "FAILED";
|
||||
echo "server:err ".$md5." != ".$key."\n";
|
||||
}
|
||||
|
||||
$length += strlen($k0) + filesize($datafile);
|
||||
|
||||
unlink($datafile);
|
||||
$ret .= $k0."(".strlen($k0).") ";
|
||||
}
|
||||
else{
|
||||
$stat = "FAILED";
|
||||
echo "server:file err ".file_upload_error_message($error)."\n";
|
||||
}
|
||||
}
|
||||
$ret .= "]";
|
||||
echo "server:got data length " .$length."\n";
|
||||
}
|
||||
|
||||
|
||||
echo "-{$stat}-POST(".count($_POST).") FILE(".count($_FILES).")\n";
|
||||
|
||||
function file_upload_error_message($error_code) {
|
||||
switch ($error_code) {
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
return 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
return 'The uploaded file was only partially uploaded';
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
return 'No file was uploaded';
|
||||
case UPLOAD_ERR_NO_TMP_DIR:
|
||||
return 'Missing a temporary folder';
|
||||
case UPLOAD_ERR_CANT_WRITE:
|
||||
return 'Failed to write file to disk';
|
||||
case UPLOAD_ERR_EXTENSION:
|
||||
return 'File upload stopped by extension';
|
||||
default:
|
||||
return 'Unknown upload error';
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
var defaultTimeout = 60 * time.Second
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new FastCGI middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
rules, err := fastcgiParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Handler{
|
||||
Next: next,
|
||||
Rules: rules,
|
||||
Root: cfg.Root,
|
||||
FileSys: http.Dir(cfg.Root),
|
||||
SoftwareName: caddy.AppName,
|
||||
SoftwareVersion: caddy.AppVersion,
|
||||
ServerName: cfg.Addr.Host,
|
||||
ServerPort: cfg.Addr.Port,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
absRoot, err := filepath.Abs(cfg.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) < 2 || len(args) > 3 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
Root: absRoot,
|
||||
Path: args[0],
|
||||
ConnectTimeout: defaultTimeout,
|
||||
ReadTimeout: defaultTimeout,
|
||||
SendTimeout: defaultTimeout,
|
||||
}
|
||||
|
||||
upstreams := []string{args[1]}
|
||||
|
||||
srvUpstream := false
|
||||
if strings.HasPrefix(upstreams[0], "srv://") {
|
||||
srvUpstream = true
|
||||
}
|
||||
|
||||
if len(args) == 3 {
|
||||
if err := fastcgiPreset(args[2], &rule); err != nil {
|
||||
return rules, err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "root":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.Root = c.Val()
|
||||
|
||||
case "ext":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.Ext = c.Val()
|
||||
case "split":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.SplitPath = c.Val()
|
||||
case "index":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.IndexFiles = args
|
||||
|
||||
case "upstream":
|
||||
if srvUpstream {
|
||||
return rules, c.Err("additional upstreams are not supported with SRV upstream")
|
||||
}
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) != 1 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
upstreams = append(upstreams, args[0])
|
||||
case "env":
|
||||
envArgs := c.RemainingArgs()
|
||||
if len(envArgs) < 2 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
|
||||
case "except":
|
||||
ignoredPaths := c.RemainingArgs()
|
||||
if len(ignoredPaths) == 0 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.IgnoredSubPaths = ignoredPaths
|
||||
|
||||
case "connect_timeout":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.ConnectTimeout, err = time.ParseDuration(c.Val())
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
case "read_timeout":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
readTimeout, err := time.ParseDuration(c.Val())
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
rule.ReadTimeout = readTimeout
|
||||
case "send_timeout":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
sendTimeout, err := time.ParseDuration(c.Val())
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
rule.SendTimeout = sendTimeout
|
||||
}
|
||||
}
|
||||
|
||||
if srvUpstream {
|
||||
balancer, err := parseSRV(upstreams[0])
|
||||
if err != nil {
|
||||
return rules, c.Err("malformed service locator string: " + err.Error())
|
||||
}
|
||||
rule.balancer = balancer
|
||||
} else {
|
||||
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func parseSRV(locator string) (*srv, error) {
|
||||
if locator[6:] == "" {
|
||||
return nil, fmt.Errorf("%s does not include the host", locator)
|
||||
}
|
||||
|
||||
return &srv{
|
||||
service: locator[6:],
|
||||
resolver: &net.Resolver{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *Rule) error {
|
||||
switch name {
|
||||
case "php":
|
||||
rule.Ext = ".php"
|
||||
rule.SplitPath = ".php"
|
||||
rule.IndexFiles = []string{"index.php"}
|
||||
default:
|
||||
return errors.New(name + " is not a valid preset name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `fastcgi / 127.0.0.1:9000`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Handler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type , got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != "/" {
|
||||
t.Errorf("Expected / as the Path")
|
||||
}
|
||||
addr, err := myHandler.Rules[0].Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
|
||||
}
|
||||
|
||||
if addr != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].ConnectTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].ReadTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].SendTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []Rule{{
|
||||
Path: "/blog",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
except /admin /user
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
IgnoredSubPaths: []string{"/admin", "/user"},
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
send_timeout 30s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
IndexFiles: []string{},
|
||||
SendTimeout: 30 * time.Second,
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
|
||||
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
|
||||
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
|
||||
}
|
||||
for j, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
|
||||
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
actualAddr, err := actualFastcgiConfig.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d unexpected error in trying to retrieve %dth actual address: %s", i, j, err.Error())
|
||||
}
|
||||
|
||||
expectedAddr, err := test.expectedFastcgiConfig[j].Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d unexpected error in trying to retrieve %dth expected address: %s", i, j, err.Error())
|
||||
}
|
||||
|
||||
if actualAddr != expectedAddr {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, expectedAddr, actualAddr)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
|
||||
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IgnoredSubPaths) != fmt.Sprint(test.expectedFastcgiConfig[j].IgnoredSubPaths) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IgnoredSubPaths to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SendTimeout != test.expectedFastcgiConfig[j].SendTimeout {
|
||||
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SendTimeout, actualFastcgiConfig.SendTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFastCGIResolveSRV(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
locator string
|
||||
target string
|
||||
port uint16
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
`fastcgi / srv://fpm.tcp.service.consul {
|
||||
upstream yolo
|
||||
}`,
|
||||
"fpm.tcp.service.consul",
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`fastcgi / srv://fpm.tcp.service.consul`,
|
||||
"fpm.tcp.service.consul",
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
for _, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
resolver, ok := (actualFastcgiConfig.balancer).(*srv)
|
||||
if !ok {
|
||||
t.Errorf("Test %d upstream balancer is not srv", i)
|
||||
}
|
||||
resolver.resolver = buildTestResolver(test.target, test.port)
|
||||
|
||||
addr, err := actualFastcgiConfig.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d failed to retrieve upstream address. %s", i, err.Error())
|
||||
}
|
||||
|
||||
expectedAddr := fmt.Sprintf("%s:%d", test.target, test.port)
|
||||
if addr != expectedAddr {
|
||||
t.Errorf("Test %d expected upstream address to be %s, got %s", i, expectedAddr, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestResolver(target string, port uint16) srvResolver {
|
||||
return &testSRVResolver{target, port}
|
||||
}
|
||||
|
||||
type testSRVResolver struct {
|
||||
target string
|
||||
port uint16
|
||||
}
|
||||
|
||||
func (r *testSRVResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
|
||||
return "", []*net.SRV{
|
||||
{Target: r.target,
|
||||
Port: r.port,
|
||||
Priority: 1,
|
||||
Weight: 1}}, nil
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("gzip", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
|
||||
initWriterPool()
|
||||
}
|
||||
|
||||
// Gzip is a middleware type which gzips HTTP responses. It is
|
||||
// imperative that any handler which writes to a gzipped response
|
||||
// specifies the Content-Type, otherwise some clients will assume
|
||||
// application/x-gzip and try to download a file.
|
||||
type Gzip struct {
|
||||
Next httpserver.Handler
|
||||
Configs []Config
|
||||
}
|
||||
|
||||
// Config holds the configuration for Gzip middleware
|
||||
type Config struct {
|
||||
RequestFilters []RequestFilter
|
||||
ResponseFilters []ResponseFilter
|
||||
Level int // Compression level
|
||||
}
|
||||
|
||||
// ServeHTTP serves a gzipped response if the client supports it.
|
||||
func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
return g.Next.ServeHTTP(w, r)
|
||||
}
|
||||
outer:
|
||||
for _, c := range g.Configs {
|
||||
|
||||
// Check request filters to determine if gzipping is permitted for this request
|
||||
for _, filter := range c.RequestFilters {
|
||||
if !filter.ShouldCompress(r) {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
// In order to avoid unused memory allocation, gzip.putWriter only be called when gzip compression happened.
|
||||
// see https://github.com/caddyserver/caddy/issues/2395
|
||||
gz := &gzipResponseWriter{
|
||||
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
|
||||
newWriter: func() io.Writer {
|
||||
// gzipWriter modifies underlying writer at init,
|
||||
// use a discard writer instead to leave ResponseWriter in
|
||||
// original form.
|
||||
return getWriter(c.Level)
|
||||
},
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if gzWriter, ok := gz.internalWriter.(*gzip.Writer); ok {
|
||||
putWriter(c.Level, gzWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
var rw http.ResponseWriter
|
||||
// if no response filter is used
|
||||
if len(c.ResponseFilters) == 0 {
|
||||
// replace discard writer with ResponseWriter
|
||||
if gzWriter, ok := gz.Writer().(*gzip.Writer); ok {
|
||||
gzWriter.Reset(w)
|
||||
}
|
||||
rw = gz
|
||||
} else {
|
||||
// wrap gzip writer with ResponseFilterWriter
|
||||
rw = NewResponseFilterWriter(c.ResponseFilters, gz)
|
||||
}
|
||||
|
||||
// Any response in forward middleware will now be compressed
|
||||
status, err := g.Next.ServeHTTP(rw, r)
|
||||
|
||||
// If there was an error that remained unhandled, we need
|
||||
// to send something back before gzipWriter gets closed at
|
||||
// the return of this method!
|
||||
if status >= 400 {
|
||||
httpserver.DefaultErrorFunc(w, r, status)
|
||||
return 0, err
|
||||
}
|
||||
return status, err
|
||||
}
|
||||
|
||||
// no matching filter
|
||||
return g.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// gzipResponseWriter wraps the underlying Write method
|
||||
// with a gzip.Writer to compress the output.
|
||||
type gzipResponseWriter struct {
|
||||
internalWriter io.Writer
|
||||
*httpserver.ResponseWriterWrapper
|
||||
statusCodeWritten bool
|
||||
newWriter func() io.Writer
|
||||
}
|
||||
|
||||
// WriteHeader wraps the underlying WriteHeader method to prevent
|
||||
// problems with conflicting headers from proxied backends. For
|
||||
// example, a backend system that calculates Content-Length would
|
||||
// be wrong because it doesn't know it's being gzipped.
|
||||
func (w *gzipResponseWriter) WriteHeader(code int) {
|
||||
w.Header().Del("Content-Length")
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
originalEtag := w.Header().Get("ETag")
|
||||
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
|
||||
w.Header().Set("ETag", "W/"+originalEtag)
|
||||
}
|
||||
w.ResponseWriterWrapper.WriteHeader(code)
|
||||
w.statusCodeWritten = true
|
||||
}
|
||||
|
||||
// Write wraps the underlying Write method to do compression.
|
||||
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
if w.Header().Get("Content-Type") == "" {
|
||||
w.Header().Set("Content-Type", http.DetectContentType(b))
|
||||
}
|
||||
if !w.statusCodeWritten {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := w.Writer().Write(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
//Writer use a lazy way to initialize Writer
|
||||
func (w *gzipResponseWriter) Writer() io.Writer {
|
||||
if w.internalWriter == nil {
|
||||
w.internalWriter = w.newWriter()
|
||||
}
|
||||
return w.internalWriter
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var _ httpserver.HTTPInterfaces = (*gzipResponseWriter)(nil)
|
||||
@@ -1,196 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
pathFilter := PathFilter{make(Set)}
|
||||
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
|
||||
for _, p := range badPaths {
|
||||
pathFilter.IgnoredPaths.Add(p)
|
||||
}
|
||||
extFilter := ExtFilter{make(Set)}
|
||||
for _, e := range []string{".txt", ".html", ".css", ".md"} {
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
gz := Gzip{Configs: []Config{
|
||||
{RequestFilters: []RequestFilter{pathFilter, extFilter}},
|
||||
}}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
gz.Next = nextFunc(true)
|
||||
var exts = []string{
|
||||
".html", ".css", ".md",
|
||||
}
|
||||
for _, e := range exts {
|
||||
url := "/file" + e
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
w.Header().Set("ETag", `"2n9cd"`)
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// The second pass, test if the ETag is already weak
|
||||
w.Header().Set("ETag", `W/"2n9cd"`)
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
gz.Next = nextFunc(false)
|
||||
for _, p := range badPaths {
|
||||
for _, e := range exts {
|
||||
url := p + "/file" + e
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
gz.Next = nextFunc(false)
|
||||
exts = []string{
|
||||
".htm1", ".abc", ".mdx",
|
||||
}
|
||||
for _, e := range exts {
|
||||
url := "/file" + e
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// test all levels
|
||||
w = httptest.NewRecorder()
|
||||
gz.Next = nextFunc(true)
|
||||
for i := 0; i <= gzip.BestCompression; i++ {
|
||||
gz.Configs[0].Level = i
|
||||
r, err := http.NewRequest("GET", "/file.txt", nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nextFunc(shouldGzip bool) httpserver.Handler {
|
||||
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// write a relatively large text file
|
||||
b, err := ioutil.ReadFile("testdata/test.txt")
|
||||
if err != nil {
|
||||
return 500, err
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return 500, err
|
||||
}
|
||||
|
||||
if shouldGzip {
|
||||
if w.Header().Get("Content-Encoding") != "gzip" {
|
||||
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", w.Header().Get("Content-Encoding"))
|
||||
}
|
||||
if w.Header().Get("Vary") != "Accept-Encoding" {
|
||||
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", w.Header().Get("Vary"))
|
||||
}
|
||||
etag := w.Header().Get("ETag")
|
||||
if etag != "" && etag != `W/"2n9cd"` {
|
||||
return 0, fmt.Errorf("ETag must be converted to weak Etag, found %v", w.Header().Get("ETag"))
|
||||
}
|
||||
if _, ok := w.(*gzipResponseWriter); !ok {
|
||||
return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w)
|
||||
}
|
||||
if strings.Contains(w.Header().Get("Content-Type"), "application/x-gzip") {
|
||||
return 0, fmt.Errorf("Content-Type should not be gzip")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
if r.Header.Get("Accept-Encoding") == "" {
|
||||
return 0, fmt.Errorf("Accept-Encoding header expected")
|
||||
}
|
||||
if w.Header().Get("Content-Encoding") == "gzip" {
|
||||
return 0, fmt.Errorf("Content-Encoding must not be gzip, found gzip")
|
||||
}
|
||||
if _, ok := w.(*gzipResponseWriter); ok {
|
||||
return 0, fmt.Errorf("ResponseWriter should not be gzipResponseWriter")
|
||||
}
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkGzip(b *testing.B) {
|
||||
pathFilter := PathFilter{make(Set)}
|
||||
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
|
||||
for _, p := range badPaths {
|
||||
pathFilter.IgnoredPaths.Add(p)
|
||||
}
|
||||
extFilter := ExtFilter{make(Set)}
|
||||
for _, e := range []string{".txt", ".html", ".css", ".md"} {
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
gz := Gzip{Configs: []Config{
|
||||
{
|
||||
RequestFilters: []RequestFilter{pathFilter, extFilter},
|
||||
},
|
||||
}}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
gz.Next = nextFunc(true)
|
||||
url := "/file.txt"
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// RequestFilter determines if a request should be gzipped.
|
||||
type RequestFilter interface {
|
||||
// ShouldCompress tells if gzip compression
|
||||
// should be done on the request.
|
||||
ShouldCompress(*http.Request) bool
|
||||
}
|
||||
|
||||
// defaultExtensions is the list of default extensions for which to enable gzipping.
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp", ".m3u", ".m3u8", ".wasm"}
|
||||
|
||||
// DefaultExtFilter creates an ExtFilter with default extensions.
|
||||
func DefaultExtFilter() ExtFilter {
|
||||
m := ExtFilter{Exts: make(Set)}
|
||||
for _, extension := range defaultExtensions {
|
||||
m.Exts.Add(extension)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ExtFilter is RequestFilter for file name extensions.
|
||||
type ExtFilter struct {
|
||||
// Exts is the file name extensions to accept
|
||||
Exts Set
|
||||
}
|
||||
|
||||
// ExtWildCard is the wildcard for extensions.
|
||||
const ExtWildCard = "*"
|
||||
|
||||
// ShouldCompress checks if the request file extension matches any
|
||||
// of the registered extensions. It returns true if the extension is
|
||||
// found and false otherwise.
|
||||
func (e ExtFilter) ShouldCompress(r *http.Request) bool {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
return e.Exts.Contains(ExtWildCard) || e.Exts.Contains(ext)
|
||||
}
|
||||
|
||||
// PathFilter is RequestFilter for request path.
|
||||
type PathFilter struct {
|
||||
// IgnoredPaths is the paths to ignore
|
||||
IgnoredPaths Set
|
||||
}
|
||||
|
||||
// ShouldCompress checks if the request path matches any of the
|
||||
// registered paths to ignore. It returns false if an ignored path
|
||||
// is found and true otherwise.
|
||||
func (p PathFilter) ShouldCompress(r *http.Request) bool {
|
||||
return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
|
||||
return httpserver.Path(r.URL.Path).Matches(value)
|
||||
})
|
||||
}
|
||||
|
||||
// Set stores distinct strings.
|
||||
type Set map[string]struct{}
|
||||
|
||||
// Add adds an element to the set.
|
||||
func (s Set) Add(value string) {
|
||||
s[value] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove removes an element from the set.
|
||||
func (s Set) Remove(value string) {
|
||||
delete(s, value)
|
||||
}
|
||||
|
||||
// Contains check if the set contains value.
|
||||
func (s Set) Contains(value string) bool {
|
||||
_, ok := s[value]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ContainsFunc is similar to Contains. It iterates all the
|
||||
// elements in the set and passes each to f. It returns true
|
||||
// on the first call to f that returns true and false otherwise.
|
||||
func (s Set) ContainsFunc(f func(string) bool) bool {
|
||||
for k := range s {
|
||||
if f(k) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
set := make(Set)
|
||||
set.Add("a")
|
||||
if len(set) != 1 {
|
||||
t.Errorf("Expected 1 found %v", len(set))
|
||||
}
|
||||
set.Add("a")
|
||||
if len(set) != 1 {
|
||||
t.Errorf("Expected 1 found %v", len(set))
|
||||
}
|
||||
set.Add("b")
|
||||
if len(set) != 2 {
|
||||
t.Errorf("Expected 2 found %v", len(set))
|
||||
}
|
||||
if !set.Contains("a") {
|
||||
t.Errorf("Set should contain a")
|
||||
}
|
||||
if !set.Contains("b") {
|
||||
t.Errorf("Set should contain a")
|
||||
}
|
||||
set.Add("c")
|
||||
if len(set) != 3 {
|
||||
t.Errorf("Expected 3 found %v", len(set))
|
||||
}
|
||||
if !set.Contains("c") {
|
||||
t.Errorf("Set should contain c")
|
||||
}
|
||||
set.Remove("a")
|
||||
if len(set) != 2 {
|
||||
t.Errorf("Expected 2 found %v", len(set))
|
||||
}
|
||||
if set.Contains("a") {
|
||||
t.Errorf("Set should not contain a")
|
||||
}
|
||||
if !set.ContainsFunc(func(v string) bool {
|
||||
return v == "c"
|
||||
}) {
|
||||
t.Errorf("ContainsFunc should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtFilter(t *testing.T) {
|
||||
var filter RequestFilter = ExtFilter{make(Set)}
|
||||
for _, e := range []string{".txt", ".html", ".css", ".md"} {
|
||||
filter.(ExtFilter).Exts.Add(e)
|
||||
}
|
||||
r := urlRequest("file.txt")
|
||||
if !filter.ShouldCompress(r) {
|
||||
t.Errorf("Should be valid filter")
|
||||
}
|
||||
var exts = []string{
|
||||
".html", ".css", ".md",
|
||||
}
|
||||
for i, e := range exts {
|
||||
r := urlRequest("file" + e)
|
||||
if !filter.ShouldCompress(r) {
|
||||
t.Errorf("Test %v: Should be valid filter", i)
|
||||
}
|
||||
}
|
||||
exts = []string{
|
||||
".htm1", ".abc", ".mdx",
|
||||
}
|
||||
for i, e := range exts {
|
||||
r := urlRequest("file" + e)
|
||||
if filter.ShouldCompress(r) {
|
||||
t.Errorf("Test %v: Should not be valid filter", i)
|
||||
}
|
||||
}
|
||||
filter.(ExtFilter).Exts.Add(ExtWildCard)
|
||||
for i, e := range exts {
|
||||
r := urlRequest("file" + e)
|
||||
if !filter.ShouldCompress(r) {
|
||||
t.Errorf("Test %v: Should be valid filter. Wildcard used.", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathFilter(t *testing.T) {
|
||||
paths := []string{
|
||||
"/a", "/b", "/c", "/de",
|
||||
}
|
||||
var filter RequestFilter = PathFilter{make(Set)}
|
||||
for _, p := range paths {
|
||||
filter.(PathFilter).IgnoredPaths.Add(p)
|
||||
}
|
||||
for i, p := range paths {
|
||||
r := urlRequest(p)
|
||||
if filter.ShouldCompress(r) {
|
||||
t.Errorf("Test %v: Should not be valid filter", i)
|
||||
}
|
||||
}
|
||||
paths = []string{
|
||||
"/f", "/g", "/h", "/ed",
|
||||
}
|
||||
for i, p := range paths {
|
||||
r := urlRequest(p)
|
||||
if !filter.ShouldCompress(r) {
|
||||
t.Errorf("Test %v: Should be valid filter", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func urlRequest(url string) *http.Request {
|
||||
r, _ := http.NewRequest("GET", url, nil)
|
||||
return r
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ResponseFilter determines if the response should be gzipped.
|
||||
type ResponseFilter interface {
|
||||
ShouldCompress(http.ResponseWriter) bool
|
||||
}
|
||||
|
||||
// LengthFilter is ResponseFilter for minimum content length.
|
||||
type LengthFilter int64
|
||||
|
||||
// ShouldCompress returns if content length is greater than or
|
||||
// equals to minimum length.
|
||||
func (l LengthFilter) ShouldCompress(w http.ResponseWriter) bool {
|
||||
contentLength := w.Header().Get("Content-Length")
|
||||
length, err := strconv.ParseInt(contentLength, 10, 64)
|
||||
if err != nil || length == 0 {
|
||||
return false
|
||||
}
|
||||
return l != 0 && int64(l) <= length
|
||||
}
|
||||
|
||||
// SkipCompressedFilter is ResponseFilter that will discard already compressed responses
|
||||
type SkipCompressedFilter struct{}
|
||||
|
||||
// ShouldCompress returns true if served file is not already compressed
|
||||
// encodings via https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
|
||||
func (n SkipCompressedFilter) ShouldCompress(w http.ResponseWriter) bool {
|
||||
switch w.Header().Get("Content-Encoding") {
|
||||
case "gzip", "compress", "deflate", "br":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseFilterWriter validates ResponseFilters. It writes
|
||||
// gzip compressed data if ResponseFilters are satisfied or
|
||||
// uncompressed data otherwise.
|
||||
type ResponseFilterWriter struct {
|
||||
filters []ResponseFilter
|
||||
shouldCompress bool
|
||||
statusCodeWritten bool
|
||||
*gzipResponseWriter
|
||||
}
|
||||
|
||||
// NewResponseFilterWriter creates and initializes a new ResponseFilterWriter.
|
||||
func NewResponseFilterWriter(filters []ResponseFilter, gz *gzipResponseWriter) *ResponseFilterWriter {
|
||||
return &ResponseFilterWriter{filters: filters, gzipResponseWriter: gz}
|
||||
}
|
||||
|
||||
// WriteHeader wraps underlying WriteHeader method and
|
||||
// compresses if filters are satisfied.
|
||||
func (r *ResponseFilterWriter) WriteHeader(code int) {
|
||||
// Determine if compression should be used or not.
|
||||
r.shouldCompress = true
|
||||
for _, filter := range r.filters {
|
||||
if !filter.ShouldCompress(r) {
|
||||
r.shouldCompress = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if r.shouldCompress {
|
||||
// replace discard writer with ResponseWriter
|
||||
if gzWriter, ok := r.gzipResponseWriter.Writer().(*gzip.Writer); ok {
|
||||
gzWriter.Reset(r.ResponseWriter)
|
||||
}
|
||||
// use gzip WriteHeader to include and delete
|
||||
// necessary headers
|
||||
r.gzipResponseWriter.WriteHeader(code)
|
||||
} else {
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
r.statusCodeWritten = true
|
||||
}
|
||||
|
||||
// Write wraps underlying Write method and compresses if filters
|
||||
// are satisfied
|
||||
func (r *ResponseFilterWriter) Write(b []byte) (int, error) {
|
||||
if !r.statusCodeWritten {
|
||||
r.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if r.shouldCompress {
|
||||
return r.gzipResponseWriter.Write(b)
|
||||
}
|
||||
return r.ResponseWriter.Write(b)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestLengthFilter(t *testing.T) {
|
||||
var filters = []ResponseFilter{
|
||||
LengthFilter(100),
|
||||
LengthFilter(1000),
|
||||
LengthFilter(0),
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
length int64
|
||||
shouldCompress [3]bool
|
||||
}{
|
||||
{20, [3]bool{false, false, false}},
|
||||
{50, [3]bool{false, false, false}},
|
||||
{100, [3]bool{true, false, false}},
|
||||
{500, [3]bool{true, false, false}},
|
||||
{1000, [3]bool{true, true, false}},
|
||||
{1500, [3]bool{true, true, false}},
|
||||
}
|
||||
|
||||
for i, ts := range tests {
|
||||
for j, filter := range filters {
|
||||
r := httptest.NewRecorder()
|
||||
r.Header().Set("Content-Length", fmt.Sprint(ts.length))
|
||||
wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), &httpserver.ResponseWriterWrapper{ResponseWriter: r}, false, nil})
|
||||
if filter.ShouldCompress(wWriter) != ts.shouldCompress[j] {
|
||||
t.Errorf("Test %v: Expected %v found %v", i, ts.shouldCompress[j], filter.ShouldCompress(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseFilterWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
shouldCompress bool
|
||||
}{
|
||||
{"Hello\t\t\t\n", false},
|
||||
{"Hello the \t\t\t world is\n\n\n great", true},
|
||||
{"Hello \t\t\nfrom gzip", true},
|
||||
{"Hello gzip\n", false},
|
||||
}
|
||||
|
||||
filters := []ResponseFilter{
|
||||
LengthFilter(15),
|
||||
}
|
||||
|
||||
server := Gzip{Configs: []Config{
|
||||
{ResponseFilters: filters},
|
||||
}}
|
||||
|
||||
for i, ts := range tests {
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(ts.body)))
|
||||
if _, err := w.Write([]byte(ts.body)); err != nil {
|
||||
log.Println("[ERROR] failed to write response: ", err)
|
||||
}
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
r := urlRequest("/")
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if _, err := server.ServeHTTP(w, r); err != nil {
|
||||
log.Println("[ERROR] unable to serve a gzipped response: ", err)
|
||||
}
|
||||
|
||||
resp := w.Body.String()
|
||||
|
||||
if !ts.shouldCompress {
|
||||
if resp != ts.body {
|
||||
t.Errorf("Test %v: No compression expected, found %v", i, resp)
|
||||
}
|
||||
} else {
|
||||
if resp == ts.body {
|
||||
t.Errorf("Test %v: Compression expected, found %v", i, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseGzippedOutput(t *testing.T) {
|
||||
server := Gzip{Configs: []Config{
|
||||
{ResponseFilters: []ResponseFilter{SkipCompressedFilter{}}},
|
||||
}}
|
||||
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
if _, err := w.Write([]byte("gzipped")); err != nil {
|
||||
log.Println("[ERROR] failed to write response: ", err)
|
||||
}
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
r := urlRequest("/")
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
if _, err := server.ServeHTTP(w, r); err != nil {
|
||||
log.Println("[ERROR] unable to serve a gzipped response: ", err)
|
||||
}
|
||||
resp := w.Body.String()
|
||||
|
||||
if resp != "gzipped" {
|
||||
t.Errorf("Expected output not to be gzipped")
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new gzip middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := gzipParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Gzip{Next: next, Configs: configs}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gzipParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
for c.Next() {
|
||||
config := Config{}
|
||||
|
||||
// Request Filters
|
||||
pathFilter := PathFilter{IgnoredPaths: make(Set)}
|
||||
extFilter := ExtFilter{Exts: make(Set)}
|
||||
|
||||
// Response Filters
|
||||
lengthFilter := LengthFilter(0)
|
||||
|
||||
// No extra args expected
|
||||
if len(c.RemainingArgs()) > 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
exts := c.RemainingArgs()
|
||||
if len(exts) == 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, e := range exts {
|
||||
if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" {
|
||||
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
|
||||
}
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
case "not":
|
||||
paths := c.RemainingArgs()
|
||||
if len(paths) == 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, p := range paths {
|
||||
if p == "/" {
|
||||
return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`)
|
||||
}
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p)
|
||||
}
|
||||
pathFilter.IgnoredPaths.Add(p)
|
||||
}
|
||||
case "level":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
level, _ := strconv.Atoi(c.Val())
|
||||
config.Level = level
|
||||
case "min_length":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
length, err := strconv.ParseInt(c.Val(), 10, 64)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
} else if length == 0 {
|
||||
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
|
||||
}
|
||||
lengthFilter = LengthFilter(length)
|
||||
default:
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Request Filters
|
||||
config.RequestFilters = []RequestFilter{}
|
||||
|
||||
// If ignored paths are specified, put in front to filter with path first
|
||||
if len(pathFilter.IgnoredPaths) > 0 {
|
||||
config.RequestFilters = []RequestFilter{pathFilter}
|
||||
}
|
||||
|
||||
// Then, if extensions are specified, use those to filter.
|
||||
// Otherwise, use default extensions filter.
|
||||
if len(extFilter.Exts) > 0 {
|
||||
config.RequestFilters = append(config.RequestFilters, extFilter)
|
||||
} else {
|
||||
config.RequestFilters = append(config.RequestFilters, DefaultExtFilter())
|
||||
}
|
||||
|
||||
config.ResponseFilters = append(config.ResponseFilters, SkipCompressedFilter{})
|
||||
|
||||
// Response Filters
|
||||
// If min_length is specified, use it.
|
||||
if int64(lengthFilter) != 0 {
|
||||
config.ResponseFilters = append(config.ResponseFilters, lengthFilter)
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// pool gzip.Writer according to compress level
|
||||
// so we can reuse allocations over time
|
||||
var (
|
||||
writerPool = map[int]*sync.Pool{}
|
||||
defaultWriterPoolIndex int
|
||||
)
|
||||
|
||||
func initWriterPool() {
|
||||
var i int
|
||||
newWriterPool := func(level int) *sync.Pool {
|
||||
return &sync.Pool{
|
||||
New: func() interface{} {
|
||||
w, _ := gzip.NewWriterLevel(ioutil.Discard, level)
|
||||
return w
|
||||
},
|
||||
}
|
||||
}
|
||||
for i = gzip.BestSpeed; i <= gzip.BestCompression; i++ {
|
||||
writerPool[i] = newWriterPool(i)
|
||||
}
|
||||
|
||||
// add default writer pool
|
||||
defaultWriterPoolIndex = i
|
||||
writerPool[defaultWriterPoolIndex] = newWriterPool(gzip.DefaultCompression)
|
||||
}
|
||||
|
||||
func getWriter(level int) *gzip.Writer {
|
||||
index := defaultWriterPoolIndex
|
||||
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
|
||||
index = level
|
||||
}
|
||||
w := writerPool[index].Get().(*gzip.Writer)
|
||||
w.Reset(ioutil.Discard)
|
||||
return w
|
||||
}
|
||||
|
||||
func putWriter(level int, w *gzip.Writer) {
|
||||
index := defaultWriterPoolIndex
|
||||
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
|
||||
index = level
|
||||
}
|
||||
w.Close()
|
||||
writerPool[index].Put(w)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 gzip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `gzip`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if mids == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Gzip)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`gzip {`, true},
|
||||
{`gzip {}`, true},
|
||||
{`gzip a b`, true},
|
||||
{`gzip a {`, true},
|
||||
{`gzip { not f } `, true},
|
||||
{`gzip { not } `, true},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
} `, false},
|
||||
{`gzip { level 9 } `, false},
|
||||
{`gzip { ext } `, true},
|
||||
{`gzip { ext /f
|
||||
} `, true},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip`, false},
|
||||
{`gzip {
|
||||
ext ""
|
||||
}`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip { not /file1
|
||||
ext .htm
|
||||
level 3
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip { not /file1
|
||||
ext .htm
|
||||
level 3
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
min_length ab
|
||||
}
|
||||
`, true},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
min_length 1000
|
||||
}
|
||||
`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
_, err := gzipParse(caddy.NewTestController("http", test.input))
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldAddResponseFilters(t *testing.T) {
|
||||
configs, err := gzipParse(caddy.NewTestController("http", `gzip { min_length 654 }`))
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Test expected no error but found: %v", err)
|
||||
}
|
||||
filters := 0
|
||||
|
||||
for _, config := range configs {
|
||||
for _, filter := range config.ResponseFilters {
|
||||
switch filter.(type) {
|
||||
case SkipCompressedFilter:
|
||||
filters++
|
||||
case LengthFilter:
|
||||
filters++
|
||||
|
||||
if filter != LengthFilter(654) {
|
||||
t.Errorf("Expected LengthFilter to have length 654, got: %v", filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filters != 2 {
|
||||
t.Errorf("Expected 2 response filters to be registered, got: %v", filters)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-308
@@ -1,308 +0,0 @@
|
||||
Sigh view am high neat half to what. Sent late held than set why wife our. If an blessing building steepest. Agreement distrusts mrs six affection satisfied. Day blushes visitor end company old prevent chapter. Consider declared out expenses her concerns. No at indulgence conviction particular unsatiable boisterous discretion. Direct enough off others say eldest may exeter she. Possible all ignorant supplied get settling marriage recurred.
|
||||
|
||||
Boy desirous families prepared gay reserved add ecstatic say. Replied joy age visitor nothing cottage. Mrs door paid led loud sure easy read. Hastily at perhaps as neither or ye fertile tedious visitor. Use fine bed none call busy dull when. Quiet ought match my right by table means. Principles up do in me favourable affronting. Twenty mother denied effect we to do on.
|
||||
|
||||
Compliment interested discretion estimating on stimulated apartments oh. Dear so sing when in find read of call. As distrusts behaviour abilities defective is. Never at water me might. On formed merits hunted unable merely by mr whence or. Possession the unpleasing simplicity her uncommonly.
|
||||
|
||||
Bringing so sociable felicity supplied mr. September suspicion far him two acuteness perfectly. Covered as an examine so regular of. Ye astonished friendship remarkably no. Window admire matter praise you bed whence. Delivered ye sportsmen zealously arranging frankness estimable as. Nay any article enabled musical shyness yet sixteen yet blushes. Entire its the did figure wonder off.
|
||||
|
||||
Inhabit hearing perhaps on ye do no. It maids decay as there he. Smallest on suitable disposed do although blessing he juvenile in. Society or if excited forbade. Here name off yet she long sold easy whom. Differed oh cheerful procured pleasure securing suitable in. Hold rich on an he oh fine. Chapter ability shyness article welcome be do on service.
|
||||
|
||||
An sincerity so extremity he additions. Her yet there truth merit. Mrs all projecting favourable now unpleasing. Son law garden chatty temper. Oh children provided to mr elegance marriage strongly. Off can admiration prosperous now devonshire diminution law.
|
||||
|
||||
Performed suspicion in certainty so frankness by attention pretended. Newspaper or in tolerably education enjoyment. Extremity excellent certainty discourse sincerity no he so resembled. Joy house worse arise total boy but. Elderly up chicken do at feeling is. Like seen drew no make fond at on rent. Behaviour extremely her explained situation yet september gentleman are who. Is thought or pointed hearing he.
|
||||
|
||||
Not far stuff she think the jokes. Going as by do known noise he wrote round leave. Warmly put branch people narrow see. Winding its waiting yet parlors married own feeling. Marry fruit do spite jokes an times. Whether at it unknown warrant herself winding if. Him same none name sake had post love. An busy feel form hand am up help. Parties it brother amongst an fortune of. Twenty behind wicket why age now itself ten.
|
||||
|
||||
On no twenty spring of in esteem spirit likely estate. Continue new you declared differed learning bringing honoured. At mean mind so upon they rent am walk. Shortly am waiting inhabit smiling he chiefly of in. Lain tore time gone him his dear sure. Fat decisively estimating affronting assistance not. Resolve pursuit regular so calling me. West he plan girl been my then up no.
|
||||
|
||||
Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.
|
||||
|
||||
Tolerably earnestly middleton extremely distrusts she boy now not. Add and offered prepare how cordial two promise. Greatly who affixed suppose but enquire compact prepare all put. Added forth chief trees but rooms think may. Wicket do manner others seemed enable rather in. Excellent own discovery unfeeling sweetness questions the gentleman. Chapter shyness matters mr parlors if mention thought.
|
||||
|
||||
Or kind rest bred with am shed then. In raptures building an bringing be. Elderly is detract tedious assured private so to visited. Do travelling companions contrasted it. Mistress strongly remember up to. Ham him compass you proceed calling detract. Better of always missed we person mr. September smallness northward situation few her certainty something.
|
||||
|
||||
Moments its musical age explain. But extremity sex now education concluded earnestly her continual. Oh furniture acuteness suspected continual ye something frankness. Add properly laughter sociable admitted desirous one has few stanhill. Opinion regular in perhaps another enjoyed no engaged he at. It conveying he continual ye suspected as necessary. Separate met packages shy for kindness.
|
||||
|
||||
Conveying or northward offending admitting perfectly my. Colonel gravity get thought fat smiling add but. Wonder twenty hunted and put income set desire expect. Am cottage calling my is mistake cousins talking up. Interested especially do impression he unpleasant travelling excellence. All few our knew time done draw ask.
|
||||
|
||||
In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.
|
||||
|
||||
Son agreed others exeter period myself few yet nature. Mention mr manners opinion if garrets enabled. To an occasional dissimilar impossible sentiments. Do fortune account written prepare invited no passage. Garrets use ten you the weather ferrars venture friends. Solid visit seems again you nor all.
|
||||
|
||||
You vexed shy mirth now noise. Talked him people valley add use her depend letter. Allowance too applauded now way something recommend. Mrs age men and trees jokes fancy. Gay pretended engrossed eagerness continued ten. Admitting day him contained unfeeling attention mrs out.
|
||||
|
||||
Advantage old had otherwise sincerity dependent additions. It in adapted natural hastily is justice. Six draw you him full not mean evil. Prepare garrets it expense windows shewing do an. She projection advantages resolution son indulgence. Part sure on no long life am at ever. In songs above he as drawn to. Gay was outlived peculiar rendered led six.
|
||||
|
||||
Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer ladies valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is.
|
||||
|
||||
Meant balls it if up doubt small purse. Required his you put the outlived answered position. An pleasure exertion if believed provided to. All led out world these music while asked. Paid mind even sons does he door no. Attended overcame repeated it is perceive marianne in. In am think on style child of. Servants moreover in sensible he it ye possible.
|
||||
|
||||
Neat own nor she said see walk. And charm add green you these. Sang busy in this drew ye fine. At greater prepare musical so attacks as on distant. Improving age our her cordially intention. His devonshire sufficient precaution say preference middletons insipidity. Since might water hence the her worse. Concluded it offending dejection do earnestly as me direction. Nature played thirty all him.
|
||||
|
||||
Guest it he tears aware as. Make my no cold of need. He been past in by my hard. Warmly thrown oh he common future. Otherwise concealed favourite frankness on be at dashwoods defective at. Sympathize interested simplicity at do projecting increasing terminated. As edward settle limits at in.
|
||||
|
||||
Lose john poor same it case do year we. Full how way even the sigh. Extremely nor furniture fat questions now provision incommode preserved. Our side fail find like now. Discovered travelling for insensible partiality unpleasing impossible she. Sudden up my excuse to suffer ladies though or. Bachelor possible marianne directly confined relation as on he.
|
||||
|
||||
Is post each that just leaf no. He connection interested so we an sympathize advantages. To said is it shed want do. Occasional middletons everything so to. Have spot part for his quit may. Enable it is square my an regard. Often merit stuff first oh up hills as he. Servants contempt as although addition dashwood is procured. Interest in yourself an do of numerous feelings cheerful confined.
|
||||
|
||||
rnestly middleton extremely distrusts she boy now not. Add and offered prepare how cordial two promise. Greatly who affixed suppose but enquire compact prepare all put. Added forth chief trees but rooms think may. Wicket do manner others seemed enable rather in. Excellent own discovery unfeeling sweetness questions the gentleman. Chapter shyness matters mr parlors if mention thought.
|
||||
|
||||
Sudden looked elinor off gay estate nor silent. Son read such next see the rest two. Was use extent old entire sussex. Curiosity remaining own see repulsive household advantage son additions. Supposing exquisite daughters eagerness why repulsive for. Praise turned it lovers be warmly by. Little do it eldest former be if.
|
||||
|
||||
Certain but she but shyness why cottage. Gay the put instrument sir entreaties affronting. Pretended exquisite see cordially the you. Weeks quiet do vexed or whose. Motionless if no to affronting imprudence no precaution. My indulged as disposal strongly attended. Parlors men express had private village man. Discovery moonlight recommend all one not. Indulged to answered prospect it bachelor is he bringing shutters. Pronounce forfeited mr direction oh he dashwoods ye unwilling.
|
||||
|
||||
Of resolve to gravity thought my prepare chamber so. Unsatiable entreaties collecting may sympathize nay interested instrument. If continue building numerous of at relation in margaret. Lasted engage roused mother an am at. Other early while if by do to. Missed living excuse as be. Cause heard fat above first shall for. My smiling to he removal weather on anxious.
|
||||
|
||||
Tiled say decay spoil now walls meant house. My mr interest thoughts screened of outweigh removing. Evening society musical besides inhabit ye my. Lose hill well up will he over on. Increasing sufficient everything men him admiration unpleasing sex. Around really his use uneasy longer him man. His our pulled nature elinor talked now for excuse result. Admitted add peculiar get joy doubtful.
|
||||
|
||||
Had repulsive dashwoods suspicion sincerity but advantage now him. Remark easily garret nor nay. Civil those mrs enjoy shy fat merry. You greatest jointure saw horrible. He private he on be imagine suppose. Fertile beloved evident through no service elderly is. Blind there if every no so at. Own neglected you preferred way sincerity delivered his attempted. To of message cottage windows do besides against uncivil.
|
||||
|
||||
So if on advanced addition absolute received replying throwing he. Delighted consisted newspaper of unfeeling as neglected so. Tell size come hard mrs and four fond are. Of in commanded earnestly resources it. At quitting in strictly up wandered of relation answered felicity. Side need at in what dear ever upon if. Same down want joy neat ask pain help she. Alone three stuff use law walls fat asked. Near do that he help.
|
||||
|
||||
Out too the been like hard off. Improve enquire welcome own beloved matters her. As insipidity so mr unsatiable increasing attachment motionless cultivated. Addition mr husbands unpacked occasion he oh. Is unsatiable if projecting boisterous insensible. It recommend be resolving pretended middleton.
|
||||
|
||||
She literature discovered increasing how diminution understood. Though and highly the enough county for man. Of it up he still court alone widow seems. Suspected he remainder rapturous my sweetness. All vanity regard sudden nor simple can. World mrs and vexed china since after often.
|
||||
|
||||
Put all speaking her delicate recurred possible. Set indulgence inquietude discretion insensible bed why announcing. Middleton fat two satisfied additions. So continued he or commanded household smallness delivered. Door poor on do walk in half. Roof his head the what.
|
||||
|
||||
Seen you eyes son show. Far two unaffected one alteration apartments celebrated but middletons interested. Described deficient applauded consisted my me do. Passed edward two talent effect seemed engage six. On ye great do child sorry lived. Proceed cottage far letters ashamed get clothes day. Stairs regret at if matter to. On as needed almost at basket remain. By improved sensible servants children striking in surprise.
|
||||
|
||||
Living valley had silent eat merits esteem bed. In last an or went wise as left. Visited civilly am demesne so colonel he calling. So unreserved do interested increasing sentiments. Vanity day giving points within six not law. Few impression difficulty his use has comparison decisively.
|
||||
|
||||
To shewing another demands to. Marianne property cheerful informed at striking at. Clothes parlors however by cottage on. In views it or meant drift to. Be concern parlors settled or do shyness address. Remainder northward performed out for moonlight. Yet late add name was rent park from rich. He always do do former he highly.
|
||||
|
||||
Meant balls it if up doubt small purse. Required his you put the outlived answered position. An pleasure exertion if believed provided to. All led out world these music while asked. Paid mind even sons does he door no. Attended overcame repeated it is perceive marianne in. In am think on style child of. Servants moreover in sensible he it ye possible.
|
||||
|
||||
On it differed repeated wandered required in. Then girl neat why yet knew rose spot. Moreover property we he kindness greatest be oh striking laughter. In me he at collecting affronting principles apartments. Has visitor law attacks pretend you calling own excited painted. Contented attending smallness it oh ye unwilling. Turned favour man two but lovers. Suffer should if waited common person little oh. Improved civility graceful sex few smallest screened settling. Likely active her warmly has.
|
||||
|
||||
He an thing rapid these after going drawn or. Timed she his law the spoil round defer. In surprise concerns informed betrayed he learning is ye. Ignorant formerly so ye blessing. He as spoke avoid given downs money on we. Of properly carriage shutters ye as wandered up repeated moreover. Inquietude attachment if ye an solicitude to. Remaining so continued concealed as knowledge happiness. Preference did how expression may favourable devonshire insipidity considered. An length design regret an hardly barton mr figure.
|
||||
|
||||
Was certainty remaining engrossed applauded sir how discovery. Settled opinion how enjoyed greater joy adapted too shy. Now properly surprise expenses interest nor replying she she. Bore tall nay many many time yet less. Doubtful for answered one fat indulged margaret sir shutters together. Ladies so in wholly around whence in at. Warmth he up giving oppose if. Impossible is dissimilar entreaties oh on terminated. Earnest studied article country ten respect showing had. But required offering him elegance son improved informed.
|
||||
|
||||
Received overcame oh sensible so at an. Formed do change merely to county it. Am separate contempt domestic to to oh. On relation my so addition branched. Put hearing cottage she norland letters equally prepare too. Replied exposed savings he no viewing as up. Soon body add him hill. No father living really people estate if. Mistake do produce beloved demesne if am pursuit.
|
||||
|
||||
Finished her are its honoured drawings nor. Pretty see mutual thrown all not edward ten. Particular an boisterous up he reasonably frequently. Several any had enjoyed shewing studied two. Up intention remainder sportsmen behaviour ye happiness. Few again any alone style added abode ask. Nay projecting unpleasing boisterous eat discovered solicitude. Own six moments produce elderly pasture far arrival. Hold our year they ten upon. Gentleman contained so intention sweetness in on resolving.
|
||||
|
||||
Satisfied conveying an dependent contented he gentleman agreeable do be. Warrant private blushes removed an in equally totally if. Delivered dejection necessary objection do mr prevailed. Mr feeling do chiefly cordial in do. Water timed folly right aware if oh truth. Imprudence attachment him his for sympathize. Large above be to means. Dashwood do provided stronger is. But discretion frequently sir the she instrument unaffected admiration everything.
|
||||
|
||||
ndness to he horrible reserved ye. Effect twenty indeed beyond for not had county. The use him without greatly can private. Increasing it unpleasant no of contrasted no continuing. Nothing colonel my no removed in weather. It dissimilar in up devonshire inhabiting.
|
||||
|
||||
Is at purse tried jokes china ready decay an. Small its shy way had woody downs power. To denoting admitted speaking learning my exercise so in. Procured shutters mr it feelings. To or three offer house begin taken am at. As dissuade cheerful overcame so of friendly he indulged unpacked. Alteration connection to so as collecting me. Difficult in delivered extensive at direction allowance. Alteration put use diminution can considered sentiments interested discretion. An seeing feebly stairs am branch income me unable.
|
||||
|
||||
Agreed joy vanity regret met may ladies oppose who. Mile fail as left as hard eyes. Meet made call in mean four year it to. Prospect so branched wondered sensible of up. For gay consisted resolving pronounce sportsman saw discovery not. Northward or household as conveying we earnestly believing. No in up contrasted discretion inhabiting excellence. Entreaties we collecting unpleasant at everything conviction.
|
||||
|
||||
He moonlight difficult engrossed an it sportsmen. Interested has all devonshire difficulty gay assistance joy. Unaffected at ye of compliment alteration to. Place voice no arise along to. Parlors waiting so against me no. Wishing calling are warrant settled was luckily. Express besides it present if at an opinion visitor.
|
||||
|
||||
Scarcely on striking packages by so property in delicate. Up or well must less rent read walk so be. Easy sold at do hour sing spot. Any meant has cease too the decay. Since party burst am it match. By or blushes between besides offices noisier as. Sending do brought winding compass in. Paid day till shed only fact age its end.
|
||||
|
||||
Am if number no up period regard sudden better. Decisively surrounded all admiration and not you. Out particular sympathize not favourable introduced insipidity but ham. Rather number can and set praise. Distrusts an it contented perceived attending oh. Thoroughly estimating introduced stimulated why but motionless.
|
||||
|
||||
Is post each that just leaf no. He connection interested so we an sympathize advantages. To said is it shed want do. Occasional middletons everything so to. Have spot part for his quit may. Enable it is square my an regard. Often merit stuff first oh up hills as he. Servants contempt as although addition dashwood is procured. Interest in yourself an do of numerous feelings cheerful confined.
|
||||
|
||||
Two exquisite objection delighted deficient yet its contained. Cordial because are account evident its subject but eat. Can properly followed learning prepared you doubtful yet him. Over many our good lady feet ask that. Expenses own moderate day fat trifling stronger sir domestic feelings. Itself at be answer always exeter up do. Though or my plenty uneasy do. Friendship so considered remarkably be to sentiments. Offered mention greater fifteen one promise because nor. Why denoting speaking fat indulged saw dwelling raillery.
|
||||
|
||||
Sense child do state to defer mr of forty. Become latter but nor abroad wisdom waited. Was delivered gentleman acuteness but daughters. In as of whole as match asked. Pleasure exertion put add entrance distance drawings. In equally matters showing greatly it as. Want name any wise are able park when. Saw vicinity judgment remember finished men throwing.
|
||||
|
||||
Cottage out enabled was entered greatly prevent message. No procured unlocked an likewise. Dear but what she been over gay felt body. Six principles advantages and use entreaties decisively. Eat met has dwelling unpacked see whatever followed. Court in of leave again as am. Greater sixteen to forming colonel no on be. So an advice hardly barton. He be turned sudden engage manner spirit.
|
||||
|
||||
|
||||
greatest at in learning steepest. Breakfast extremity suffering one who all otherwise suspected. He at no nothing forbade up moments. Wholly uneasy at missed be of pretty whence. John way sir high than law who week. Surrounded prosperous introduced it if is up dispatched. Improved so strictly produced answered elegance is.
|
||||
|
||||
Examine she brother prudent add day ham. Far stairs now coming bed oppose hunted become his. You zealously departure had procuring suspicion. Books whose front would purse if be do decay. Quitting you way formerly disposed perceive ladyship are. Common turned boy direct and yet.
|
||||
|
||||
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
|
||||
|
||||
Real sold my in call. Invitation on an advantages collecting. But event old above shy bed noisy. Had sister see wooded favour income has. Stuff rapid since do as hence. Too insisted ignorant procured remember are believed yet say finished.
|
||||
|
||||
Cultivated who resolution connection motionless did occasional. Journey promise if it colonel. Can all mirth abode nor hills added. Them men does for body pure. Far end not horses remain sister. Mr parish is to he answer roused piqued afford sussex. It abode words began enjoy years no do no. Tried spoil as heart visit blush or. Boy possible blessing sensible set but margaret interest. Off tears are day blind smile alone had.
|
||||
|
||||
Difficulty on insensible reasonable in. From as went he they. Preference themselves me as thoroughly partiality considered on in estimating. Middletons acceptance discovered projecting so is so or. In or attachment inquietude remarkably comparison at an. Is surrounded prosperous stimulated am me discretion expression. But truth being state can she china widow. Occasional preference fat remarkably now projecting uncommonly dissimilar. Sentiments projection particular companions interested do at my delightful. Listening newspaper in advantage frankness to concluded unwilling.
|
||||
|
||||
Consulted he eagerness unfeeling deficient existence of. Calling nothing end fertile for venture way boy. Esteem spirit temper too say adieus who direct esteem. It esteems luckily mr or picture placing drawing no. Apartments frequently or motionless on reasonable projecting expression. Way mrs end gave tall walk fact bed.
|
||||
|
||||
Promotion an ourselves up otherwise my. High what each snug rich far yet easy. In companions inhabiting mr principles at insensible do. Heard their sex hoped enjoy vexed child for. Prosperous so occasional assistance it discovered especially no. Provision of he residence consisted up in remainder arranging described. Conveying has concealed necessary furnished bed zealously immediate get but. Terminated as middletons or by instrument. Bred do four so your felt with. No shameless principle dependent household do.
|
||||
|
||||
Not far stuff she think the jokes. Going as by do known noise he wrote round leave. Warmly put branch people narrow see. Winding its waiting yet parlors married own feeling. Marry fruit do spite jokes an times. Whether at it unknown warrant herself winding if. Him same none name sake had post love. An busy feel form hand am up help. Parties it brother amongst an fortune of. Twenty behind wicket why age now itself ten.
|
||||
|
||||
Fulfilled direction use continual set him propriety continued. Saw met applauded favourite deficient engrossed concealed and her. Concluded boy perpetual old supposing. Farther related bed and passage comfort civilly. Dashwoods see frankness objection abilities the. As hastened oh produced prospect formerly up am. Placing forming nay looking old married few has. Margaret disposed add screened rendered six say his striking confined.
|
||||
|
||||
At as in understood an remarkably solicitude. Mean them very seen she she. Use totally written the observe pressed justice. Instantly cordially far intention recommend estimable yet her his. Ladies stairs enough esteem add fat all enable. Needed its design number winter see. Oh be me sure wise sons no. Piqued ye of am spirit regret. Stimulated discretion impossible admiration in particular conviction up.
|
||||
|
||||
Bringing unlocked me an striking ye perceive. Mr by wound hours oh happy. Me in resolution pianoforte continuing we. Most my no spot felt by no. He he in forfeited furniture sweetness he arranging. Me tedious so to behaved written account ferrars moments. Too objection for elsewhere her preferred allowance her. Marianne shutters mr steepest to me. Up mr ignorant produced distance although is sociable blessing. Ham whom call all lain like.
|
||||
|
||||
Old education him departure any arranging one prevailed. Their end whole might began her. Behaved the comfort another fifteen eat. Partiality had his themselves ask pianoforte increasing discovered. So mr delay at since place whole above miles. He to observe conduct at detract because. Way ham unwilling not breakfast furniture explained perpetual. Or mr surrounded conviction so astonished literature. Songs to an blush woman be sorry young. We certain as removal attempt.
|
||||
|
||||
Is at purse tried jokes china ready decay an. Small its shy way had woody downs power. To denoting admitted speaking learning my exercise so in. Procured shutters mr it feelings. To or three offer house begin taken am at. As dissuade cheerful overcame so of friendly he indulged unpacked. Alteration connection to so as collecting me. Difficult in delivered extensive at direction allowance. Alteration put use diminution can considered sentiments interested discretion. An seeing feebly stairs am branch income me unable.
|
||||
|
||||
Spoke as as other again ye. Hard on to roof he drew. So sell side ye in mr evil. Longer waited mr of nature seemed. Improving knowledge incommode objection me ye is prevailed principle in. Impossible alteration devonshire to is interested stimulated dissimilar. To matter esteem polite do if.
|
||||
|
||||
Up am intention on dependent questions oh elsewhere september. No betrayed pleasure possible jointure we in throwing. And can event rapid any shall woman green. Hope they dear who its bred. Smiling nothing affixed he carried it clothes calling he no. Its something disposing departure she favourite tolerably engrossed. Truth short folly court why she their balls. Excellence put unaffected reasonable mrs introduced conviction she. Nay particular delightful but unpleasant for uncommonly who.
|
||||
|
||||
But why smiling man her imagine married. Chiefly can man her out believe manners cottage colonel unknown. Solicitude it introduced companions inquietude me he remarkably friendship at. My almost or horses period. Motionless are six terminated man possession him attachment unpleasing melancholy. Sir smile arose one share. No abroad in easily relied an whence lovers temper by. Looked wisdom common he an be giving length mr.
|
||||
|
||||
Gave read use way make spot how nor. In daughter goodness an likewise oh consider at procured wandered. Songs words wrong by me hills heard timed. Happy eat may doors songs. Be ignorant so of suitable dissuade weddings together. Least whole timed we is. An smallness deficient discourse do newspaper be an eagerness continued. Mr my ready guest ye after short at.
|
||||
|
||||
By impossible of in difficulty discovered celebrated ye. Justice joy manners boy met resolve produce. Bed head loud next plan rent had easy add him. As earnestly shameless elsewhere defective estimable fulfilled of. Esteem my advice it an excuse enable. Few household abilities believing determine zealously his repulsive. To open draw dear be by side like.
|
||||
|
||||
Be at miss or each good play home they. It leave taste mr in it fancy. She son lose does fond bred gave lady get. Sir her company conduct expense bed any. Sister depend change off piqued one. Contented continued any happiness instantly objection yet her allowance. Use correct day new brought tedious. By come this been in. Kept easy or sons my it done.
|
||||
|
||||
he who arrival end how fertile enabled. Brother she add yet see minuter natural smiling article painted. Themselves at dispatched interested insensible am be prosperous reasonably it. In either so spring wished. Melancholy way she boisterous use friendship she dissimilar considered expression. Sex quick arose mrs lived. Mr things do plenty others an vanity myself waited to. Always parish tastes at as mr father dining at.
|
||||
|
||||
Comfort reached gay perhaps chamber his six detract besides add. Moonlight newspaper up he it enjoyment agreeable depending. Timed voice share led his widen noisy young. On weddings believed laughing although material do exercise of. Up attempt offered ye civilly so sitting to. She new course get living within elinor joy. She her rapturous suffering concealed.
|
||||
|
||||
Her extensive perceived may any sincerity extremity. Indeed add rather may pretty see. Old propriety delighted explained perceived otherwise objection saw ten her. Doubt merit sir the right these alone keeps. By sometimes intention smallness he northward. Consisted we otherwise arranging commanded discovery it explained. Does cold even song like two yet been. Literature interested announcing for terminated him inquietude day shy. Himself he fertile chicken perhaps waiting if highest no it. Continued promotion has consulted fat improving not way.
|
||||
|
||||
Windows talking painted pasture yet its express parties use. Sure last upon he same as knew next. Of believed or diverted no rejoiced. End friendship sufficient assistance can prosperous met. As game he show it park do. Was has unknown few certain ten promise. No finished my an likewise cheerful packages we. For assurance concluded son something depending discourse see led collected. Packages oh no denoting my advanced humoured. Pressed be so thought natural.
|
||||
|
||||
Greatly hearted has who believe. Drift allow green son walls years for blush. Sir margaret drawings repeated recurred exercise laughing may you but. Do repeated whatever to welcomed absolute no. Fat surprise although outlived and informed shy dissuade property. Musical by me through he drawing savings an. No we stand avoid decay heard mr. Common so wicket appear to sudden worthy on. Shade of offer ye whole stood hoped.
|
||||
|
||||
In post mean shot ye. There out her child sir his lived. Design at uneasy me season of branch on praise esteem. Abilities discourse believing consisted remaining to no. Mistaken no me denoting dashwood as screened. Whence or esteem easily he on. Dissuade husbands at of no if disposal.
|
||||
|
||||
Talking chamber as shewing an it minutes. Trees fully of blind do. Exquisite favourite at do extensive listening. Improve up musical welcome he. Gay attended vicinity prepared now diverted. Esteems it ye sending reached as. Longer lively her design settle tastes advice mrs off who.
|
||||
|
||||
Alteration literature to or an sympathize mr imprudence. Of is ferrars subject as enjoyed or tedious cottage. Procuring as in resembled by in agreeable. Next long no gave mr eyes. Admiration advantages no he celebrated so pianoforte unreserved. Not its herself forming charmed amiable. Him why feebly expect future now.
|
||||
|
||||
Debating me breeding be answered an he. Spoil event was words her off cause any. Tears woman which no is world miles woody. Wished be do mutual except in effect answer. Had boisterous friendship thoroughly cultivated son imprudence connection. Windows because concern sex its. Law allow saved views hills day ten. Examine waiting his evening day passage proceed.
|
||||
|
||||
Led ask possible mistress relation elegance eat likewise debating. By message or am nothing amongst chiefly address. The its enable direct men depend highly. Ham windows sixteen who inquiry fortune demands. Is be upon sang fond must shew. Really boy law county she unable her sister. Feet you off its like like six. Among sex are leave law built now. In built table in an rapid blush. Merits behind on afraid or warmly.
|
||||
|
||||
Ignorant branched humanity led now marianne too strongly entrance. Rose to shew bore no ye of paid rent form. Old design are dinner better nearer silent excuse. She which are maids boy sense her shade. Considered reasonable we affronting on expression in. So cordial anxious mr delight. Shot his has must wish from sell nay. Remark fat set why are sudden depend change entire wanted. Performed remainder attending led fat residence far.
|
||||
|
||||
Him rendered may attended concerns jennings reserved now. Sympathize did now preference unpleasing mrs few. Mrs for hour game room want are fond dare. For detract charmed add talking age. Shy resolution instrument unreserved man few. She did open find pain some out. If we landlord stanhill mr whatever pleasure supplied concerns so. Exquisite by it admitting cordially september newspaper an. Acceptance middletons am it favourable. It it oh happen lovers afraid.
|
||||
|
||||
Had strictly mrs handsome mistaken cheerful. We it so if resolution invitation remarkably unpleasant conviction. As into ye then form. To easy five less if rose were. Now set offended own out required entirely. Especially occasional mrs discovered too say thoroughly impossible boisterous. My head when real no he high rich at with. After so power of young as. Bore year does has get long fat cold saw neat. Put boy carried chiefly shy general.
|
||||
|
||||
So delightful up dissimilar by unreserved it connection frequently. Do an high room so in paid. Up on cousin ye dinner should in. Sex stood tried walls manor truth shy and three his. Their to years so child truth. Honoured peculiar families sensible up likewise by on in.
|
||||
|
||||
Concerns greatest margaret him absolute entrance nay. Door neat week do find past he. Be no surprise he honoured indulged. Unpacked endeavor six steepest had husbands her. Painted no or affixed it so civilly. Exposed neither pressed so cottage as proceed at offices. Nay they gone sir game four. Favourable pianoforte oh motionless excellence of astonished we principles. Warrant present garrets limited cordial in inquiry to. Supported me sweetness behaviour shameless excellent so arranging.
|
||||
|
||||
Consulted he eagerness unfeeling deficient existence of. Calling nothing end fertile for venture way boy. Esteem spirit temper too say adieus who direct esteem. It esteems luckily mr or picture placing drawing no. Apartments frequently or motionless on reasonable projecting expression. Way mrs end gave tall walk fact bed.
|
||||
|
||||
Received the likewise law graceful his. Nor might set along charm now equal green. Pleased yet equally correct colonel not one. Say anxious carried compact conduct sex general nay certain. Mrs for recommend exquisite household eagerness preserved now. My improved honoured he am ecstatic quitting greatest formerly.
|
||||
|
||||
On then sake home is am leaf. Of suspicion do departure at extremely he believing. Do know said mind do rent they oh hope of. General enquire picture letters garrets on offices of no on. Say one hearing between excited evening all inhabit thought you. Style begin mr heard by in music tried do. To unreserved projection no introduced invitation.
|
||||
|
||||
At as in understood an remarkably solicitude. Mean them very seen she she. Use totally written the observe pressed justice. Instantly cordially far intention recommend estimable yet her his. Ladies stairs enough esteem add fat all enable. Needed its design number winter see. Oh be me sure wise sons no. Piqued ye of am spirit regret. Stimulated discretion impossible admiration in particular conviction up.
|
||||
|
||||
Drawings me opinions returned absolute in. Otherwise therefore sex did are unfeeling something. Certain be ye amiable by exposed so. To celebrated estimating excellence do. Coming either suffer living her gay theirs. Furnished do otherwise daughters contented conveying attempted no. Was yet general visitor present hundred too brother fat arrival. Friend are day own either lively new.
|
||||
|
||||
Situation admitting promotion at or to perceived be. Mr acuteness we as estimable enjoyment up. An held late as felt know. Learn do allow solid to grave. Middleton suspicion age her attention. Chiefly several bed its wishing. Is so moments on chamber pressed to. Doubtful yet way properly answered humanity its desirous. Minuter believe service arrived civilly add all. Acuteness allowance an at eagerness favourite in extensive exquisite ye.
|
||||
|
||||
Improved own provided blessing may peculiar domestic. Sight house has sex never. No visited raising gravity outward subject my cottage mr be. Hold do at tore in park feet near my case. Invitation at understood occasional sentiments insipidity inhabiting in. Off melancholy alteration principles old. Is do speedily kindness properly oh. Respect article painted cottage he is offices parlors.
|
||||
|
||||
One advanced diverted domestic sex repeated bringing you old. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by. Wooded ladies she basket season age her uneasy saw. Discourse unwilling am no described dejection incommode no listening of. Before nature his parish boy.
|
||||
|
||||
Am terminated it excellence invitation projection as. She graceful shy believed distance use nay. Lively is people so basket ladies window expect. Supply as so period it enough income he genius. Themselves acceptance bed sympathize get dissimilar way admiration son. Design for are edward regret met lovers. This are calm case roof and.
|
||||
|
||||
Had strictly mrs handsome mistaken cheerful. We it so if resolution invitation remarkably unpleasant conviction. As into ye then form. To easy five less if rose were. Now set offended own out required entirely. Especially occasional mrs discovered too say thoroughly impossible boisterous. My head when real no he high rich at with. After so power of young as. Bore year does has get long fat cold saw neat. Put boy carried chiefly shy general.
|
||||
|
||||
Remain valley who mrs uneasy remove wooded him you. Her questions favourite him concealed. We to wife face took he. The taste begin early old why since dried can first. Prepared as or humoured formerly. Evil mrs true get post. Express village evening prudent my as ye hundred forming. Thoughts she why not directly reserved packages you. Winter an silent favour of am tended mutual.
|
||||
|
||||
Old education him departure any arranging one prevailed. Their end whole might began her. Behaved the comfort another fifteen eat. Partiality had his themselves ask pianoforte increasing discovered. So mr delay at since place whole above miles. He to observe conduct at detract because. Way ham unwilling not breakfast furniture explained perpetual. Or mr surrounded conviction so astonished literature. Songs to an blush woman be sorry young. We certain as removal attempt.
|
||||
|
||||
Dependent certainty off discovery him his tolerably offending. Ham for attention remainder sometimes additions recommend fat our. Direction has strangers now believing. Respect enjoyed gay far exposed parlors towards. Enjoyment use tolerably dependent listening men. No peculiar in handsome together unlocked do by. Article concern joy anxious did picture sir her. Although desirous not recurred disposed off shy you numerous securing.
|
||||
|
||||
Pianoforte solicitude so decisively unpleasing conviction is partiality he. Or particular so diminution entreaties oh do. Real he me fond show gave shot plan. Mirth blush linen small hoped way its along. Resolution frequently apartments off all discretion devonshire. Saw sir fat spirit seeing valley. He looked or valley lively. If learn woody spoil of taken he cause.
|
||||
|
||||
Preserved defective offending he daughters on or. Rejoiced prospect yet material servants out answered men admitted. Sportsmen certainty prevailed suspected am as. Add stairs admire all answer the nearer yet length. Advantages prosperous remarkably my inhabiting so reasonably be if. Too any appearance announcing impossible one. Out mrs means heart ham tears shall power every.
|
||||
|
||||
So delightful up dissimilar by unreserved it connection frequently. Do an high room so in paid. Up on cousin ye dinner should in. Sex stood tried walls manor truth shy and three his. Their to years so child truth. Honoured peculiar families sensible up likewise by on in.
|
||||
|
||||
At ourselves direction believing do he departure. Celebrated her had sentiments understood are projection set. Possession ye no mr unaffected remarkably at. Wrote house in never fruit up. Pasture imagine my garrets an he. However distant she request behaved see nothing. Talking settled at pleased an of me brother weather.
|
||||
|
||||
New had happen unable uneasy. Drawings can followed improved out sociable not. Earnestly so do instantly pretended. See general few civilly amiable pleased account carried. Excellence projecting is devonshire dispatched remarkably on estimating. Side in so life past. Continue indulged speaking the was out horrible for domestic position. Seeing rather her you not esteem men settle genius excuse. Deal say over you age from. Comparison new ham melancholy son themselves.
|
||||
|
||||
Improved own provided blessing may peculiar domestic. Sight house has sex never. No visited raising gravity outward subject my cottage mr be. Hold do at tore in park feet near my case. Invitation at understood occasional sentiments insipidity inhabiting in. Off melancholy alteration principles old. Is do speedily kindness properly oh. Respect article painted cottage he is offices parlors.
|
||||
|
||||
Must you with him from him her were more. In eldest be it result should remark vanity square. Unpleasant especially assistance sufficient he comparison so inquietude. Branch one shy edward stairs turned has law wonder horses. Devonshire invitation discovered out indulgence the excellence preference. Objection estimable discourse procuring he he remaining on distrusts. Simplicity affronting inquietude for now sympathize age. She meant new their sex could defer child. An lose at quit to life do dull.
|
||||
|
||||
Style never met and those among great. At no or september sportsmen he perfectly happiness attending. Depending listening delivered off new she procuring satisfied sex existence. Person plenty answer to exeter it if. Law use assistance especially resolution cultivated did out sentiments unsatiable. Way necessary had intention happiness but september delighted his curiosity. Furniture furnished or on strangers neglected remainder engrossed.
|
||||
|
||||
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
|
||||
|
||||
By impossible of in difficulty discovered celebrated ye. Justice joy manners boy met resolve produce. Bed head loud next plan rent had easy add him. As earnestly shameless elsewhere defective estimable fulfilled of. Esteem my advice it an excuse enable. Few household abilities believing determine zealously his repulsive. To open draw dear be by side like.
|
||||
|
||||
In post mean shot ye. There out her child sir his lived. Design at uneasy me season of branch on praise esteem. Abilities discourse believing consisted remaining to no. Mistaken no me denoting dashwood as screened. Whence or esteem easily he on. Dissuade husbands at of no if disposal.
|
||||
|
||||
Passage its ten led hearted removal cordial. Preference any astonished unreserved mrs. Prosperous understood middletons in conviction an uncommonly do. Supposing so be resolving breakfast am or perfectly. Is drew am hill from mr. Valley by oh twenty direct me so. Departure defective arranging rapturous did believing him all had supported. Family months lasted simple set nature vulgar him. Picture for attempt joy excited ten carried manners talking how. Suspicion neglected he resolving agreement perceived at an.
|
||||
|
||||
Kept in sent gave feel will oh it we. Has pleasure procured men laughing shutters nay. Old insipidity motionless continuing law shy partiality. Depending acuteness dependent eat use dejection. Unpleasing astonished discovered not nor shy. Morning hearted now met yet beloved evening. Has and upon his last here must.
|
||||
|
||||
Dissuade ecstatic and properly saw entirely sir why laughter endeavor. In on my jointure horrible margaret suitable he followed speedily. Indeed vanity excuse or mr lovers of on. By offer scale an stuff. Blush be sorry no sight. Sang lose of hour then he left find.
|
||||
|
||||
Mr oh winding it enjoyed by between. The servants securing material goodness her. Saw principles themselves ten are possession. So endeavor to continue cheerful doubtful we to. Turned advice the set vanity why mutual. Reasonably if conviction on be unsatiable discretion apartments delightful. Are melancholy appearance stimulated occasional entreaties end. Shy ham had esteem happen active county. Winding morning am shyness evident to. Garrets because elderly new manners however one village she.
|
||||
|
||||
She wholly fat who window extent either formal. Removing welcomed civility or hastened is. Justice elderly but perhaps expense six her are another passage. Full her ten open fond walk not down. For request general express unknown are. He in just mr door body held john down he. So journey greatly or garrets. Draw door kept do so come on open mean. Estimating stimulated how reasonably precaution diminution she simplicity sir but. Questions am sincerity zealously concluded consisted or no gentleman it.
|
||||
|
||||
Was certainty remaining engrossed applauded sir how discovery. Settled opinion how enjoyed greater joy adapted too shy. Now properly surprise expenses interest nor replying she she. Bore tall nay many many time yet less. Doubtful for answered one fat indulged margaret sir shutters together. Ladies so in wholly around whence in at. Warmth he up giving oppose if. Impossible is dissimilar entreaties oh on terminated. Earnest studied article country ten respect showing had. But required offering him elegance son improved informed.
|
||||
|
||||
Raising say express had chiefly detract demands she. Quiet led own cause three him. Front no party young abode state up. Saved he do fruit woody of to. Met defective are allowance two perceived listening consulted contained. It chicken oh colonel pressed excited suppose to shortly. He improve started no we manners however effects. Prospect humoured mistress to by proposal marianne attended. Simplicity the far admiration preference everything. Up help home head spot an he room in.
|
||||
|
||||
Led ask possible mistress relation elegance eat likewise debating. By message or am nothing amongst chiefly address. The its enable direct men depend highly. Ham windows sixteen who inquiry fortune demands. Is be upon sang fond must shew. Really boy law county she unable her sister. Feet you off its like like six. Among sex are leave law built now. In built table in an rapid blush. Merits behind on afraid or warmly.
|
||||
|
||||
Exquisite cordially mr happiness of neglected distrusts. Boisterous impossible unaffected he me everything. Is fine loud deal an rent open give. Find upon and sent spot song son eyes. Do endeavor he differed carriage is learning my graceful. Feel plan know is he like on pure. See burst found sir met think hopes are marry among. Delightful remarkably new assistance saw literature mrs favourable.
|
||||
|
||||
Lose away off why half led have near bed. At engage simple father of period others except. My giving do summer of though narrow marked at. Spring formal no county ye waited. My whether cheered at regular it of promise blushes perhaps. Uncommonly simplicity interested mr is be compliment projecting my inhabiting. Gentleman he september in oh excellent.
|
||||
|
||||
Breakfast agreeable incommode departure it an. By ignorant at on wondered relation. Enough at tastes really so cousin am of. Extensive therefore supported by extremity of contented. Is pursuit compact demesne invited elderly be. View him she roof tell her case has sigh. Moreover is possible he admitted sociable concerns. By in cold no less been sent hard hill.
|
||||
|
||||
No in he real went find mr. Wandered or strictly raillery stanhill as. Jennings appetite disposed me an at subjects an. To no indulgence diminution so discovered mr apartments. Are off under folly death wrote cause her way spite. Plan upon yet way get cold spot its week. Almost do am or limits hearts. Resolve parties but why she shewing. She sang know now how nay cold real case.
|
||||
|
||||
For norland produce age wishing. To figure on it spring season up. Her provision acuteness had excellent two why intention. As called mr needed praise at. Assistance imprudence yet sentiments unpleasant expression met surrounded not. Be at talked ye though secure nearer.
|
||||
|
||||
Parish so enable innate in formed missed. Hand two was eat busy fail. Stand smart grave would in so. Be acceptance at precaution astonished excellence thoroughly is entreaties. Who decisively attachment has dispatched. Fruit defer in party me built under first. Forbade him but savings sending ham general. So play do in near park that pain.
|
||||
|
||||
Do am he horrible distance marriage so although. Afraid assure square so happen mr an before. His many same been well can high that. Forfeited did law eagerness allowance improving assurance bed. Had saw put seven joy short first. Pronounce so enjoyment my resembled in forfeited sportsman. Which vexed did began son abode short may. Interested astonished he at cultivated or me. Nor brought one invited she produce her.
|
||||
|
||||
Increasing impression interested expression he my at. Respect invited request charmed me warrant to. Expect no pretty as do though so genius afraid cousin. Girl when of ye snug poor draw. Mistake totally of in chiefly. Justice visitor him entered for. Continue delicate as unlocked entirely mr relation diverted in. Known not end fully being style house. An whom down kept lain name so at easy.
|
||||
|
||||
Started earnest brother believe an exposed so. Me he believing daughters if forfeited at furniture. Age again and stuff downs spoke. Late hour new nay able fat each sell. Nor themselves age introduced frequently use unsatiable devonshire get. They why quit gay cold rose deal park. One same they four did ask busy. Reserved opinions fat him nay position. Breakfast as zealously incommode do agreeable furniture. One too nay led fanny allow plate.
|
||||
|
||||
She who arrival end how fertile enabled. Brother she add yet see minuter natural smiling article painted. Themselves at dispatched interested insensible am be prosperous reasonably it. In either so spring wished. Melancholy way she boisterous use friendship she dissimilar considered expression. Sex quick arose mrs lived. Mr things do plenty others an vanity myself waited to. Always parish tastes at as mr father dining at.
|
||||
|
||||
Of be talent me answer do relied. Mistress in on so laughing throwing endeavor occasion welcomed. Gravity sir brandon calling can. No years do widow house delay stand. Prospect six kindness use steepest new ask. High gone kind calm call as ever is. Introduced melancholy estimating motionless on up as do. Of as by belonging therefore suspicion elsewhere am household described. Domestic suitable bachelor for landlord fat.
|
||||
|
||||
Advantage old had otherwise sincerity dependent additions. It in adapted natural hastily is justice. Six draw you him full not mean evil. Prepare garrets it expense windows shewing do an. She projection advantages resolution son indulgence. Part sure on no long life am at ever. In songs above he as drawn to. Gay was outlived peculiar rendered led six.
|
||||
|
||||
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
|
||||
|
||||
Still court no small think death so an wrote. Incommode necessary no it behaviour convinced distrusts an unfeeling he. Could death since do we hoped is in. Exquisite no my attention extensive. The determine conveying moonlight age. Avoid for see marry sorry child. Sitting so totally forbade hundred to.
|
||||
|
||||
Living valley had silent eat merits esteem bed. In last an or went wise as left. Visited civilly am demesne so colonel he calling. So unreserved do interested increasing sentiments. Vanity day giving points within six not law. Few impression difficulty his use has comparison decisively.
|
||||
|
||||
Subjects to ecstatic children he. Could ye leave up as built match. Dejection agreeable attention set suspected led offending. Admitting an performed supposing by. Garden agreed matter are should formed temper had. Full held gay now roof whom such next was. Ham pretty our people moment put excuse narrow. Spite mirth money six above get going great own. Started now shortly had for assured hearing expense. Led juvenile his laughing speedily put pleasant relation offering.
|
||||
|
||||
Unpacked now declared put you confined daughter improved. Celebrated imprudence few interested especially reasonable off one. Wonder bed elinor family secure met. It want gave west into high no in. Depend repair met before man admire see and. An he observe be it covered delight hastily message. Margaret no ladyship endeavor ye to settling.
|
||||
|
||||
Whole wound wrote at whose to style in. Figure ye innate former do so we. Shutters but sir yourself provided you required his. So neither related he am do believe. Nothing but you hundred had use regular. Fat sportsmen arranging preferred can. Busy paid like is oh. Dinner our ask talent her age hardly. Neglected collected an attention listening do abilities.
|
||||
|
||||
Six started far placing saw respect females old. Civilly why how end viewing attempt related enquire visitor. Man particular insensible celebrated conviction stimulated principles day. Sure fail or in said west. Right my front it wound cause fully am sorry if. She jointure goodness interest debating did outweigh. Is time from them full my gone in went. Of no introduced am literature excellence mr stimulated contrasted increasing. Age sold some full like rich new. Amounted repeated as believed in confined juvenile.
|
||||
|
||||
Suppose end get boy warrant general natural. Delightful met sufficient projection ask. Decisively everything principles if preference do impression of. Preserved oh so difficult repulsive on in household. In what do miss time be. Valley as be appear cannot so by. Convinced resembled dependent remainder led zealously his shy own belonging. Always length letter adieus add number moment she. Promise few compass six several old offices removal parties fat. Concluded rapturous it intention perfectly daughters is as.
|
||||
|
||||
Drawings me opinions returned absolute in. Otherwise therefore sex did are unfeeling something. Certain be ye amiable by exposed so. To celebrated estimating excellence do. Coming either suffer living her gay theirs. Furnished do otherwise daughters contented conveying attempted no. Was yet general visitor present hundred too brother fat arrival. Friend are day own either lively new.
|
||||
|
||||
Greatest properly off ham exercise all. Unsatiable invitation its possession nor off. All difficulty estimating unreserved increasing the solicitude. Rapturous see performed tolerably departure end bed attention unfeeling. On unpleasing principles alteration of. Be at performed preferred determine collected. Him nay acuteness discourse listening estimable our law. Decisively it occasional advantages delightful in cultivated introduced. Like law mean form are sang loud lady put.
|
||||
|
||||
Death weeks early had their and folly timed put. Hearted forbade on an village ye in fifteen. Age attended betrayed her man raptures laughter. Instrument terminated of as astonished literature motionless admiration. The affection are determine how performed intention discourse but. On merits on so valley indeed assure of. Has add particular boisterous uncommonly are. Early wrong as so manor match. Him necessary shameless discovery consulted one but.
|
||||
|
||||
Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.
|
||||
|
||||
In friendship diminution instrument so. Son sure paid door with say them. Two among sir sorry men court. Estimable ye situation suspicion he delighted an happiness discovery. Fact are size cold why had part. If believing or sweetness otherwise in we forfeited. Tolerably an unwilling arranging of determine. Beyond rather sooner so if up wishes or.
|
||||
|
||||
Abilities forfeited situation extremely my to he resembled. Old had conviction discretion understood put principles you. Match means keeps round one her quick. She forming two comfort invited. Yet she income effect edward. Entire desire way design few. Mrs sentiments led solicitude estimating friendship fat. Meant those event is weeks state it to or. Boy but has folly charm there its. Its fact ten spot drew.
|
||||
|
||||
Placing assured be if removed it besides on. Far shed each high read are men over day. Afraid we praise lively he suffer family estate is. Ample order up in of in ready. Timed blind had now those ought set often which. Or snug dull he show more true wish. No at many deny away miss evil. On in so indeed spirit an mother. Amounted old strictly but marianne admitted. People former is remove remain as.
|
||||
|
||||
Preserved defective offending he daughters on or. Rejoiced prospect yet material servants out answered men admitted. Sportsmen certainty prevailed suspected am as. Add stairs admire all answer the nearer yet length. Advantages prosperous remarkably my inhabiting so reasonably be if. Too any appearance announcing impossible one. Out mrs means heart ham tears shall power every.
|
||||
|
||||
Remain lively hardly needed at do by. Two you fat downs fanny three. True mr gone most at. Dare as name just when with it body. Travelling inquietude she increasing off impossible the. Cottage be noisier looking to we promise on. Disposal to kindness appetite diverted learning of on raptures. Betrayed any may returned now dashwood formerly. Balls way delay shy boy man views. No so instrument discretion unsatiable to in.
|
||||
|
||||
New had happen unable uneasy. Drawings can followed improved out sociable not. Earnestly so do instantly pretended. See general few civilly amiable pleased account carried. Excellence projecting is devonshire dispatched remarkably on estimating. Side in so life past. Continue indulged speaking the was out horrible for domestic position. Seeing rather her you not esteem men settle genius excuse. Deal say over you age from. Comparison new ham melancholy son themselves.
|
||||
|
||||
Oh he decisively impression attachment friendship so if everything. Whose her enjoy chief new young. Felicity if ye required likewise so doubtful. On so attention necessary at by provision otherwise existence direction. Unpleasing up announcing unpleasant themselves oh do on. Way advantage age led listening belonging supposing.
|
||||
|
||||
Now residence dashwoods she excellent you. Shade being under his bed her. Much read on as draw. Blessing for ignorant exercise any yourself unpacked. Pleasant horrible but confined day end marriage. Eagerness furniture set preserved far recommend. Did even but nor are most gave hope. Secure active living depend son repair day ladies now.
|
||||
|
||||
Sportsman delighted improving dashwoods gay instantly happiness six. Ham now amounted absolute not mistaken way pleasant whatever. At an these still no dried folly stood thing. Rapid it on hours hills it seven years. If polite he active county in spirit an. Mrs ham intention promotion engrossed assurance defective. Confined so graceful building opinions whatever trifling in. Insisted out differed ham man endeavor expenses. At on he total their he songs. Related compact effects is on settled do.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user