mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
1818 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27785f7993 | |||
| ad4191a07e | |||
| 91da965a39 | |||
| b37da03989 | |||
| 92af3ee4d8 | |||
| 1e8ab1cadf | |||
| 729e4f0239 | |||
| 790c842fad | |||
| f28a159b72 | |||
| f77a7a805a | |||
| 236341f78b | |||
| ac3bbdbd3f | |||
| ce2a9cd8f9 | |||
| 4462e3978b | |||
| 344017dc21 | |||
| a56a833423 | |||
| 6b66b19deb | |||
| 33257de2e8 | |||
| 702dec0647 | |||
| 8d1da68b47 | |||
| 7a7e3d160b | |||
| 5a1243ff42 | |||
| edf9cd34cc | |||
| f415ea263e | |||
| 3ca419e2cf | |||
| 7d15435361 | |||
| e26a855d8b | |||
| c0ce2b1d50 | |||
| 59bf71c293 | |||
| 464ade1da7 | |||
| ce47cf51be | |||
| 6be0386716 | |||
| 398d9a6bb5 | |||
| 956266cd79 | |||
| 6cabc9bfe3 | |||
| da674fd599 | |||
| 4e1229e7c9 | |||
| 5341c85a27 | |||
| fbd6412359 | |||
| 36d2027493 | |||
| a148b92381 | |||
| 36a62f0915 | |||
| d85e90a7b4 | |||
| d5cc10f7aa | |||
| 96bfb9f347 | |||
| 5e48f0a412 | |||
| 18c93756b4 | |||
| cfe52084aa | |||
| 6aa0e30af3 | |||
| 5a41e8bc1a | |||
| 9e4eeb4fb7 | |||
| c62b6b9f1a | |||
| 52584f7f23 | |||
| 2be0dc40f0 | |||
| e3e62a952d | |||
| 6bc3e7536e | |||
| df9d062a8f | |||
| eafbf0b218 | |||
| 73d52490d0 | |||
| 4a095590b1 | |||
| c8514ad7b7 | |||
| e3f2d96a5e | |||
| bcddfb2daa | |||
| 75ccc05d84 | |||
| 0a0d2cc1cf | |||
| 50749b4e84 | |||
| 06873175bf | |||
| f49e0c9b56 | |||
| ccdc28631a | |||
| a2c410b8e1 | |||
| 73794f2a2c | |||
| 4b877eebc4 | |||
| c4842e0fc1 | |||
| ff8c430ff0 | |||
| 1262ae92e9 | |||
| 6083871088 | |||
| ce3580bf91 | |||
| 9720da5bc8 | |||
| 286d8d1e89 | |||
| 977a3c3226 | |||
| 82cbd7a96b | |||
| cdf7cf5c3f | |||
| 579007822f | |||
| e50de809a5 | |||
| c37481cc7b | |||
| 91ff734327 | |||
| 524dcee9f6 | |||
| 0cc48e849c | |||
| 58b2edd229 | |||
| 6271abb22a | |||
| 58053fce48 | |||
| 55bded68c2 | |||
| dc3efc939c | |||
| bdb61f4a1d | |||
| 1183d91c7b | |||
| 463c9d9dd2 | |||
| 1bd9e9e590 | |||
| b650a26727 | |||
| 943ed931db | |||
| 2417d70bcb | |||
| 1a7612071a | |||
| 5072d70f38 | |||
| b210101f45 | |||
| 18edf5864e | |||
| ce7d3db1be | |||
| f32eed1912 | |||
| cdb79a60f2 | |||
| 7419573266 | |||
| d8f92baee2 | |||
| 9e9298ee5d | |||
| dc6c986b3f | |||
| 65cb966d38 | |||
| d264a2cf0a | |||
| 139a3cfb13 | |||
| 04da9c7374 | |||
| 16250da3f0 | |||
| 45a0e4cf49 | |||
| e14a62f188 | |||
| 94e382ef0a | |||
| d8d339740b | |||
| 205aee6662 | |||
| 62fea30e87 | |||
| bbee961415 | |||
| 59e6ceb518 | |||
| 82929b122a | |||
| 38c76647c9 | |||
| 696b46f075 | |||
| e5ef285e59 | |||
| 11adb2e5a7 | |||
| 9369b81498 | |||
| 0e34c7c970 | |||
| eeb23a2469 | |||
| ecf852ea43 | |||
| 6bac558c98 | |||
| ae10122f7e | |||
| c6ba43f888 | |||
| 8464020f7c | |||
| 0155b0c5fb | |||
| 21d92d6873 | |||
| 696792781a | |||
| 601838ac96 | |||
| 8048e9c3bc | |||
| fab3b5bf19 | |||
| c7c34266da | |||
| d6a35381e9 | |||
| eee9d00255 | |||
| 6a84d9392e | |||
| 633567744d | |||
| c3523305f0 | |||
| 3f770603bc | |||
| 54acb9b2de | |||
| 8b9c9efdba | |||
| a1a8d0f655 | |||
| 5d813a1b58 | |||
| 04bee0f36d | |||
| 7cbbb01f94 | |||
| 466efb7e67 | |||
| d98a7aad0f | |||
| 4babe4b201 | |||
| 533039e6d8 | |||
| b857265f9c | |||
| 153d4a5ac6 | |||
| d5fe4928f2 | |||
| 20483c23f8 | |||
| 9f9ad21aaa | |||
| 53635ba538 | |||
| 6352c9054a | |||
| e641d2fd65 | |||
| 1da70d3ba1 | |||
| 3198200479 | |||
| 7dc1dc1c78 | |||
| a3aa414ff3 | |||
| 54c63002cc | |||
| c555e95366 | |||
| 466269fd10 | |||
| 8653b70c32 | |||
| e363491a28 | |||
| b0685a64be | |||
| ff98aa3dd0 | |||
| c212365a3a | |||
| fea0d5ac3a | |||
| 9f16ac84a0 | |||
| 5874fbeb7e | |||
| 17e7e6076a | |||
| 9e98d6cd52 | |||
| 76f12f475b | |||
| 32fa0ce6a0 | |||
| 80eb45fcfb | |||
| 36f8759a7b | |||
| e2917784d0 | |||
| fec840a861 | |||
| d0bf3e1647 | |||
| b8722d9af3 | |||
| 7dc23b18ae | |||
| 9d398adf5d | |||
| 22a266a259 | |||
| 5a6b765673 | |||
| 8acf043297 | |||
| 98c17bcdf2 | |||
| 8c392a02c8 | |||
| 30337ac33f | |||
| b783caaaed | |||
| c972ea39c8 | |||
| 12fd349916 | |||
| dd4c4d7eb6 | |||
| 0cdaaba4b8 | |||
| 63f749112b | |||
| e19a007b38 | |||
| e85ba0d4db | |||
| b89cbe18e2 | |||
| 14500d8204 | |||
| a2900e46f4 | |||
| 08c17c7c31 | |||
| 49cb225cbd | |||
| 53e117802f | |||
| 23f89f30e9 | |||
| ff32ade1d8 | |||
| 3ce9075d3d | |||
| f2e999aab2 | |||
| 8cc3416bbc | |||
| c4d64a418b | |||
| f3108bb7bf | |||
| c2853ea64b | |||
| 561069fdb6 | |||
| 66a07773ab | |||
| a1dd6f0b34 | |||
| 2b9bbc5236 | |||
| 2f5e840ea9 | |||
| ab3cc8f961 | |||
| efb1c54e13 | |||
| 3c1957a612 | |||
| 2bd6fd0aea | |||
| f1342e37ed | |||
| 94af37087b | |||
| 5fcfdab6c7 | |||
| 016384abef | |||
| 036633b64a | |||
| 550b1170bd | |||
| d44016b937 | |||
| 4baca884c5 | |||
| d0455c7b9c | |||
| e5d33e73f3 | |||
| 9ced4b17e5 | |||
| b48bda4a6d | |||
| 5d9989405a | |||
| 733f622f7a | |||
| 20a54d0e07 | |||
| b5a07d43fa | |||
| 8561f42786 | |||
| e1ea58b7c4 | |||
| e9ce45ce61 | |||
| cc638c7faa | |||
| b766dab9fa | |||
| c885edda24 | |||
| bb7787d2ee | |||
| 8620581f95 | |||
| 99a6b2db67 | |||
| 8944332e13 | |||
| be1c57acfe | |||
| b06b3981cf | |||
| 8cb4e90852 | |||
| 871d11af00 | |||
| 6397a85e50 | |||
| d0ddfc849d | |||
| 617012c3fb | |||
| 4adbcd2565 | |||
| d01bcd591c | |||
| c9b022b5e0 | |||
| a661007a55 | |||
| 0c0142c8cc | |||
| 37f05e450f | |||
| 9b9a77a160 | |||
| 4670d13c8c | |||
| 9077cce126 | |||
| 76d9d695be | |||
| a4d70262aa | |||
| 79f2deee42 | |||
| bac54de9eb | |||
| 3f83eccfbd | |||
| d60a26ae30 | |||
| bbf954cbf2 | |||
| 73916ccc30 | |||
| fcad474064 | |||
| 4449d3dcd9 | |||
| c4a177bd3b | |||
| 8ecd543519 | |||
| 7af499c28b | |||
| 64fd281f5b | |||
| bedad34b25 | |||
| 0e7635c54b | |||
| 40a3a6b24f | |||
| 09a1f02971 | |||
| 8e54d5cecb | |||
| 7ef405f9b2 | |||
| 11bf28f783 | |||
| 98bba33861 | |||
| abdf13ea30 | |||
| a251831feb | |||
| 1ea96def31 | |||
| 635714fe38 | |||
| 5f135a27d5 | |||
| 45a3d0b526 | |||
| a122304196 | |||
| 14a6e4b4ed | |||
| 72e4ba8b5b | |||
| 1991083322 | |||
| 7ba804353c | |||
| ac933f1685 | |||
| 20ee457cae | |||
| 34a99598f7 | |||
| 191ec27c26 | |||
| b1ae8a71f1 | |||
| d4f4fcdb4c | |||
| ef58536711 | |||
| 17709a7d3f | |||
| 5a691fbaf5 | |||
| fd3008459e | |||
| e7af23e1e6 | |||
| 536daf36be | |||
| 5e0f4083c4 | |||
| c3e0733406 | |||
| 70cbfdc585 | |||
| 3dc98c8ce3 | |||
| 151d0baa94 | |||
| 9d947713ff | |||
| 1dfe1e5ada | |||
| 628920e20e | |||
| 15d25f1ca4 | |||
| 2ef8905966 | |||
| fdad616df7 | |||
| 590862a962 | |||
| 40c09d6789 | |||
| bba1059ef9 | |||
| 1d3212a598 | |||
| c75ee0000e | |||
| 8cdc65edd2 | |||
| a609fa5f56 | |||
| 78341a3a9a | |||
| fdc62d015f | |||
| e8e55955f4 | |||
| 8b8afd72d7 | |||
| c5524b0bab | |||
| c5aa5843d9 | |||
| 745ae6ff2f | |||
| 432a2d23a7 | |||
| 83345062d7 | |||
| f372f5fce7 | |||
| 454b1e3939 | |||
| 45ac11088e | |||
| eb3bbc409f | |||
| b830667a25 | |||
| ba5aeab19d | |||
| 441a8f5eff | |||
| 4f6500c95b | |||
| 7dd385f6b4 | |||
| ac0dd303be | |||
| 676202a31e | |||
| c8a99d2f81 | |||
| 8e8e2f596d | |||
| f7003bee3f | |||
| 532ab661c7 | |||
| 68be4a9161 | |||
| 46bc0d5c4e | |||
| 8e75ae2495 | |||
| d56ac28bec | |||
| 3fd8218f67 | |||
| d06c15cae6 | |||
| 59b1e8b0bc | |||
| dbd76f7a57 | |||
| e081d8b5c2 | |||
| 8eefeb6788 | |||
| 5fb3c504c9 | |||
| 0f04f2fd44 | |||
| ce8b1dfe94 | |||
| 4b3c532573 | |||
| 4d76ccb1c4 | |||
| de7bf4f241 | |||
| 681c95a749 | |||
| e5a8927635 | |||
| 2019eec5a5 | |||
| 33d1033928 | |||
| 0d8b95334f | |||
| ee615371a8 | |||
| 4c6082df64 | |||
| 8898066455 | |||
| fffc1bed73 | |||
| 824ec6cb95 | |||
| 5b5e365295 | |||
| c6c221b8db | |||
| 985049e0c2 | |||
| 3a4f8e8d0c | |||
| f3a3bf6204 | |||
| 81a3101efe | |||
| 22a4b6cde2 | |||
| 94c63e42d6 | |||
| c110b27ef5 | |||
| 6e9439d22e | |||
| f4cdf53761 | |||
| 89f5b646c3 | |||
| a24e361761 | |||
| 5ac04b91bb | |||
| 1b1aecb1e6 | |||
| 3d43c5b697 | |||
| d534a2139f | |||
| c4e65df262 | |||
| 88d3dcae42 | |||
| db4cd8ee2d | |||
| da5b3cfc50 | |||
| 372c77da3a | |||
| 251c38bfb2 | |||
| ba1bee2b8f | |||
| b64894c31e | |||
| d88dd74dec | |||
| 7157bdc79d | |||
| 72af3f8256 | |||
| c8daaba4be | |||
| af48bbd234 | |||
| 1e1e69b90f | |||
| cf1b355d30 | |||
| 1dd413bd69 | |||
| 1bbad72ff1 | |||
| b2aed643f4 | |||
| 62e8c4b76b | |||
| 6490ff6224 | |||
| 57710e8b0d | |||
| 4678471fe0 | |||
| d746b95906 | |||
| 3c8b2b5954 | |||
| cf3ce49104 | |||
| ca3d23bc70 | |||
| e7c842215e | |||
| beae16f07c | |||
| 1240690973 | |||
| b35d19d78e | |||
| cf4e0c9c9c | |||
| ac97cf426f | |||
| f28af63732 | |||
| 38c2463416 | |||
| df018ea64a | |||
| 4ff46ad447 | |||
| 59c6513b31 | |||
| aede4ccbce | |||
| 9315738dab | |||
| 502a8979a8 | |||
| d6110f8e9e | |||
| d7698ecf13 | |||
| 9ea0591951 | |||
| ffafb2eca8 | |||
| 6bb1e0c674 | |||
| 6f37e9d31b | |||
| b58872925a | |||
| 8d7136fc06 | |||
| 2125ae5f99 | |||
| 3fd3feeffe | |||
| 62622eb853 | |||
| 87c389f73d | |||
| cf03c9a6c8 | |||
| 48abb41135 | |||
| 7eb4bb8e1c | |||
| 39e55072d7 | |||
| 88a2811e2a | |||
| 065eeb42c3 | |||
| d4b10b69a7 | |||
| f77264b776 | |||
| ad2ed5b0ae | |||
| fdb6d64f9d | |||
| 227664336e | |||
| 32329a473d | |||
| e5bf8cab24 | |||
| 6db4771aa8 | |||
| b1cd0bfeff | |||
| 2e84fe4504 | |||
| d2be213e10 | |||
| a1bc94e409 | |||
| 80dd95a495 | |||
| 5a45719227 | |||
| 345ece3850 | |||
| 2b44a7d052 | |||
| 58085edc16 | |||
| 6f05faa670 | |||
| eddb6f0a79 | |||
| 70b75d1433 | |||
| 15fa5cf2da | |||
| e74678ed43 | |||
| d84c823855 | |||
| b49f65d5de | |||
| fd8fe24bcb | |||
| 281603895b | |||
| fbad4e15c2 | |||
| ab301fec00 | |||
| deec149891 | |||
| 9ca87cd139 | |||
| cad9b3f62f | |||
| e585a74115 | |||
| d9b6563d88 | |||
| 0a3f68f0d7 | |||
| e625c7c051 | |||
| 937654d1e0 | |||
| 33aba7eb91 | |||
| d252d40681 | |||
| 81c4ea6be7 | |||
| 1fdc46e571 | |||
| a798e0c951 | |||
| 07b7c99965 | |||
| 807617965a | |||
| a50462974c | |||
| 6fe5c1a69f | |||
| 54355d8fb3 | |||
| e486c9c6e7 | |||
| 0f1e5bcebf | |||
| fee4890e94 | |||
| b14baf7e20 | |||
| 2b06edccd3 | |||
| 492d5aa37f | |||
| 1e4a4109a7 | |||
| daa4de572e | |||
| 83451ea2a0 | |||
| 06fed0db17 | |||
| 98cf26377c | |||
| ff82057131 | |||
| 6492592b4a | |||
| 6c847d0723 | |||
| 01e05afa0c | |||
| e7fc26e3fb | |||
| 37ae21001d | |||
| b23eec4fac | |||
| 727ef24306 | |||
| d3860f95f5 | |||
| 9b4134b287 | |||
| ddff08392a | |||
| 71c14fa16e | |||
| ff22fbd79a | |||
| a762dde145 | |||
| 416af05a00 | |||
| 2f92443de7 | |||
| 49fdc6a20a | |||
| ac4fa2c3a9 | |||
| e1a6b60736 | |||
| 9b5ad487d7 | |||
| d291b76721 | |||
| 881da313dd | |||
| 1bdbf9d6ba | |||
| 2536ea74d9 | |||
| 9acfec5418 | |||
| a0e6eb3ba9 | |||
| 6e6d9e7e9e | |||
| 238250e7e6 | |||
| 324ec15890 | |||
| c64361a753 | |||
| 6d9dcb1729 | |||
| da97ac7c63 | |||
| 374d0a3f09 | |||
| bee9c50a71 | |||
| bac29cc20a | |||
| e6b1028da9 | |||
| 4c62397ff8 | |||
| e516aebc08 | |||
| da8ae9e511 | |||
| d377c79a5d | |||
| 389a6eb344 | |||
| 85d793ce88 | |||
| 1f29c52151 | |||
| 9705f34970 | |||
| db21b0319d | |||
| 25b934824f | |||
| c23c6d9cb4 | |||
| 9697c47e21 | |||
| 39030d9e1b | |||
| 61c7a51bfa | |||
| 45e783c3f9 | |||
| 2bccc1466e | |||
| a3af232dc5 | |||
| 04089c533b | |||
| e0bc426050 | |||
| bd2a33dd14 | |||
| 20dfaab703 | |||
| 249c9a17f5 | |||
| c431a07af5 | |||
| e2234497b7 | |||
| 96425f0f40 | |||
| d05dac8d2e | |||
| 81e26970a3 | |||
| f561dc0bc1 | |||
| 21382702d2 | |||
| e97649493b | |||
| 19d6f666aa | |||
| 6f5cff5393 | |||
| 5c96ee1d9c | |||
| 3c578dfbc1 | |||
| a093aea797 | |||
| 9f1762873a | |||
| c3417a0757 | |||
| 72bc6932b0 | |||
| a41e3d2515 | |||
| 7f35600b28 | |||
| cc6aa6b54b | |||
| 239f6825f7 | |||
| 1d38d113f8 | |||
| 6908136092 | |||
| da016f8d5a | |||
| 2f2d357fb6 | |||
| 924b53eb3c | |||
| 2b51be7fd7 | |||
| 376e1090a3 | |||
| dd4de698cf | |||
| a682100c5e | |||
| aba3d37c88 | |||
| 0890e330e2 | |||
| 19a85d08c6 | |||
| 5a0d373fcd | |||
| ecf91f525f | |||
| b541c717ca | |||
| c05c5163e2 | |||
| 3513b6f2f7 | |||
| 4a6121f989 | |||
| e652d12cfc | |||
| b97a7909d8 | |||
| 7c9867917a | |||
| a762bec06d | |||
| b75016e646 | |||
| ddf4b1fd3b | |||
| 69c2d78f69 | |||
| f31875dfde | |||
| 4e98cc3005 | |||
| d3a77ce3c3 | |||
| 48d294a695 | |||
| b149a86bc2 | |||
| ac80f6edc3 | |||
| ef95173827 | |||
| 36a3e204b6 | |||
| e0b63d92f4 | |||
| 004a7f84ef | |||
| ed8a48e7f1 | |||
| c86c26a056 | |||
| 0a7ca64f53 | |||
| c4e2cf96e7 | |||
| 42b7d57421 | |||
| c64cf218b0 | |||
| 027f697fdf | |||
| 6a7b777f14 | |||
| 67b137175e | |||
| dfa3b8645d | |||
| 2dca50dee8 | |||
| 3faad41b43 | |||
| c21ff8343c | |||
| 2072eec11f | |||
| 497ebb9ccb | |||
| e4e773c9ea | |||
| 32e63e6b94 | |||
| 86ccafbe58 | |||
| 3ef78d3db3 | |||
| 9ec1c17846 | |||
| 7ababfc909 | |||
| 31062dd6c2 | |||
| b952fd8f8f | |||
| 28e0bfbbbe | |||
| 30ce73e8fb | |||
| 987a5f98c4 | |||
| 859a93d296 | |||
| a14fce0b1e | |||
| 25b567b301 | |||
| 3d066789d3 | |||
| 93d982a5a4 | |||
| 572b9e4d67 | |||
| 2a82f7b520 | |||
| 1a9f700287 | |||
| 462128cd80 | |||
| cf69d190a2 | |||
| 3441cdef64 | |||
| 8a2f2f8d37 | |||
| 86854dca89 | |||
| b3a5b725db | |||
| f28d8b8601 | |||
| 5989eb0635 | |||
| cbd9b814b9 | |||
| 32dbbfd64c | |||
| 3395f6c775 | |||
| 61cf8b79bc | |||
| bb6764fd22 | |||
| c981b08b23 | |||
| 7271b57136 | |||
| 874bcff564 | |||
| eb279e7e8a | |||
| f4c729bd22 | |||
| ea35893be4 | |||
| 1efd1029dd | |||
| 426d165254 | |||
| a3127bed5f | |||
| b94e513116 | |||
| b6e5a599fb | |||
| 8fc35edc3b | |||
| 260c023e1e | |||
| 27f9b58c5d | |||
| f23d8cb37f | |||
| 3f49b32086 | |||
| 0aacaea918 | |||
| 9e0b1b4216 | |||
| e7001e6538 | |||
| 4d9741dda6 | |||
| 74a5cb2fe3 | |||
| ba2e9d80fd | |||
| a05a664d56 | |||
| 9f9fbf2e1b | |||
| 63e4352db7 | |||
| 640a0ef956 | |||
| 591b209024 | |||
| f1c1ea9905 | |||
| 6b801b111b | |||
| 717c88ec0f | |||
| 03a22aeb7e | |||
| 18332df358 | |||
| b9f8c183fa | |||
| 37d050922b | |||
| 04514fb791 | |||
| 6c2bf36dab | |||
| 4f5fe2de24 | |||
| 90c24d2f32 | |||
| d95c21ded5 | |||
| 4f4b34d481 | |||
| ed0342f171 | |||
| f14cdcc436 | |||
| b471b7e835 | |||
| b79ff7403f | |||
| 7560778602 | |||
| fc10951dde | |||
| 3e48e6a535 | |||
| 44fc9b18a6 | |||
| 3b6c387b84 | |||
| 35e4c1a7bf | |||
| 25bfdfe92c | |||
| 008ad398ce | |||
| 52d7379063 | |||
| e92a911e7d | |||
| 84845a66ab | |||
| e2f6ab3472 | |||
| f3a183ecc1 | |||
| e958686ae4 | |||
| 1f7d8d8ab0 | |||
| a7766c9033 | |||
| ce8ee831b3 | |||
| 741d7685f1 | |||
| 88e3a26c99 | |||
| f52b1e80f5 | |||
| 202679efde | |||
| 75915e0a25 | |||
| 9e386fc921 | |||
| 9099375b11 | |||
| 36b440c04b | |||
| 2a46f2a14e | |||
| 741880a38b | |||
| 43c339c7e3 | |||
| 49c2807ba1 | |||
| da08c94a8c | |||
| c827a71d5d | |||
| 2ecc837020 | |||
| c37ad7f677 | |||
| 737c7c4372 | |||
| 367397dbd6 | |||
| a2dbfdc10e | |||
| ef5f9c771d | |||
| 05957b4965 | |||
| 72fcdec8d8 | |||
| f4bb43781c | |||
| 197297b0d7 | |||
| a541eb7899 | |||
| 2ea6c95ac4 | |||
| c7674e2060 | |||
| c12847e5ba | |||
| bec130a563 | |||
| 09b7ce6c93 | |||
| b860be01bb | |||
| f7b5187bf3 | |||
| 09a7af8cae | |||
| 5f2670fdde | |||
| ecf913e58d | |||
| d05f89294e | |||
| 1ef7f3c4b1 | |||
| f25ae8230f | |||
| 1cfd960f3c | |||
| 2dba44327a | |||
| cae9f7de9c | |||
| a11e14aca8 | |||
| dc63e50172 | |||
| 04c7c442c5 | |||
| 7bd2adf0dc | |||
| 1fe39e4633 | |||
| 216a617249 | |||
| d25a3e95e4 | |||
| 11103bd8d6 | |||
| f1ba7fa343 | |||
| 7091a2090b | |||
| 57ffe5a619 | |||
| b1208d3fdf | |||
| b089d14b67 | |||
| e72fc20c78 | |||
| 5b7e0361dd | |||
| 86f36bdb61 | |||
| 3278106421 | |||
| f9b8e31ad7 | |||
| fbdfc979ec | |||
| 2acaf2fa6f | |||
| f4fcfa8793 | |||
| 79db939259 | |||
| f9b6ede92b | |||
| 184abe3bc8 | |||
| fde9bbeb32 | |||
| c59fd1c76e | |||
| 600ee9a89f | |||
| c5983e305f | |||
| 8d057c8614 | |||
| ac197f1694 | |||
| d8be787f39 | |||
| a8c8b48390 | |||
| 4d4ea94465 | |||
| aeaf58b16a | |||
| 73ed286309 | |||
| 9e900b0a08 | |||
| f1b2637d44 | |||
| 178c4d11d9 | |||
| 7613ae3bf0 | |||
| ad664e5bba | |||
| cf06abd691 | |||
| a6abec8210 | |||
| 82b049229b | |||
| fae612d53b | |||
| bae4ac9764 | |||
| 6e340cb1d6 | |||
| 0d8d0ba5a0 | |||
| 8655ea671b | |||
| 0d8526b7d9 | |||
| c9e0517e5e | |||
| e74558eaea | |||
| b0ccab7b4a | |||
| 47079c3d24 | |||
| b4cab78bec | |||
| 3c96718027 | |||
| 4b6e0e9369 | |||
| 2bcbdd6a17 | |||
| 8f2196c047 | |||
| c7d4d051cb | |||
| e283af4d9b | |||
| 12cd2d528c | |||
| 8a6c778c8d | |||
| 77eae62d9f | |||
| 97c8c9582a | |||
| ed0c0db6a3 | |||
| 202849055c | |||
| 060ab92d29 | |||
| 0830c728fe | |||
| dab679df86 | |||
| 9453224639 | |||
| fd1765973a | |||
| 0efe39a705 | |||
| a3f3bc67e1 | |||
| 8b93bfe751 | |||
| 897b6c5b0e | |||
| fc928e0b3b | |||
| 93b301372b | |||
| ce4981d046 | |||
| 62b210b544 | |||
| 5f6a0a4c0b | |||
| cae9880800 | |||
| 6d49392602 | |||
| 4593982065 | |||
| 94100a7ba6 | |||
| e9c2e50684 | |||
| 82b0c0b9eb | |||
| 55601d3ec2 | |||
| 829a0f34d0 | |||
| bb80f99190 | |||
| 946ff5e87b | |||
| 0a04fa40f4 | |||
| 48d7f1ead2 | |||
| be2f5c4b38 | |||
| 281007c482 | |||
| b6326d402d | |||
| e2a3ec4c3d | |||
| 3468986260 | |||
| 55f69fd742 | |||
| 1af7865e6c | |||
| 4636ca1051 | |||
| 94e3e7e5eb | |||
| 3c086fb2e6 | |||
| 55aa492dc1 | |||
| 7dadcd5834 | |||
| 73327e784d | |||
| bb23f68a43 | |||
| 6a27968f73 | |||
| 1e7ec3397b | |||
| 168723a026 | |||
| 92bd914418 | |||
| 9110dc4745 | |||
| 1ed786f836 | |||
| 4d5bc9fa6c | |||
| 98d8c0f81b | |||
| 32b8857eea | |||
| 9e163a655d | |||
| 4d867e848b | |||
| c748ef944b | |||
| 55d22f4ead | |||
| 3f787a20e3 | |||
| 6276be4e90 | |||
| f639d3cd68 | |||
| 43020533f7 | |||
| a5836aebfa | |||
| 3dd4c0eb6a | |||
| 1e27b5be89 | |||
| a946d65fe6 | |||
| f04ff063ed | |||
| 35ec61cc88 | |||
| 8f23c430ae | |||
| 5eadea6615 | |||
| 34d3cd7c92 | |||
| f11cd4d9dd | |||
| a8c9d47805 | |||
| 3966936bd6 | |||
| b7fd1f4e9e | |||
| b0397df719 | |||
| eb48885d4d | |||
| afbda595f6 | |||
| b65ddbc750 | |||
| aba0ae358e | |||
| 2e295b51b3 | |||
| 59dbea768c | |||
| a44d59f1e5 | |||
| e4ff77ed07 | |||
| b6c4178f0a | |||
| 23631cfaca | |||
| 8631f33940 | |||
| ab5087e215 | |||
| d56a9a1c5d | |||
| 41bdd77545 | |||
| b7827a342a | |||
| e17a18365d | |||
| d1216f409d | |||
| 12f594779c | |||
| b3f5e4d4ad | |||
| 5b93799a62 | |||
| fd14f257df | |||
| d044e497f6 | |||
| 6f4835f91a | |||
| 9002db2ae0 | |||
| 19c6bbf6a2 | |||
| ef2ca1da3d | |||
| fbc18c5b85 | |||
| d93fe53e84 | |||
| 0a40970dea | |||
| 6478eee338 | |||
| a60c739797 | |||
| 19ca7d812e | |||
| bc37cf0d1c | |||
| 78b95deb55 | |||
| b787569820 | |||
| 016344bae7 | |||
| 0b51369932 | |||
| 1fb66d534a | |||
| f0b1edaf8c | |||
| 4dbb4274d9 | |||
| 9886e89e42 | |||
| 3e402e0692 | |||
| 0a1721d5b2 | |||
| 4d907d57fa | |||
| 24352e799a | |||
| e17d43b58a | |||
| 580b50ea20 | |||
| 659df6967e | |||
| b9244cdf2e | |||
| a2ba00bdc8 | |||
| 1d47e590e5 | |||
| 280ba9db85 | |||
| 7f98a6cccf | |||
| a5b117fcdf | |||
| f56d2090b6 | |||
| 37e3cf684d | |||
| 7949388da8 | |||
| dd119e04b1 | |||
| f7cfe79905 | |||
| 3dc5e0e181 | |||
| 1ca34c4ecf | |||
| 837ee9f042 | |||
| d448c919e8 | |||
| 7d5b6b96ea | |||
| 7b064535bf | |||
| b42334eb91 | |||
| 94c746c44f | |||
| 7d46a7d5f4 | |||
| 9e2cef38f6 | |||
| e166ebf68b | |||
| 33b1d4c55d | |||
| ae2e0900c1 | |||
| 91ac2c58fa | |||
| 69662d4d7d | |||
| fc6afe2a8b | |||
| 51d2ff4e47 | |||
| d46967d1e2 | |||
| 4d78013646 | |||
| 5cced604e4 | |||
| a39ed2823e | |||
| 4bed399ca4 | |||
| 93c330c4ce | |||
| 76ec785e87 | |||
| e9b9432da5 | |||
| c31e86db02 | |||
| 13557eb5ef | |||
| 02213402e8 | |||
| e1f23a1eb7 | |||
| 485af2c6ba | |||
| 171fd34b3c | |||
| 1017142d9b | |||
| be9f644425 | |||
| 8628a50b7d | |||
| 161db70c15 | |||
| e7b8be31cf | |||
| e56f7affc9 | |||
| 2b1cc77f4b | |||
| 18e9aa4d57 | |||
| cf5aa1bed1 | |||
| c35b201685 | |||
| a1481bc29e | |||
| d34e92ee70 | |||
| bcea5182c6 | |||
| 2fb4810cdb | |||
| 411dd7dff5 | |||
| 96f04cdc38 | |||
| b963c7c9ac | |||
| fc7f7dffa8 | |||
| 47c5b6c9c4 | |||
| 8774c90709 | |||
| 01465932e7 | |||
| 72c0527b7d | |||
| e23af5e99a | |||
| 57f1d3c205 | |||
| 7a159ad934 | |||
| 6fdc83faeb | |||
| d36685acdd | |||
| 051d2a68c0 | |||
| 34c369155c | |||
| 7f7a6abafd | |||
| 5e1573dd84 | |||
| e8006acf80 | |||
| 295d21f37d | |||
| 866427491c | |||
| 9905f48c8e | |||
| 0970c058f7 | |||
| ad057ab873 | |||
| 09341fca12 | |||
| c3e6463676 | |||
| d18cf12f14 | |||
| abc7c6a148 | |||
| b143bbdbaa | |||
| be0fb0053d | |||
| 2712dcd1f5 | |||
| cac58eaab9 | |||
| 9a4e26a518 | |||
| a729be295a | |||
| b6078eded1 | |||
| ea642f6e1d | |||
| 4d71620cb0 | |||
| e4028b23c7 | |||
| 96c7c2768c | |||
| 78d857a374 | |||
| 19148eba44 | |||
| 6a32076271 | |||
| ef617f9ce4 | |||
| 3843cea959 | |||
| dd1c49bde9 | |||
| e99b3af0a5 | |||
| 88c646c86c | |||
| 64cded8246 | |||
| e3be524447 | |||
| a62a7f7cf1 | |||
| 9d456bba9b | |||
| 89ad7593bd | |||
| d227bec0ff | |||
| a3f0fff734 | |||
| efeeece735 | |||
| 234783548f | |||
| 5a29107f3b | |||
| 976f5182e1 | |||
| 30c949085c | |||
| 6762df415c | |||
| 1818b1ea62 | |||
| b67543f81c | |||
| 94ff7dc6fb | |||
| cc229aefae | |||
| 7d91cfb512 | |||
| 8548641dc1 | |||
| c46898592f | |||
| 362ead2760 | |||
| a6ea1e6b55 | |||
| 0f19df8a81 | |||
| ee5c842c7d | |||
| c487b702a2 | |||
| bb6613d0ae | |||
| 821c0fab09 | |||
| 5b1962303d | |||
| 41c4484222 | |||
| 4ebff9a130 | |||
| 6936658019 | |||
| b5b31e398c | |||
| f9f1aafe0c | |||
| d1b667fbce | |||
| 91465d8e6f | |||
| f8ad050dda | |||
| 0d004ccbab | |||
| 2e5eb63850 | |||
| f24ecee603 | |||
| c5635f21a3 | |||
| 605f1942ef | |||
| fec491fb12 | |||
| 794d271152 | |||
| 29362e45bc | |||
| a16beb98de | |||
| 38885e4301 | |||
| 136119f8ac | |||
| e3ec7394ab | |||
| ddd69d19c0 | |||
| 8ecc366582 | |||
| 4db54f8ddc | |||
| 8f9f6caa4e | |||
| 7e41f6ed62 | |||
| 159eb68a11 | |||
| 815231b1e0 | |||
| 0feb0d9244 | |||
| 1db138ed55 | |||
| 4c0d4dd780 | |||
| c626774da2 | |||
| acf43857a3 | |||
| e2f6c51fb0 | |||
| c4a7378466 | |||
| a17e9b6b02 | |||
| f978967e5e | |||
| fc413e2403 | |||
| 38719765bf | |||
| f3596f734d | |||
| 1d15fe069a | |||
| 72a5579d83 | |||
| cd0b47d068 | |||
| 4c93ab8c68 | |||
| c0ebe31560 | |||
| cc1ff93250 | |||
| 42ac2d2dde | |||
| d764111886 | |||
| 8cd6b8aa99 | |||
| da8a4fafcc | |||
| 9f9de389d5 | |||
| 7568b0e215 | |||
| a75663501d | |||
| 96ae288c4b | |||
| a3a826572f | |||
| fe7ad8ee05 | |||
| 3614a093e3 | |||
| 6325bcf5b2 | |||
| 307c2ffe3c | |||
| 06913ab74f | |||
| 506630200b | |||
| df194d567f | |||
| 9727603250 | |||
| a0c8428f8c | |||
| dd91812b11 | |||
| 10619f06b4 | |||
| 0a1e472fc2 | |||
| 4e92c71259 | |||
| 2236780190 | |||
| 3faffdce2d | |||
| d6242e9cac | |||
| 691204ceed | |||
| bd4d9c6fe2 | |||
| 3440f5cfbe | |||
| a518049fa2 | |||
| 35e309cf87 | |||
| e0fdddc73f | |||
| 0c07f7adcc | |||
| a48ed9a246 | |||
| d4a14af14d | |||
| f7e3ed13f9 | |||
| 71c4962ff6 | |||
| b713a7796e | |||
| 65e812d3a9 | |||
| 5c3085fe51 | |||
| 6af26e2306 | |||
| a914565f51 | |||
| 24893bf740 | |||
| 26cbea9e12 | |||
| f7fcd7447a | |||
| 16bd63fc26 | |||
| e158cda057 | |||
| 7121e2c770 | |||
| f122b3bbdf | |||
| 6717edcb87 | |||
| dee2e8e67d | |||
| 4544dabd56 | |||
| 222781abca | |||
| 55a098cae8 | |||
| 837c17c396 | |||
| f9bc74626d | |||
| 17c91152e0 | |||
| d414ef0d0f | |||
| e66aa25fce | |||
| 75d82e8666 | |||
| af42d2a54a | |||
| f5cd4f17f8 | |||
| 02c7770b57 | |||
| 0f049856a4 | |||
| bd14171b88 | |||
| e6ba930e65 | |||
| 61a6b9511a | |||
| 87efc67f48 | |||
| 9e2da6ec48 | |||
| 3f9f675c43 | |||
| 698399e61f | |||
| ec676fa15e | |||
| 122e3a9430 | |||
| 79a7f8a460 | |||
| bb85a84561 | |||
| be6fc35326 | |||
| 79de2a5de2 | |||
| ca1f1362cc | |||
| 0ca0d552eb | |||
| 8baead6107 | |||
| 4f5a29d6d1 | |||
| da7562367c | |||
| 6001c94f30 | |||
| 104a5998cb | |||
| 6cbd3ab096 | |||
| 7f9fa5730b | |||
| bdccc51437 | |||
| 0e039a1868 | |||
| 10ab037833 | |||
| 540a651fdf | |||
| ee893325c4 | |||
| 8120e57850 | |||
| 043e000459 | |||
| 66fb8f031b | |||
| 9e2bef146e | |||
| 4c642e9d3c | |||
| 30b19190dc | |||
| 840bc505f6 | |||
| 8c843ceefd | |||
| aa5a595762 | |||
| 9dfb940d80 | |||
| 1dbfeb7ecd | |||
| f4054b6954 | |||
| faaef83954 | |||
| 287543a0e6 | |||
| 7545755b00 | |||
| 26dc212f4c | |||
| a5128da67a | |||
| 37eedf5cdc | |||
| f2e680430a | |||
| 740a6a7ad5 | |||
| 1236e492a9 | |||
| 80db177f5a | |||
| 3d1cac313c | |||
| dc4a5ae1fd | |||
| da7b9a6bbc | |||
| 55de037035 | |||
| c468b114e4 | |||
| d96bd5269a | |||
| ed4148f20e | |||
| f8e2cc8008 | |||
| 5d32af8a6b | |||
| ed10863494 | |||
| 4e1717db4c | |||
| 159b68aab4 | |||
| d76cf6d337 | |||
| 69950e57f0 | |||
| d44ab3dbab | |||
| b199825c3b | |||
| 94becb89f6 | |||
| 1f4231e1f0 | |||
| bdcbd11d65 | |||
| 008160998a | |||
| 1cafb1eea5 | |||
| 392f1d70eb | |||
| d79d2611ca | |||
| e3cea042d6 | |||
| 679668e3c0 | |||
| 730269743f | |||
| bfc61824b9 | |||
| 444f9e40d5 | |||
| c5006321a7 | |||
| b9d3e7721e | |||
| 49a229835a | |||
| 414b47d653 | |||
| b62d087bb6 | |||
| 4704625e3a | |||
| 53c4797606 | |||
| 60b6c0c03d | |||
| 5f3ef9c0da | |||
| bb5a322ce2 | |||
| bb072faeee | |||
| a2be7b4548 | |||
| 7796ff0f69 | |||
| afd6b7ea27 | |||
| b62de4c521 | |||
| 2e8a74ecff | |||
| e94e90b046 | |||
| 7173764d6d | |||
| e4643f048a | |||
| 236c8c4eaf | |||
| 3b910645e7 | |||
| 9669363504 | |||
| b5d79bdccc | |||
| 1d3d705aae | |||
| 2ab466599d | |||
| 851026d3fa | |||
| 32da2ed706 | |||
| d8c50264cc | |||
| 8d81ae88da | |||
| f1f1eb040a | |||
| 36fa6e857b | |||
| b401267aa4 | |||
| e63b83c8c4 | |||
| 0ac8bf58ea | |||
| 7fb1c7e91d | |||
| 60690c78ae | |||
| 4b92808bbf | |||
| 73397a0973 | |||
| 35e25be1a4 | |||
| f7129b219e | |||
| e1153f8797 | |||
| ff28bc8b0a | |||
| 0b01489f7d | |||
| 7a69770026 | |||
| ec51e14451 | |||
| 86e9749d6c | |||
| aa89f30f2a | |||
| da794a866e | |||
| 705cd16dee | |||
| 0168a627a4 | |||
| 7b29568eb1 | |||
| a585379bbb | |||
| e240cd5ba2 | |||
| e043bbdd62 | |||
| 094436c23a | |||
| c6b2600c62 | |||
| d1eb2ea9e2 | |||
| 453d3eb567 | |||
| 53b7b131cb | |||
| bec9b9a3f7 | |||
| bf47951f3a | |||
| 604c8abb59 | |||
| ef4a4b0ab8 | |||
| 24bdb433c9 | |||
| 6006de5795 | |||
| a578c43810 | |||
| 74b758034e | |||
| 04571ff393 | |||
| 7adff28aa9 | |||
| 1589129ea1 | |||
| 97dcc79a7f | |||
| bc2feece4b | |||
| 1f17895b12 | |||
| 665f24d85f | |||
| 2df30d186e | |||
| 6451e10d3e | |||
| d222a9e9f2 | |||
| 00997db5ae | |||
| 2d5320c454 | |||
| 2fa6e278d2 | |||
| ef381fbb54 | |||
| a74b20f278 | |||
| f536bc94b2 | |||
| 95140f948f | |||
| afc540f6b7 | |||
| fcf2622c26 | |||
| d9ebc5398a | |||
| eea68c34ad | |||
| 8a2d0890a2 | |||
| 1a82943db2 | |||
| 3c36edec1f | |||
| 0f913e89db | |||
| 2b7ec1b023 | |||
| 33fa29fda0 | |||
| d3c229375c | |||
| c82d7c2dd2 | |||
| 553d76dab3 | |||
| d4f0ac2303 | |||
| 4588812d24 | |||
| 9467dbdd40 | |||
| 68c416e414 | |||
| 71c4fdbc85 | |||
| 7dbe42286d | |||
| b5579ca910 | |||
| cd2b255ae3 | |||
| 93f29a7598 | |||
| 32ef35b952 | |||
| abf22909f1 | |||
| a86e16ddde | |||
| 263fa064cd | |||
| 915172e9ef | |||
| 4d066b7e30 | |||
| a7f0705bcf | |||
| 16f18bfeff | |||
| 7a42e60bcb | |||
| aecdecbdf8 | |||
| a790db134d | |||
| 75b77d508d | |||
| 4240817a3a | |||
| 24307e85b7 | |||
| b030d0cf79 | |||
| b0d3a8e1b5 | |||
| 62456c1bed | |||
| 3f1f6720ee | |||
| ab0cbf3e12 | |||
| c12257a1a7 | |||
| e039577d66 | |||
| 9995466a18 | |||
| f424f450f2 | |||
| 677f67db48 | |||
| 47d1f5eecf | |||
| d8391d6fbd | |||
| 640cd059ce | |||
| a78cea7d8a | |||
| 7044cbbd67 | |||
| 292c15cd48 | |||
| 06a7f1d3da | |||
| efbf01b49d | |||
| 4b349805db | |||
| 47096e112a | |||
| 68add78230 | |||
| ebae65b6af | |||
| 460c0c8a42 | |||
| 528d1b03f1 | |||
| 9d33d9d6b0 | |||
| d9729b4a2e | |||
| 1db6c244bb | |||
| 13c5d25a2e | |||
| c166261513 | |||
| 84998a4d19 | |||
| 6b27d4ce11 | |||
| f11e136068 | |||
| 707ea554ac | |||
| 65f7190030 | |||
| a5a5c06716 | |||
| 9c832893af | |||
| 4e15901df1 | |||
| 9a32d08e9f | |||
| c811d416a7 | |||
| 92391bfdf9 | |||
| 6c1f2af53a | |||
| 7875f98b71 | |||
| 076fc4d72c | |||
| d7db1b9576 | |||
| 2a166f088d | |||
| 2175c68319 | |||
| 3418770fe1 | |||
| d7051e986f | |||
| f36d9bfa2a | |||
| e79a88856a | |||
| db2368cd0b | |||
| 0f9d26829c | |||
| 29404e34d9 | |||
| 14b64fef43 | |||
| e0f10c2b03 | |||
| 01aca02edc | |||
| 5cdfa0aaaf | |||
| 90921a9deb | |||
| d6a7dfc1a5 | |||
| 00093a2052 | |||
| 3a795de828 | |||
| cde60ed6b2 | |||
| b717e6f2d8 | |||
| 3aff1677cc | |||
| 9e97d79c81 | |||
| 47415937dd | |||
| aed649475e | |||
| 41e1f1ffa5 | |||
| 20b6e971c0 | |||
| c42e60a3d2 | |||
| 995a2ea618 | |||
| 13db60d382 | |||
| c9233d7446 | |||
| 6080c4fab1 | |||
| 820b2af43a | |||
| 1cb0053720 | |||
| 822a615c6c | |||
| 593557659c | |||
| c6e2b9ccc0 | |||
| b4780a41d3 | |||
| 4790dacbf7 | |||
| 4852f0580b | |||
| 9d7639a9d3 | |||
| 6c52368124 | |||
| c78eb50eb8 | |||
| 9ce0e8e17c | |||
| 32825e8a79 | |||
| cb8691a381 | |||
| 5ddcd60332 | |||
| 68cd4bdeab | |||
| ccd3e55b32 | |||
| 56ec7b9887 | |||
| 2d6ff40649 | |||
| 9bdd9bdecc | |||
| dd946f8ab5 | |||
| 593aec9ab1 | |||
| 6b173b5170 | |||
| 130301e32e | |||
| 2013838bfd | |||
| 879558b9ee | |||
| ee059c0910 | |||
| 6c6e0e3f73 | |||
| b1c8b48e6e | |||
| 6f05794bb8 | |||
| 346135fed3 | |||
| 69939108e1 | |||
| 674f454e70 | |||
| a881838836 | |||
| e4b50aa814 | |||
| fd8490c689 | |||
| d0a51048d7 | |||
| 506f131428 | |||
| e42c6bf0bb | |||
| 535f956682 | |||
| 4e94b85ec2 | |||
| 6b3b04ffb7 | |||
| 36bc3a453f | |||
| 99c069df15 | |||
| 04fd7ce9e1 | |||
| cc958947e5 | |||
| f44cd5d740 | |||
| bba2d63de8 | |||
| 3420bd6e06 | |||
| b10d846019 | |||
| 11ddb5c6ca | |||
| b37fed4cc8 | |||
| 5397eef234 | |||
| d311345aa5 | |||
| d6df615588 | |||
| ce6e30c09e | |||
| ee754b4a47 | |||
| 5f72b7438a | |||
| ea9607302a | |||
| 26bb17337e | |||
| 5e8491cf7f | |||
| e42c6ab520 | |||
| 9c039474b3 | |||
| b378316103 | |||
| a94c7dd788 | |||
| 823a7eac03 | |||
| cf2808ae45 | |||
| c382c885e4 | |||
| 7ae9e3a262 | |||
| 74d162f377 | |||
| ad7b453f03 | |||
| b2afc30d12 | |||
| 1076daa8c9 | |||
| 8394d72f48 | |||
| 018fd21741 | |||
| a1312465b5 | |||
| e2273ea676 | |||
| c6eaf0db36 | |||
| a0eca49795 | |||
| e0173ec4c7 | |||
| 99fa4581aa | |||
| 37b1a81fc7 | |||
| 4272536518 | |||
| 0d5a8a7383 | |||
| df6efe5d88 | |||
| d9dc9326f2 | |||
| b5fff09b54 | |||
| a96c4d707b | |||
| 0f9df18dfb | |||
| 8ea98f8cce | |||
| f2f7e6825f | |||
| 2f5e2f39cb | |||
| 4c11854927 | |||
| 6ce83aad2b | |||
| 2743f97892 | |||
| 978aef2ae7 | |||
| 48a12c605a | |||
| 95b4e61a07 | |||
| 2501691ea4 | |||
| a5a90fe6fc | |||
| 0fccd3707d | |||
| 253c069b26 | |||
| 2c7de8f328 | |||
| 64d203491c | |||
| b2ee6638e4 | |||
| 557410ffd7 | |||
| 40105094e7 | |||
| 0dba8d406b | |||
| 2ce5102473 | |||
| e3d64169ed | |||
| a5b565e193 | |||
| 7443fd0973 | |||
| ba613a1567 | |||
| 0bfdb50ade | |||
| a5f20829cb | |||
| abe3e5f597 | |||
| e12428efb9 | |||
| 0650dd7171 | |||
| 7c844909b9 | |||
| 898896f9e0 | |||
| 63ccc626f9 | |||
| 434ec7b6ea | |||
| b2549c317c | |||
| 20c01883c3 | |||
| 340a53fb80 | |||
| 25847a6192 | |||
| 69eb5cdd8e | |||
| 70d6caf95b | |||
| 47717fee88 | |||
| a9064a7871 | |||
| 21c26f48d0 | |||
| 857b4f90d9 | |||
| 97e702b963 | |||
| accb3e616d | |||
| 33786408f0 | |||
| 9a78857b31 | |||
| 1e730a74a0 | |||
| 46f7930787 | |||
| 68793ffe13 | |||
| 4637f14b7f | |||
| a3b853dd47 | |||
| d3aedbeb9a | |||
| da6a097dcc | |||
| 0ed5b364c6 | |||
| 2dbd14b6dc | |||
| 085f6e9560 | |||
| 20118bdfd2 | |||
| 088f41b334 | |||
| e4fdf171c7 | |||
| 6029973bdc | |||
| 995edf0566 | |||
| 5f32f9b1c8 | |||
| 264e5b7911 | |||
| a28d5585f5 | |||
| 082ae70d1d | |||
| 2aa958e058 | |||
| 290cf82936 | |||
| a872ff2d77 | |||
| 4ed9387801 | |||
| 225d5977ff | |||
| 4a4b80450a | |||
| 747d59b895 | |||
| ca95b561dc | |||
| 9df9ad975d | |||
| 9cd1587cf7 | |||
| 782ba32457 | |||
| d11819721d | |||
| 7ee3653342 | |||
| 9e3852f21c | |||
| 447d0ce0e2 | |||
| 49bb3f1387 | |||
| 53a89c953a | |||
| 75d713cdea | |||
| 32c104c660 | |||
| 0d2ed0784f | |||
| 479c611420 | |||
| d0556d6236 | |||
| 37e3fe5f1f | |||
| 9dfbbbcda4 | |||
| b1e1caba29 | |||
| b51e8bc191 | |||
| 27722463a7 | |||
| 3bc4e84ed3 | |||
| 60beddf6c8 | |||
| d00bb87f17 | |||
| 0f332bd9fb | |||
| e04e06d6e2 | |||
| 17fa5a9334 | |||
| 9b74901b40 | |||
| 78e6d7db95 | |||
| 2cf06bc3ee | |||
| 264820e3e8 | |||
| 979041a072 | |||
| ff344535ba | |||
| 1df35eb687 | |||
| cba96c9b35 | |||
| d7f0133f5f | |||
| e5d064d513 | |||
| c33a49fc5e | |||
| a837bb6681 | |||
| dbef6c73bc | |||
| fa2403c1d3 | |||
| c1916c0fb5 | |||
| dba4dcb4a5 | |||
| 9d26a9268b | |||
| 1b17072a89 | |||
| 7d46108c12 | |||
| cd53ec9bcc | |||
| 1ac32a5256 | |||
| 9e12c45d82 | |||
| 24d9d23743 | |||
| ce74333348 | |||
| 46f5325c15 | |||
| aa89b95075 | |||
| 27fc1672d4 | |||
| e6c5482b7c | |||
| 95dce5cdfc | |||
| 51139a5f56 | |||
| dd3ff0fcb5 | |||
| d088194585 | |||
| 5f187738e6 | |||
| c10d2e0d45 | |||
| 1a8f753303 | |||
| 23f7f5ebba | |||
| bdd145b0de | |||
| 0cbaed2443 | |||
| 99c0cbdf29 | |||
| 96985fb3fd | |||
| 981ca72ee6 | |||
| 6a32de4b47 | |||
| f5d0ed5b1c | |||
| 55801b48ec | |||
| 3ec870cb56 | |||
| cd0421ceb8 | |||
| 9a27beb79c | |||
| e6532b6d85 | |||
| 7d96cfa424 | |||
| c7af6725ca | |||
| feec7c5b40 | |||
| 07964a6112 | |||
| a93db40138 | |||
| b7c8afab2f | |||
| 6ca475def8 | |||
| d8e7adcdb4 | |||
| 113b175db7 | |||
| 40bf7c5285 | |||
| abeb337f45 | |||
| d0a0216602 | |||
| 2a0cfb608d | |||
| d33256f1dc | |||
| db2cd9e941 | |||
| 3e6f5de92f | |||
| 9f793dad28 | |||
| f2f5d4984d | |||
| 29fec4742e | |||
| 0a9a19305c | |||
| 4e9c432c14 | |||
| 6bf36d922c | |||
| 076d4e0ec5 | |||
| b87e6ccb76 | |||
| 22707edcbf | |||
| 7578298b3f | |||
| d2892fc799 | |||
| 21b2e5a059 | |||
| 878ae7ea89 | |||
| c657948824 | |||
| a39e71ca26 | |||
| 8f4e7f7fdc | |||
| a674450198 | |||
| 843f6e83a9 | |||
| 058ff94828 | |||
| 9378f38371 | |||
| 2dc39feabd | |||
| 09aad777f4 | |||
| da72a5fbcd | |||
| 1146a9b90b | |||
| 2fbfafc408 | |||
| b5dc1dde8b | |||
| 63b39c78ee | |||
| 13d9bcc0c7 | |||
| ba0d63d722 | |||
| 9672850d11 | |||
| 00e43197fd | |||
| 284ab11c7f | |||
| e62b222372 | |||
| a0e93009f0 | |||
| 5d4726446d | |||
| 010ac23e8a | |||
| cdfc67db01 | |||
| 6d869ef55b | |||
| 2fa6129c3a | |||
| bb6a921d1e | |||
| 9aaf81328f | |||
| 35225fe2d3 | |||
| 01266ece6b | |||
| 1b7415a81b | |||
| abdadf1ee1 | |||
| d7ae9fb4a2 | |||
| fb78592425 | |||
| af56c5033c | |||
| 3858e31942 | |||
| 37f0a37ed2 | |||
| 411f3256cc | |||
| 811c6a986f | |||
| 974acbf38c | |||
| 634b8b707f | |||
| 0e43271cc9 | |||
| 5ae1790e52 | |||
| 16997d85eb | |||
| 62d7d61381 | |||
| ae2a2d5b00 | |||
| bcdf04d00e | |||
| ba88be0fe9 | |||
| 8471c2d9d8 | |||
| dcc67863dc | |||
| ac7f50b4cd | |||
| 612d77eaab | |||
| 04996b2850 | |||
| 261beb046e | |||
| b8c43e55db | |||
| 13cf980879 | |||
| e6063fb26b | |||
| 1e4baa53f0 | |||
| 80ef5d761c | |||
| affd470820 | |||
| 89783ac0c2 | |||
| fe62afd3d9 | |||
| dca59d0eda | |||
| ec5f94adc8 | |||
| a38a2a0e4f | |||
| fe1978c6f5 | |||
| 509db0b08f | |||
| eae024027f | |||
| decfda2705 | |||
| 318781512b | |||
| 286d558c54 | |||
| 822c231f1c | |||
| 24fc2ae59e | |||
| 7b3d005662 | |||
| 04162aaa79 | |||
| db1adcac97 | |||
| 1e78262fc5 | |||
| 4497a16fb0 |
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
|
||||
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
.git* text eol=auto core.whitespace whitespace=trailing-space
|
||||
+17
-16
@@ -1,18 +1,19 @@
|
||||
_gitignore/
|
||||
*.log
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
# artifacts from pprof tooling
|
||||
*.prof
|
||||
*.test
|
||||
|
||||
# build artifacts
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
|
||||
# mac specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
_gitignore/
|
||||
Vagrantfile
|
||||
.vagrant/
|
||||
/.idea
|
||||
|
||||
# go modules
|
||||
vendor
|
||||
dist/builds/
|
||||
dist/release/
|
||||
|
||||
error.log
|
||||
access.log
|
||||
|
||||
/*.conf
|
||||
Caddyfile
|
||||
|
||||
og_static/
|
||||
|
||||
.vscode/
|
||||
@@ -1,49 +0,0 @@
|
||||
linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule
|
||||
ignoretests: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- ineffassign
|
||||
- misspell
|
||||
|
||||
run:
|
||||
# default concurrency is a available CPU number.
|
||||
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
||||
deadline: 5m
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
format: 'colored-line-number'
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# we aren't calling unknown URL
|
||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
||||
linters:
|
||||
- gosec
|
||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||
- text: "G203" # G203: Use of unescaped data in HTML templates
|
||||
linters:
|
||||
- gosec
|
||||
# we're shelling out to known commands, not relying on user-defined input.
|
||||
- text: "G204" # G204: Audit use of command execution
|
||||
linters:
|
||||
- gosec
|
||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
|
||||
before_install:
|
||||
# Decrypts a script that installs an authenticated cookie
|
||||
# for git to use when cloning from googlesource.com.
|
||||
# Bypasses "bandwidth limit exceeded" errors.
|
||||
# See github.com/golang/go/issues/12933
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_3df18f9af81d_key -iv $encrypted_3df18f9af81d_iv -in dist/gitcookie.sh.enc -out dist/gitcookie.sh -d; fi
|
||||
|
||||
install:
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi
|
||||
- go get -t ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get github.com/gordonklaus/ineffassign
|
||||
- go get github.com/client9/misspell/cmd/misspell
|
||||
|
||||
script:
|
||||
- diff <(echo -n) <(gofmt -s -d .)
|
||||
- ineffassign .
|
||||
- misspell -error .
|
||||
- go vet ./...
|
||||
- go test -race ./...
|
||||
|
||||
after_script:
|
||||
- golint ./...
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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>
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
## Contributing to Caddy
|
||||
|
||||
Welcome! Our community focuses on helping others and making Caddy the best it
|
||||
can be. We gladly accept contributions and encourage you to get involved!
|
||||
|
||||
|
||||
### Join us in the forum
|
||||
|
||||
The [Caddy forum](https://forum.caddyserver.com) is the place for all discussion
|
||||
that doesn't belong in issues or pull requests. Feel free to participate with us!
|
||||
|
||||
If you want to file a bug report or make an improvement to Caddy, however, you
|
||||
should submit an issue or pull request.
|
||||
|
||||
|
||||
### Bug reports
|
||||
|
||||
Please [search this repository](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
|
||||
with a variety of keywords to ensure your bug is not already reported.
|
||||
|
||||
If unique, [open an issue](https://github.com/mholt/caddy/issues) and answer the
|
||||
questions so we can understand and reproduce the problematic behavior.
|
||||
|
||||
The burden is on you to convince us that it is actually a bug in Caddy. This is
|
||||
easiest to do when you write clear, concise instructions so we can reproduce
|
||||
the behavior (even if it seems obvious). The more detailed and specific you are,
|
||||
the faster we will be able to help you. Check out
|
||||
[How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||
|
||||
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're
|
||||
getting free help. If we helped you, please consider
|
||||
[donating](https://caddyserver.com/donate) - it keeps us motivated!
|
||||
|
||||
|
||||
### Minor improvements and new tests
|
||||
|
||||
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time for
|
||||
minor changes or new tests. Make sure to write tests to assert your change is
|
||||
working properly and is thoroughly covered. We'll ask most pull requests to be
|
||||
[squashed](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html),
|
||||
especially with small commits.
|
||||
|
||||
Your pull request may be thoroughly reviewed. This is because if we accept the
|
||||
PR, we also assume responsibility for it, although we would prefer you to
|
||||
help maintain your code after it gets merged.
|
||||
|
||||
|
||||
### Proposals, suggestions, ideas, new features
|
||||
|
||||
First, please [search](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
|
||||
with a variety of keywords to ensure your suggestion/proposal is new.
|
||||
|
||||
If so, you may open either an issue or a pull request for discussion and
|
||||
feedback.
|
||||
|
||||
The advantage of issues is that you don't have to spend time implementing your
|
||||
idea, but you should still describe it thoroughly as if someone reading it would
|
||||
implement the whole thing starting from scratch.
|
||||
|
||||
The advantage of pull requests is that we can immediately see the impact the
|
||||
change will have on the project, what the code will look like, and how to
|
||||
improve it. The disadvantage of pull requests is that they are unlikely to get
|
||||
accepted without significant changes first, or it may be rejected entirely.
|
||||
Don't worry, that won't happen without an open discussion first.
|
||||
|
||||
If you are going to spend significant time writing code for a new pull request,
|
||||
best to open an issue to "claim" it and get feedback before you invest a lot of
|
||||
time. Not all pull requests are merged, and that's okay,
|
||||
[Read why.](https://github.com/turbolinks/turbolinks/pull/124#issuecomment-239826060)
|
||||
|
||||
Remember: pull requests should always be thoroughly documented both via godoc
|
||||
and with at least a rough draft of documentation that might go on the website
|
||||
for users to read.
|
||||
|
||||
|
||||
### Collaborator status
|
||||
|
||||
If your pull request is merged, congratulations! You're technically a
|
||||
collaborator. We may also grant you "Collaborator status" which means you can
|
||||
push to the repository and merge other pull requests. We hope that you will
|
||||
stay involved by reviewing pull requests, submitting more of your own, and
|
||||
resolving issues as you are able to. Thanks for making Caddy amazing!
|
||||
|
||||
We ask that collaborators will conduct thorough code reviews and be nice to
|
||||
new contributors. Before merging a PR, it's best to get the approval of
|
||||
at least one or two other collaborators and/or the project owner. We prefer
|
||||
squashed commits instead of many little, semantically-unimportant commits. Also,
|
||||
CI and other post-commit hooks must pass before being merged except in certain
|
||||
unusual circumstances.
|
||||
|
||||
Collaborator status may be removed for inactive users from time to time as
|
||||
we see fit; this is not an insult, just a basic security precaution in case
|
||||
the account becomes inactive or abandoned. Privileges can always be restored
|
||||
later.
|
||||
|
||||
**Reviewing pull requests:** Please help submit and review pull requests as
|
||||
you are able! We would ask that every pull request be reviewed by at least
|
||||
one collaborator who did not open the pull request before merging. This will
|
||||
help ensure high code quality as new collaborators are added to the project.
|
||||
|
||||
Read [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
on the Go wiki for an idea of what we look for in good, clean Go code, and
|
||||
check out [what Linus suggests](https://gist.github.com/matthewhudson/1475276)
|
||||
for good commit messages.
|
||||
|
||||
|
||||
|
||||
### Vulnerabilities
|
||||
|
||||
If you've found a vulnerability that is serious, please email me: Matthew dot
|
||||
Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
|
||||
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! Caddy would not be what it is today without your
|
||||
contributions.
|
||||
@@ -0,0 +1,30 @@
|
||||
(Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please take a few minutes to carefully answer the following questions. If your issue is not a bug report, you do not need to use this template. Thanks!)
|
||||
|
||||
### 1. What version of Caddy are you running (`caddy -version`)?
|
||||
|
||||
|
||||
### 2. What are you trying to do?
|
||||
|
||||
|
||||
### 3. What is your entire Caddyfile?
|
||||
```text
|
||||
(Put Caddyfile here)
|
||||
```
|
||||
|
||||
### 4. How did you run Caddy (give the full command and describe the execution environment)?
|
||||
|
||||
|
||||
### 5. Please paste any relevant HTTP request(s) here.
|
||||
|
||||
(paste curl command, or full HTTP request including headers and body, here)
|
||||
|
||||
|
||||
### 6. What did you expect to see?
|
||||
|
||||
|
||||
### 7. What did you see instead (give full error messages and/or log)?
|
||||
|
||||
|
||||
### 8. How can someone who is starting from scratch reproduce the bug as minimally as possible?
|
||||
|
||||
(Please strip away any extra infrastructure such as containers, reverse proxies, upstream apps, dependencies, etc, to prove this is a bug in Caddy and not an external misconfiguration. Your chances of getting this bug fixed go way up the easier it is to replicate. Thank you!)
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -179,7 +178,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
|
||||
@@ -187,7 +186,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,384 +1,168 @@
|
||||
Caddy 2 Development Branch
|
||||
===========================
|
||||
<a href="https://caddyserver.com"><img src="https://caddyserver.com/resources/images/caddy-lower.png" alt="Caddy" width="350"></a>
|
||||
|
||||
[](https://dev.azure.com/mholt-dev/Caddy/_build/latest?definitionId=5&branchName=v2)
|
||||
[](https://app.fuzzit.dev/orgs/caddyserver-gh/dashboard)
|
||||
|
||||
This is the development branch for Caddy 2. This code (version 2) is not yet feature-complete or production-ready, but is already being used in production, and we encourage you to deploy it today on sites that are not very visible or important so that it can obtain crucial experience in the field.
|
||||
|
||||
Please file issues to propose new features and report bugs, and after the bug or feature has been discussed, submit a pull request! We need your help to build this web server into what you want it to be. (Caddy 2 issues and pull requests receive priority over Caddy 1 issues and pull requests.)
|
||||
|
||||
**Caddy 2 is the web server of the Go community.** We are looking for maintainers to represent the community! Please become involved (issues, PRs, [our forum](https://caddy.community) etc.) and express interest if you are committed to being a collaborator on the Caddy project.
|
||||
[](https://forum.caddyserver.com) [](https://twitter.com/caddyserver) [](https://godoc.org/github.com/mholt/caddy) [](https://travis-ci.org/mholt/caddy) [](https://ci.appveyor.com/project/mholt/caddy)
|
||||
[](https://goreportcard.com/report/mholt/caddy)
|
||||
[](https://sourcegraph.com/github.com/mholt/caddy?badge)
|
||||
|
||||
|
||||
### Menu
|
||||
Caddy is a general-purpose web server for Windows, Mac, Linux, BSD, and
|
||||
[Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is
|
||||
a capable but easier alternative to other popular web servers.
|
||||
|
||||
- [Build from source](#build-from-source)
|
||||
[Releases](https://github.com/mholt/caddy/releases) ·
|
||||
[User Guide](https://caddyserver.com/docs) ·
|
||||
[Community](https://forum.caddyserver.com)
|
||||
|
||||
**Attend the [Caddy launch event](https://www.facebook.com/events/1413078512092363/) on April 20! It's free, and we will try to get a live stream going.**
|
||||
|
||||
<a href="https://www.facebook.com/events/1413078512092363/"><img src="http://i.imgur.com/CjGICWU.png" alt="Caddy launch event" width="500"></a>
|
||||
|
||||
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy)!
|
||||
|
||||
## Menu
|
||||
|
||||
- [Features](#features)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Full Documentation](#full-documentation)
|
||||
- [List of Improvements](#list-of-improvements)
|
||||
- [FAQ](#faq)
|
||||
- [Running from Source](#running-from-source)
|
||||
- [Running in Production](#running-in-production)
|
||||
- [Contributing](#contributing)
|
||||
- [About the Project](#about-the-project)
|
||||
|
||||
|
||||
## Build from source
|
||||
|
||||
Requirements:
|
||||
## Features
|
||||
|
||||
- [Go 1.13 or newer](https://golang.org/dl/)
|
||||
- Do NOT disable [Go modules](https://github.com/golang/go/wiki/Modules) (`export GO111MODULE=auto`)
|
||||
- **Easy configuration** with Caddyfile
|
||||
- **Automatic HTTPS** via [Let's Encrypt](https://letsencrypt.org); Caddy
|
||||
obtains and manages all cryptographic assets for you
|
||||
- **HTTP/2** enabled by default (powered by Go standard library)
|
||||
- **Virtual hosting** for hundreds of sites per server instance, including TLS
|
||||
SNI
|
||||
- Experimental **QUIC support** for those that like speed
|
||||
- TLS session ticket **key rotation** for more secure connections
|
||||
- **Brilliant extensibility** so Caddy can be customized for your needs
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
|
||||
Download the `v2` source code:
|
||||
|
||||
```bash
|
||||
$ git clone -b v2 "https://github.com/caddyserver/caddy.git"
|
||||
```
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
$ cd caddy/cmd/caddy/
|
||||
$ go build
|
||||
```
|
||||
|
||||
That will put a `caddy(.exe)` binary into the current directory. You can move it into your PATH or use `go install` to do that automatically (assuming `$GOPATH/bin` is already in your PATH). You can also use `go run main.go` for quick, temporary builds while developing.
|
||||
|
||||
The initial build may be slow as dependencies are downloaded. Subsequent builds should be very fast. If you encounter any Go-module-related errors, try clearing your Go module cache (`$GOPATH/pkg/mod`) and Go package cache (`$GOPATH/pkg`) and read [the Go wiki page about modules for help](https://github.com/golang/go/wiki/Modules). If you have issues with Go modules, please consult the Go community for help. But if there is an actual error in Caddy, please report it to us.
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
(Until the stable 2.0 release, there may be breaking changes in v2, please be aware!)
|
||||
Caddy binaries have no dependencies and are available for every platform.
|
||||
Install Caddy any one of these ways:
|
||||
|
||||
These instructions assume an executable build of Caddy 2 is named `caddy` in the current folder. If it's in your PATH, you may omit the path to the binary (`./`).
|
||||
- **[Download page](https://caddyserver.com/download)** allows you to
|
||||
customize your build in the browser
|
||||
- **[Latest release](https://github.com/mholt/caddy/releases/latest)** for
|
||||
pre-built binaries
|
||||
- **curl [getcaddy.com](https://getcaddy.com)** for auto install:
|
||||
`curl https://getcaddy.com | bash`
|
||||
|
||||
Start Caddy:
|
||||
Once `caddy` is in your PATH, you can `cd` to your website's folder and run
|
||||
`caddy` to serve it. By default, Caddy serves the current directory at
|
||||
[localhost:2015](http://localhost:2015).
|
||||
|
||||
```bash
|
||||
$ ./caddy start
|
||||
```
|
||||
|
||||
There are no config files with Caddy 2. Instead, you POST configuration to it:
|
||||
|
||||
```bash
|
||||
$ curl -X POST "http://localhost:2019/load" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"example": {
|
||||
"listen": ["127.0.0.1:2080"],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [{
|
||||
"handler": "file_server",
|
||||
"browse": {}
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Now visit http://localhost:2080 in your browser and you will see the contents of the current directory displayed.
|
||||
|
||||
To change Caddy's configuration, simply POST a new payload to that endpoint. Config changes are extremely lightweight and efficient, and should be graceful on all platforms -- _even Windows_.
|
||||
|
||||
Updating configuration using heredoc can be tedious, so you can still use a config file if you prefer. Put your configuration in any file (`caddy.json` for example) and then POST that instead:
|
||||
|
||||
```bash
|
||||
$ curl -X POST "http://localhost:2019/load" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @caddy.json
|
||||
```
|
||||
|
||||
Or you can tell Caddy to load its configuration from a file in the first place (this simply does the work of the above curl command for you):
|
||||
|
||||
```bash
|
||||
$ ./caddy start --config caddy.json
|
||||
```
|
||||
|
||||
To stop Caddy:
|
||||
|
||||
```bash
|
||||
$ ./caddy stop
|
||||
```
|
||||
|
||||
Note that this will stop any process named the same as `os.Args[0]`.
|
||||
|
||||
For other commands, please see [the Caddy 2 documentation](https://caddyserver.com/docs/command-line).
|
||||
|
||||
### Caddyfile
|
||||
|
||||
Caddy 2 can be configured with a Caddyfile, much like in v1, for example:
|
||||
To customize how your site is served, create a file named Caddyfile by your
|
||||
site and paste this into it:
|
||||
|
||||
```plain
|
||||
example.com
|
||||
localhost
|
||||
|
||||
try_files {path}.html {path}
|
||||
encode gzip zstd
|
||||
reverse_proxy /api localhost:9005
|
||||
php_fastcgi /blog unix//path/to/socket
|
||||
file_server
|
||||
gzip
|
||||
browse
|
||||
websocket /echo cat
|
||||
ext .html
|
||||
log /var/log/access.log
|
||||
proxy /api 127.0.0.1:7005
|
||||
header /api Access-Control-Allow-Origin *
|
||||
```
|
||||
|
||||
Instead of being its primary mode of configuration, an internal _config adapter_ adapts the Caddyfile to Caddy's native JSON structure. You can see it in action with the [`adapt` command](https://caddyserver.com/docs/command-line#caddy-adapt):
|
||||
|
||||
```bash
|
||||
$ ./caddy adapt --config path/to/Caddyfile --adapter caddyfile --pretty
|
||||
```
|
||||
When you run `caddy` in that directory, it will automatically find and use
|
||||
that Caddyfile to configure itself.
|
||||
|
||||
If you just want to run Caddy with your Caddyfile directly, the CLI wraps this up for you nicely. Either of the following commands:
|
||||
This simple file enables compression, allows directory browsing (for folders
|
||||
without an index file), hosts a WebSocket echo server at /echo, serves clean
|
||||
URLs, logs requests to 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.
|
||||
|
||||
```bash
|
||||
$ ./caddy start
|
||||
$ ./caddy run
|
||||
```
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
will use your Caddyfile if it is called `Caddyfile` in the current directory.
|
||||
To host multiple sites and do more with the Caddyfile, please see the
|
||||
[Caddyfile documentation](https://caddyserver.com/docs/caddyfile).
|
||||
|
||||
If your Caddyfile is somewhere else, you can still use it:
|
||||
Note that production sites are served over
|
||||
[HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
|
||||
```bash
|
||||
$ ./caddy start|run --config path/to/Caddyfile --adapter caddyfile
|
||||
```
|
||||
Caddy has a command line interface. Run `caddy -h` to view basic help or see
|
||||
the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
|
||||
[Learn more about the Caddyfile in v2.](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#caddyfile-adapter)
|
||||
**Running as root:** We advise against this. You can still listen on ports
|
||||
< 1024 using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Caddy 2 exposes an unprecedented level of control compared to any web server in existence. In Caddy 2, 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 2 is also ridiculously extensible, with a module system that makes vast improvements over Caddy 1's plugin system.
|
||||
## Running from Source
|
||||
|
||||
Nearly all of Caddy 2's configuration is contained in a single config document, rather than being spread across CLI flags and env variables and a configuration file as with other web servers (and Caddy 1).
|
||||
Note: You will need **[Go 1.8](https://golang.org/dl/)** or newer.
|
||||
|
||||
To wield the power of this design, you need to know how the config document is structured. Please see the [the Caddy 2 documentation in our wiki](https://caddyserver.com/docs/json/) for details about Caddy's config structure.
|
||||
1. `go get github.com/mholt/caddy/caddy`
|
||||
2. `cd` into your website's directory
|
||||
3. Run `caddy` (assuming `$GOPATH/bin` is in your `$PATH`)
|
||||
|
||||
Configuration is normally given to Caddy through an API endpoint, which is likewise documented in the wiki pages. However, you can also use config files of various formats with [config adapters](https://caddyserver.com/docs/config-adapters).
|
||||
Caddy's `main()` is in the caddy subfolder. To recompile Caddy, use
|
||||
`build.bash` found in that folder.
|
||||
|
||||
|
||||
## Full Documentation
|
||||
|
||||
Caddy 2 is very much in development, and so is its documentation. You can find it here:
|
||||
## Running in Production
|
||||
|
||||
**https://caddyserver.com/docs/**
|
||||
The Caddy project does not officially maintain any system-specific
|
||||
integrations, but your download file includes
|
||||
[unofficial resources](https://github.com/mholt/caddy/tree/master/dist/init)
|
||||
contributed by the community that you may find helpful for running Caddy in
|
||||
production.
|
||||
|
||||
Note that breaking changes are expected until the stable 2.0 release. The v2 Caddyfile will probably be the last thing documented, as it is rapidly changing.
|
||||
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.
|
||||
|
||||
|
||||
## List of Improvements
|
||||
|
||||
The following is a non-comprehensive list of significant improvements over Caddy 1. Not everything in this list is finished yet, but they will be finished or at least will be possible with Caddy 2:
|
||||
## Contributing
|
||||
|
||||
- Centralized configuration. No more disparate use of environment variables, config files (potentially multiple!), CLI flags, etc.
|
||||
- REST API. Control Caddy with HTTP requests to an administration endpoint. Changes are applied immediately and efficiently.
|
||||
- Dynamic configuration. Any and all specific config values can be modified directly through the admin API with a REST endpoint.
|
||||
- Change only specific configuration settings instead of needing to specify the whole config each time. This makes it safe and easy to change Caddy's config with manually-crafted curl commands, for example.
|
||||
- No configuration files. Except optionally to bootstrap its configuration at startup. You can still use config files if you wish, and we expect that most people will.
|
||||
- Export the current Caddy configuration with an API GET request.
|
||||
- Silky-smooth graceful reloads. Update the configuration up to dozens of times per second with no dropped requests and very little memory cost. Our unique graceful reload technology is lighter and faster **and works on all platforms, including Windows**.
|
||||
- An embedded scripting language! Caddy2 has native Starlark integration. Do things you never thought possible with higher performance than Lua, JavaScript, and other VMs. Starlark is expressive, familiar (dialect of Python), _almost_ Turing-complete, and highly efficient. (We're still improving performance here.)
|
||||
- Using [XDG standards](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables) instead of dumping all assets in `$HOME/.caddy`.
|
||||
- Caddy plugins are now called "Caddy modules" (although the terms "plugin" and "module" may be used interchangeably). Caddy modules are a concept unrelated [Go modules](https://github.com/golang/go/wiki/Modules), except that Caddy modules may be implemented by Go modules. Caddy modules are centrally-registered, properly namespaced, and generically loaded & configured, as opposed to how scattered and unorganized Caddy 1-era plugins are.
|
||||
- Modules are easier to write, since they do not have to both deserialize their own configuration from a configuration DSL and provision themselves like plugins did. Modules are initialized pre-configured and have the ability to validate the configuration and perform provisioning steps if necessary.
|
||||
- Can specify different storage mechanisms in different parts of the configuration, if more than one is needed.
|
||||
- "Top-level" Caddy modules are simply called "apps" because literally any long-lived application can be served by Caddy 2.
|
||||
- Even more of Caddy is made of modules, allowing for unparalleled extensibility, flexibility, and control. Caddy 2 is arguably the most flexible, extensible, programmable web server ever made.
|
||||
- TLS improvements!
|
||||
- TLS configuration is now centralized and decoupled from specific sites
|
||||
- A single certificate cache is used process-wide, reducing duplication and improving memory use
|
||||
- Customize how to manage each certificate ("automation policies") based on the hostname
|
||||
- Automation policy doesn't have to be limited to just ACME - could be any way to manage certificates
|
||||
- Fine-grained control over TLS handshakes
|
||||
- If an ACME challenge fails, other enabled challenges will be tried (no other web server does this)
|
||||
- TLS Session Ticket Ephemeral Keys (STEKs) can be rotated in a cluster for increased performance (no other web server does this either!)
|
||||
- Ability to select a specific certificate per ClientHello given multiple qualifying certificates
|
||||
- Provide TLS certificates without persisting them to disk; keep private keys entirely in memory
|
||||
- Certificate management at startup is now asynchronous and much easier to use through machine reboots and in unsupervised settings
|
||||
- All-new HTTP server core
|
||||
- Listeners can be configured for any network type, address, and port range
|
||||
- Customizable TLS connection policies
|
||||
- HTTP handlers are configured by "routes" which consist of matcher and handler components. Match matches an HTTP request, and handle defines the list of handlers to invoke as a result of the match.
|
||||
- Some matchers are regular expressions, which expose capture groups to placeholders.
|
||||
- New matchers include negation and matching based on remote IP address / CIDR ranges.
|
||||
- Placeholders are vastly improved generally
|
||||
- Placeholders (variables) are more properly namespaced.
|
||||
- Multiple routes may match an HTTP request, creating a "composite route" quickly on the fly.
|
||||
- The actual handler for any given request is its composite route.
|
||||
- User defines the order of middlewares (careful! easy to break things).
|
||||
- Adding middlewares no longer requires changes to Caddy's code base (there is no authoritative list).
|
||||
- Routes may be marked as terminal, meaning no more routes will be matched.
|
||||
- Routes may be grouped so that only the first matching route in a group is applied.
|
||||
- Requests may be "re-handled" if they are modified and need to be sent through the chain again (internal redirect).
|
||||
- Vastly more powerful static file server, with native content-negotiation abilities
|
||||
- Done away with URL-rewriting hacks often needed in Caddy 1
|
||||
- Highly descriptive/traceable errors
|
||||
- Very flexible error handling, with the ability to specify a whole list of routes just for error cases
|
||||
- The proxy has numerous improvements, including dynamic backends and more configurable health checks
|
||||
- FastCGI support integrated with the reverse proxy
|
||||
- More control over automatic HTTPS: disable entirely, disable only HTTP->HTTPS redirects, disable only cert management, and for certain names, etc.
|
||||
- Use Starlark to build custom, dynamic HTTP handlers at request-time
|
||||
- We are finding that -- on average -- Caddy 2's Starlark handlers are ~1.25-2x faster than NGINX+Lua.
|
||||
**[Join our community](https://forum.caddyserver.com) where you can chat with
|
||||
other Caddy users and developers!**
|
||||
|
||||
And a few major features still being worked on:
|
||||
Please see our [contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md)
|
||||
and check out the [developer wiki](https://github.com/mholt/caddy/wiki).
|
||||
|
||||
- Logging
|
||||
- Kubernetes ingress controller (mostly done, just polishing it -- and it's amazing)
|
||||
- More config adapters. Caddy's native JSON config structure is powerful and complex. Config adapters upsample various formats to Caddy's native config. There are already adapters for Caddyfile, JSON 5, and JSON-C. Planned are NGINX config, YAML, and TOML. The community might be interested in building Traefik and Apache config adapters!
|
||||
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://forum.caddyserver.com)!
|
||||
|
||||
If you want to contribute to the documentation, please submit pull requests to [caddyserver/caddyserver.com](https://github.com/caddyserver/caddyserver.com).
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
## FAQ
|
||||
Special thanks to
|
||||
[](https://www.digitalocean.com)
|
||||
for hosting the Caddy project.
|
||||
|
||||
### How do I configure Caddy 2?
|
||||
|
||||
Caddy's primary mode of configuration is a REST API, which accepts a JSON document. The JSON structure is described [interactively in the docs](https://caddyserver.com/docs/json/). The advantages of exposing this low-level structure are 1) it has near-parity with actual memory initialization, 2) it allows us to offer wrappers over this configuration to any degree of convenience that is needed, and 3) it performs very well under rapid config changes.
|
||||
|
||||
Basically, you will [start Caddy](https://caddyserver.com/docs/command-line#caddy-run), then [POST a JSON config to its API endpoint](https://caddyserver.com/docs/api#post-load).
|
||||
## About the Project
|
||||
|
||||
Although this makes Caddy 2 highly programmable, not everyone will want to configure Caddy via JSON with an API. Sometimes we just want to give Caddy a simple, static config file and have it do its thing. That's what **[config adapters](https://caddyserver.com/docs/config-adapters)** are for! You can configure Caddy more ways than one, depending on your needs and preferences. See the next questions that explain this more.
|
||||
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 2 feels harder to use. How is this an improvement over Caddy 1?
|
||||
**The name "Caddy":** 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's ease of use is one of the main reasons it is special. We are not taking that away in Caddy 2, but first we had to be sure to tackle the fundamental design limitations with Caddy 1. Usability can then be layered on top. This approach has several advantages which we discuss in the next question.
|
||||
|
||||
### What about the Caddyfile; are there easier ways to configure Caddy 2?
|
||||
|
||||
Yes! Caddy's native JSON configuration via API is nice when you are automating config changes at scale, but if you just have a simple, static configuration in a file, you can do that too with the [Caddyfile](https://github.com/caddyserver/caddy/wiki/v2:-Documentation#caddyfile-adapter).
|
||||
|
||||
The v2 Caddyfile is very similar to the v1 Caddyfile, but they are not compatible. Several improvements have been made to request matching and directives in v2, giving you more power with less complexity and fewer inconsistencies.
|
||||
|
||||
Caddy's default _config adapter_ is the Caddyfile adapter. This takes a Caddyfile as input and [outputs the JSON config](https://caddyserver.com/docs/command-line#caddy-adapt). You can even run Caddy directly without having to see or think about the underlying JSON config.
|
||||
|
||||
The following _config adapters_ are already being built or plan to be built:
|
||||
|
||||
- Caddyfile
|
||||
- JSON 5
|
||||
- JSON-C
|
||||
- nginx
|
||||
- YAML
|
||||
- TOML
|
||||
- any others that the community would like to contribute
|
||||
|
||||
Config adapters allow you to configure Caddy not just one way but _any_ of these ways. For example, you'll be able to bring your existing NGINX config to Caddy and it will spit out the Caddy config JSON you need (to the best of its ability). How cool is that! You can then easily tweak the resulting config by hand, if necessary.
|
||||
|
||||
All config adapters vary in their theoretical expressiveness; that is, if you need more advanced configuration you'll have to drop down to the JSON config, because the Caddyfile or an nginx config may not be expressive enough.
|
||||
|
||||
However, we expect that most users will be able to use the Caddyfile (or another easy config adapter) exclusively for their sites.
|
||||
|
||||
### Why JSON for configuration? Why not _<any other serialization format>_?
|
||||
|
||||
We know there might be strong opinions on this one. Regardless, for Caddy 2, we've decided to go with JSON. If that proves to be a fatal mistake, then Caddy 3 probably won't use JSON.
|
||||
|
||||
JSON may not be the fastest, the most compact, the easiest to write, serialization format that exists. But those aren't our goals. It has withstood the test of time and checks all our boxes.
|
||||
|
||||
- It is almost entirely ubiquitous. JSON works natively in web browsers and has mature libraries in pretty much every language.
|
||||
- It is human-readable (as opposed to a binary format).
|
||||
- It is easy to tweak by hand. Although composing raw JSON by hand is not awesome, this will not be mainstream once our config adapters are done.
|
||||
- It is generally easy to convert other serializations or config formats into JSON, as opposed to the other way around.
|
||||
- Even though JSON deserialization is not fast per-se, that kind of performance is not really a concern since config reloads are not the server's hottest path like HTTP request handling or TLS handshakes are. Even with JSON, Caddy 2 can handle dozens of config changes per second, which is probably plenty for now.
|
||||
- It maps almost 1:1 to the actual, in-memory values that power your HTTP handlers and other parts of the server (no need to parse a config file with some arbitrary DSL and do a bunch of extra pre-processing).
|
||||
|
||||
Ultimately, we think all these properties are appropriate -- if not ideal -- for a web server configuration.
|
||||
|
||||
If you're still not happy with the choice of JSON, feel free to contribute a config adapter of your own choice!
|
||||
|
||||
Or just use YAML or TOML, which seamlessly translate to JSON.
|
||||
|
||||
### JSON is declarative; what if I need more programmability (i.e. imperative syntax)?
|
||||
|
||||
NGINX also realized the need for imperative logic in declarative configs, so they tried "if" statements, [but it was a bad idea](https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/).
|
||||
|
||||
We have good news. Caddy 2 can give you the power of imperative logic without the perils of mixing declarative and imperative config such as befell NGINX. We do this by allowing embedded imperative syntax awithin the Caddy's declarative config.
|
||||
|
||||
Caddy 2's configuration is declarative because configuration is very much declarative in nature. Configuration is a tricky medium, as it is read and written by both computers and humans. Computers use it, but humans constantly refer to it and update it. Declarative syntaxes are fairly straightforward to make sense of, whereas it is difficult to reason about imperative logic.
|
||||
|
||||
However, sometimes computation is useful, and in some cases, the only way to express what you need. This can be illustrated really well in the simple case of trying to decide whether a particular HTTP middleware should be invoked as part of an HTTP request. A lot of the time, such logic is as simple as: "GET requests for any path starting with /foo/bar", which can be expressed declaratively in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/foo/bar"
|
||||
}
|
||||
```
|
||||
|
||||
But what if you need to match /foo/bar OR /topaz? How do you express that OR clause? Maybe an array:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": ["GET"],
|
||||
"path": ["/foo/bar", "/topaz"]
|
||||
}
|
||||
```
|
||||
|
||||
Now what if you need add a NOT or AND clause? JSON quickly tires out. As you learn about Caddy 2's request matching, you will see how we handled this. Caddy 2's JSON gives you the ability to express moderately-complex logic such as:
|
||||
|
||||
```js
|
||||
// this is not actual Caddy config, just logic pseudocode
|
||||
IF (Host = "example.com")
|
||||
OR (Host = "sub.example.com" AND Path != "/foo/bar")
|
||||
```
|
||||
|
||||
Already, this is more expressive power than most web servers offer with their native config, yet Caddy 2 offers this in JSON.
|
||||
|
||||
But in most web servers, to make logic this complex feasible, you'll generally call out to Lua or some extra DSL. For example, in NGINX you could use a Lua module to express this logic. Traefik 2.0 has [yet another kind of clunky-looking custom DSL](https://blog.containo.us/back-to-traefik-2-0-2f9aa17be305#d22e) just for this.
|
||||
|
||||
Caddy 2 solves this in a novel way with [Starlark expressions](https://godoc.org/go.starlark.net/starlark#Eval). Starlark is a familiar dialect of Python! So, no new DSLs to learn and no VMs to slow things down:
|
||||
|
||||
```python
|
||||
req.host == 'example.com' ||
|
||||
(req.host == 'sub.example.com' && req.path != '/foo/bar')
|
||||
```
|
||||
|
||||
Starlark performs at least as well as NGINX+Lua (more performance tests ongoing, as well as optimizations to make it even faster!) and because it's basically Python, it's familiar and easy to use.
|
||||
|
||||
In summary: Caddy 2 config is declarative, but can be imperative where that is useful.
|
||||
|
||||
### What is Caddy 2 licensed as?
|
||||
|
||||
Caddy 2 is licensed under the Apache 2.0 open source license. There are no official Caddy 2 distributions that are proprietary.
|
||||
|
||||
### Does Caddy 2 have telemetry?
|
||||
|
||||
No. There was not enough academic interest to continue supporting it. If telemetry does get added later, it will not be on by default or will be vastly reduced in its scope.
|
||||
|
||||
## Does Caddy 2 use HTTPS by default?
|
||||
|
||||
Yes. HTTPS is automatic and enabled by default when possible, just like in Caddy 1. Basically, if your HTTP routes specify a `host` matcher with qualifying domain names, those names will be enabled for automatic HTTPS. Automatic HTTPS is disabled for domains which match certificates that are manually loaded by your config.
|
||||
|
||||
## How do I avoid Let's Encrypt rate limits with Caddy 2?
|
||||
|
||||
As you are testing and developing with Caddy 2, you should use test ("staging") certificates from Let's Encrypt to avoid rate limits. By default, Caddy 2 uses Let's Encrypt's production endpoint to get real certificates for your domains, but their [rate limits](https://letsencrypt.org/docs/rate-limits/) forbid testing and development use of this endpoint for good reasons. You can switch to their [staging endpoint](https://letsencrypt.org/docs/staging-environment/) by adding the staging CA to your automation policy in the `tls` app:
|
||||
|
||||
```json
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"management": {
|
||||
"module": "acme",
|
||||
"ca": "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or with the Caddyfile, using a global options block at the top:
|
||||
|
||||
```
|
||||
{
|
||||
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
}
|
||||
```
|
||||
|
||||
## Can we get some access controls on the admin endpoint?
|
||||
|
||||
Yeah, that's coming. For now, you can use a permissioned unix socket for some basic security.
|
||||
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
|
||||
@@ -1,875 +0,0 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
|
||||
|
||||
// AdminConfig configures Caddy's API endpoint, which is used
|
||||
// to manage Caddy while it is running.
|
||||
type AdminConfig struct {
|
||||
// If true, the admin endpoint will be completely disabled.
|
||||
// Note that this makes any runtime changes to the config
|
||||
// impossible, since the interface to do so is through the
|
||||
// admin endpoint.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
|
||||
// The address to which the admin endpoint's listener should
|
||||
// bind itself. Can be any single network address that can be
|
||||
// parsed by Caddy. Default: localhost:2019
|
||||
Listen string `json:"listen,omitempty"`
|
||||
|
||||
// If true, CORS headers will be emitted, and requests to the
|
||||
// API will be rejected if their `Host` and `Origin` headers
|
||||
// do not match the expected value(s). Use `origins` to
|
||||
// customize which origins/hosts are allowed.If `origins` is
|
||||
// not set, the listen address is the only value allowed by
|
||||
// default.
|
||||
EnforceOrigin bool `json:"enforce_origin,omitempty"`
|
||||
|
||||
// The list of allowed origins for API requests. Only used if
|
||||
// `enforce_origin` is true. If not set, the listener address
|
||||
// will be the default value. If set but empty, no origins will
|
||||
// be allowed.
|
||||
Origins []string `json:"origins,omitempty"`
|
||||
|
||||
// Options related to configuration management.
|
||||
Config *ConfigSettings `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigSettings configures the, uh, configuration... and
|
||||
// management thereof.
|
||||
type ConfigSettings struct {
|
||||
// Whether to keep a copy of the active config on disk. Default is true.
|
||||
Persist *bool `json:"persist,omitempty"`
|
||||
}
|
||||
|
||||
// listenAddr extracts a singular listen address from ac.Listen,
|
||||
// returning the network and the address of the listener.
|
||||
func (admin AdminConfig) listenAddr() (string, string, error) {
|
||||
input := admin.Listen
|
||||
if input == "" {
|
||||
input = DefaultAdminListen
|
||||
}
|
||||
listenAddr, err := ParseNetworkAddress(input)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("parsing admin listener address: %v", err)
|
||||
}
|
||||
if listenAddr.PortRangeSize() != 1 {
|
||||
return "", "", fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
|
||||
}
|
||||
return listenAddr.Network, listenAddr.JoinHostPort(0), nil
|
||||
}
|
||||
|
||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||
func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
|
||||
muxWrap := adminHandler{
|
||||
enforceOrigin: admin.EnforceOrigin,
|
||||
allowedOrigins: admin.allowedOrigins(listenAddr),
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
// addRoute just calls muxWrap.mux.Handle after
|
||||
// wrapping the handler with error handling
|
||||
addRoute := func(pattern string, h AdminHandler) {
|
||||
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.ServeHTTP(w, r)
|
||||
muxWrap.handleError(w, r, err)
|
||||
})
|
||||
muxWrap.mux.Handle(pattern, wrapper)
|
||||
}
|
||||
|
||||
// register standard config control endpoints
|
||||
addRoute("/load", AdminHandlerFunc(handleLoad))
|
||||
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
|
||||
addRoute("/id/", AdminHandlerFunc(handleConfigID))
|
||||
addRoute("/stop", AdminHandlerFunc(handleStop))
|
||||
|
||||
// register debugging endpoints
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
muxWrap.mux.Handle("/debug/vars", expvar.Handler())
|
||||
|
||||
// register third-party module endpoints
|
||||
for _, m := range GetModules("admin.api") {
|
||||
router := m.New().(AdminRouter)
|
||||
for _, route := range router.Routes() {
|
||||
addRoute(route.Pattern, route.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
return muxWrap
|
||||
}
|
||||
|
||||
// allowedOrigins returns a list of origins that are allowed.
|
||||
// If admin.Origins is nil (null), the provided listen address
|
||||
// will be used as the default origin. If admin.Origins is
|
||||
// empty, no origins will be allowed, effectively bricking the
|
||||
// endpoint, but whatever.
|
||||
func (admin AdminConfig) allowedOrigins(listen string) []string {
|
||||
uniqueOrigins := make(map[string]struct{})
|
||||
for _, o := range admin.Origins {
|
||||
uniqueOrigins[o] = struct{}{}
|
||||
}
|
||||
if admin.Origins == nil {
|
||||
uniqueOrigins[listen] = struct{}{}
|
||||
}
|
||||
var allowed []string
|
||||
for origin := range uniqueOrigins {
|
||||
allowed = append(allowed, origin)
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
// replaceAdmin replaces the running admin server according
|
||||
// to the relevant configuration in cfg. If no configuration
|
||||
// for the admin endpoint exists in cfg, a default one is
|
||||
// used, so that there is always an admin server (unless it
|
||||
// is explicitly configured to be disabled).
|
||||
func replaceAdmin(cfg *Config) error {
|
||||
// always be sure to close down the old admin endpoint
|
||||
// as gracefully as possible, even if the new one is
|
||||
// disabled -- careful to use reference to the current
|
||||
// (old) admin endpoint since it will be different
|
||||
// when the function returns
|
||||
oldAdminServer := adminServer
|
||||
defer func() {
|
||||
// do the shutdown asynchronously so that any
|
||||
// current API request gets a response; this
|
||||
// goroutine may last a few seconds
|
||||
if oldAdminServer != nil {
|
||||
go func(oldAdminServer *http.Server) {
|
||||
err := stopAdminServer(oldAdminServer)
|
||||
if err != nil {
|
||||
Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
|
||||
}
|
||||
}(oldAdminServer)
|
||||
}
|
||||
}()
|
||||
|
||||
// always get a valid admin config
|
||||
adminConfig := DefaultAdminConfig
|
||||
if cfg != nil && cfg.Admin != nil {
|
||||
adminConfig = cfg.Admin
|
||||
}
|
||||
|
||||
// if new admin endpoint is to be disabled, we're done
|
||||
if adminConfig.Disabled {
|
||||
Log().Named("admin").Warn("admin endpoint disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract a singular listener address
|
||||
netw, addr, err := adminConfig.listenAddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := adminConfig.newAdminHandler(addr)
|
||||
|
||||
ln, err := Listen(netw, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminServer = &http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 1024 * 64,
|
||||
}
|
||||
|
||||
go adminServer.Serve(ln)
|
||||
|
||||
Log().Named("admin").Info(
|
||||
"admin endpoint started",
|
||||
zap.String("address", addr),
|
||||
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
||||
zap.Strings("origins", handler.allowedOrigins),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopAdminServer(srv *http.Server) error {
|
||||
if srv == nil {
|
||||
return fmt.Errorf("no admin server")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := srv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down admin server: %v", err)
|
||||
}
|
||||
Log().Named("admin").Info("stopped previous server")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminRouter is a type which can return routes for the admin API.
|
||||
type AdminRouter interface {
|
||||
Routes() []AdminRoute
|
||||
}
|
||||
|
||||
// AdminRoute represents a route for the admin endpoint.
|
||||
type AdminRoute struct {
|
||||
Pattern string
|
||||
Handler AdminHandler
|
||||
}
|
||||
|
||||
type adminHandler struct {
|
||||
enforceOrigin bool
|
||||
allowedOrigins []string
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// ServeHTTP is the external entry point for API requests.
|
||||
// It will only be called once per request.
|
||||
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Log().Named("admin.api").Info("received request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("uri", r.RequestURI),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Reflect("headers", r.Header),
|
||||
)
|
||||
h.serveHTTP(w, r)
|
||||
}
|
||||
|
||||
// serveHTTP is the internal entry point for API requests. It may
|
||||
// be called more than once per request, for example if a request
|
||||
// is rewritten (i.e. internal redirect).
|
||||
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.enforceOrigin {
|
||||
// DNS rebinding mitigation
|
||||
err := h.checkHost(r)
|
||||
if err != nil {
|
||||
h.handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// cross-site mitigation
|
||||
origin, err := h.checkOrigin(r)
|
||||
if err != nil {
|
||||
h.handleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// TODO: authentication & authorization, if configured
|
||||
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == ErrInternalRedir {
|
||||
h.serveHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
apiErr, ok := err.(APIError)
|
||||
if !ok {
|
||||
apiErr = APIError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if apiErr.Code == 0 {
|
||||
apiErr.Code = http.StatusInternalServerError
|
||||
}
|
||||
if apiErr.Message == "" && apiErr.Err != nil {
|
||||
apiErr.Message = apiErr.Err.Error()
|
||||
}
|
||||
|
||||
Log().Named("admin.api").Error("request error",
|
||||
zap.Error(err),
|
||||
zap.Int("status_code", apiErr.Code),
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(apiErr.Code)
|
||||
json.NewEncoder(w).Encode(apiErr)
|
||||
}
|
||||
|
||||
// checkHost returns a handler that wraps next such that
|
||||
// it will only be called if the request's Host header matches
|
||||
// a trustworthy/expected value. This helps to mitigate DNS
|
||||
// rebinding attacks.
|
||||
func (h adminHandler) checkHost(r *http.Request) error {
|
||||
var allowed bool
|
||||
for _, allowedHost := range h.allowedOrigins {
|
||||
if r.Host == allowedHost {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("host not allowed: %s", r.Host),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkOrigin ensures that the Origin header, if
|
||||
// set, matches the intended target; prevents arbitrary
|
||||
// sites from issuing requests to our listener. It
|
||||
// returns the origin that was obtained from r.
|
||||
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
||||
origin := h.getOriginHost(r)
|
||||
if origin == "" {
|
||||
return origin, APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("missing required Origin header"),
|
||||
}
|
||||
}
|
||||
if !h.originAllowed(origin) {
|
||||
return origin, APIError{
|
||||
Code: http.StatusForbidden,
|
||||
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
||||
}
|
||||
}
|
||||
return origin, nil
|
||||
}
|
||||
|
||||
func (h adminHandler) getOriginHost(r *http.Request) string {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = r.Header.Get("Referer")
|
||||
}
|
||||
originURL, err := url.Parse(origin)
|
||||
if err == nil && originURL.Host != "" {
|
||||
origin = originURL.Host
|
||||
}
|
||||
return origin
|
||||
}
|
||||
|
||||
func (h adminHandler) originAllowed(origin string) bool {
|
||||
for _, allowedOrigin := range h.allowedOrigins {
|
||||
originCopy := origin
|
||||
if !strings.Contains(allowedOrigin, "://") {
|
||||
// no scheme specified, so allow both
|
||||
originCopy = strings.TrimPrefix(originCopy, "http://")
|
||||
originCopy = strings.TrimPrefix(originCopy, "https://")
|
||||
}
|
||||
if originCopy == allowedOrigin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodPost {
|
||||
return 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 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 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 APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("malformed Content-Type"),
|
||||
}
|
||||
}
|
||||
adapterName := ct[slashIdx+1:]
|
||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
|
||||
}
|
||||
}
|
||||
result, warnings, err := cfgAdapter.Adapt(body, nil)
|
||||
if err != nil {
|
||||
return 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 {
|
||||
Log().Named("admin.api.load").Error(err.Error())
|
||||
}
|
||||
w.Write(respBody)
|
||||
}
|
||||
body = result
|
||||
}
|
||||
}
|
||||
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err = Load(body, forceReload)
|
||||
if err != nil {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("loading config: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
Log().Named("admin.api").Info("load complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := readConfig(r.URL.Path, w)
|
||||
if err != nil {
|
||||
return APIError{Code: http.StatusBadRequest, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete:
|
||||
|
||||
// DELETE does not use a body, but the others do
|
||||
var body []byte
|
||||
if r.Method != http.MethodDelete {
|
||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
|
||||
}
|
||||
}
|
||||
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer bufPool.Put(buf)
|
||||
|
||||
_, err := io.Copy(buf, r.Body)
|
||||
if err != nil {
|
||||
return APIError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("reading request body: %v", err),
|
||||
}
|
||||
}
|
||||
body = buf.Bytes()
|
||||
}
|
||||
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method %s not allowed", r.Method),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||
idPath := r.URL.Path
|
||||
|
||||
parts := strings.Split(idPath, "/")
|
||||
if len(parts) < 3 || parts[2] == "" {
|
||||
return fmt.Errorf("request path is missing object ID")
|
||||
}
|
||||
if parts[0] != "" || parts[1] != "id" {
|
||||
return fmt.Errorf("malformed object path")
|
||||
}
|
||||
id := parts[2]
|
||||
|
||||
// map the ID to the expanded path
|
||||
currentCfgMu.RLock()
|
||||
expanded, ok := rawCfgIndex[id]
|
||||
defer currentCfgMu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown object ID '%s'", id)
|
||||
}
|
||||
|
||||
// piece the full URL path back together
|
||||
parts = append([]string{expanded}, parts[3:]...)
|
||||
r.URL.Path = path.Join(parts...)
|
||||
|
||||
return ErrInternalRedir
|
||||
}
|
||||
|
||||
func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||
err := handleUnload(w, r)
|
||||
if err != nil {
|
||||
Log().Named("admin.api").Error("unload error", zap.Error(err))
|
||||
}
|
||||
go func() {
|
||||
err := stopAdminServer(adminServer)
|
||||
var exitCode int
|
||||
if err != nil {
|
||||
exitCode = ExitCodeFailedQuit
|
||||
Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
|
||||
}
|
||||
Log().Named("admin.api").Info("stopping now, bye!! 👋")
|
||||
os.Exit(exitCode)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUnload stops the current configuration that is running.
|
||||
// Note that doing this can also be accomplished with DELETE /config/
|
||||
// but we leave this function because handleStop uses it.
|
||||
func handleUnload(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodPost {
|
||||
return APIError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method not allowed"),
|
||||
}
|
||||
}
|
||||
currentCfgMu.RLock()
|
||||
hasCfg := currentCfg != nil
|
||||
currentCfgMu.RUnlock()
|
||||
if !hasCfg {
|
||||
Log().Named("admin.api").Info("nothing to unload")
|
||||
return nil
|
||||
}
|
||||
Log().Named("admin.api").Info("unloading")
|
||||
if err := stopAndCleanup(); err != nil {
|
||||
Log().Named("admin.api").Error("error unloading", zap.Error(err))
|
||||
} else {
|
||||
Log().Named("admin.api").Info("unloading completed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsyncedConfigAccess traverses into the current config and performs
|
||||
// the operation at path according to method, using body and out as
|
||||
// needed. This is a low-level, unsynchronized function; most callers
|
||||
// will want to use changeConfig or readConfig instead. This requires a
|
||||
// read or write lock on currentCfgMu, depending on method (GET needs
|
||||
// only a read lock; all others need a write lock).
|
||||
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
||||
var err error
|
||||
var val interface{}
|
||||
|
||||
// if there is a request body, decode it into the
|
||||
// variable that will be set in the config according
|
||||
// to method and path
|
||||
if len(body) > 0 {
|
||||
err = json.Unmarshal(body, &val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding request body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(out)
|
||||
|
||||
cleanPath := strings.Trim(path, "/")
|
||||
if cleanPath == "" {
|
||||
return fmt.Errorf("no traversable path")
|
||||
}
|
||||
|
||||
parts := strings.Split(cleanPath, "/")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("path missing")
|
||||
}
|
||||
|
||||
// A path that ends with "..." implies:
|
||||
// 1) the part before it is an array
|
||||
// 2) the payload is an array
|
||||
// and means that the user wants to expand the elements
|
||||
// in the payload array and append each one into the
|
||||
// destination array, like so:
|
||||
// array = append(array, elems...)
|
||||
// This special case is handled below.
|
||||
ellipses := parts[len(parts)-1] == "..."
|
||||
if ellipses {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
|
||||
var ptr interface{} = rawCfg
|
||||
|
||||
traverseLoop:
|
||||
for i, part := range parts {
|
||||
switch v := ptr.(type) {
|
||||
case map[string]interface{}:
|
||||
// if the next part enters a slice, and the slice is our destination,
|
||||
// handle it specially (because appending to the slice copies the slice
|
||||
// header, which does not replace the original one like we want)
|
||||
if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
|
||||
var idx int
|
||||
if method != http.MethodPost {
|
||||
idxStr := parts[len(parts)-1]
|
||||
idx, err = strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
||||
path, idxStr, err)
|
||||
}
|
||||
if idx < 0 || idx >= len(arr) {
|
||||
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
||||
}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
err = enc.Encode(arr[idx])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
if ellipses {
|
||||
valArray, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("final element is not an array")
|
||||
}
|
||||
v[part] = append(arr, valArray...)
|
||||
} else {
|
||||
v[part] = append(arr, val)
|
||||
}
|
||||
case http.MethodPut:
|
||||
// avoid creation of new slice and a second copy (see
|
||||
// https://github.com/golang/go/wiki/SliceTricks#insert)
|
||||
arr = append(arr, nil)
|
||||
copy(arr[idx+1:], arr[idx:])
|
||||
arr[idx] = val
|
||||
v[part] = arr
|
||||
case http.MethodPatch:
|
||||
arr[idx] = val
|
||||
case http.MethodDelete:
|
||||
v[part] = append(arr[:idx], arr[idx+1:]...)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
}
|
||||
break traverseLoop
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
err = enc.Encode(v[part])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %v", err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
// if the part is an existing list, POST appends to
|
||||
// it, otherwise it just sets or creates the value
|
||||
if arr, ok := v[part].([]interface{}); ok {
|
||||
if ellipses {
|
||||
valArray, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("final element is not an array")
|
||||
}
|
||||
v[part] = append(arr, valArray...)
|
||||
} else {
|
||||
v[part] = append(arr, val)
|
||||
}
|
||||
} else {
|
||||
v[part] = val
|
||||
}
|
||||
case http.MethodPut:
|
||||
if _, ok := v[part]; ok {
|
||||
return fmt.Errorf("[%s] key already exists: %s", path, part)
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodPatch:
|
||||
if _, ok := v[part]; !ok {
|
||||
return fmt.Errorf("[%s] key does not exist: %s", path, part)
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodDelete:
|
||||
delete(v, part)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
}
|
||||
} else {
|
||||
// if we are "PUTting" a new resource, the key(s) in its path
|
||||
// might not exist yet; that's OK but we need to make them as
|
||||
// we go, while we still have a pointer from the level above
|
||||
if v[part] == nil && method == http.MethodPut {
|
||||
v[part] = make(map[string]interface{})
|
||||
}
|
||||
ptr = v[part]
|
||||
}
|
||||
|
||||
case []interface{}:
|
||||
partInt, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||
strings.Join(parts[:i+1], "/"), part, err)
|
||||
}
|
||||
if partInt < 0 || partInt >= len(v) {
|
||||
return fmt.Errorf("[/%s] array index out of bounds: %s",
|
||||
strings.Join(parts[:i+1], "/"), part)
|
||||
}
|
||||
ptr = v[partInt]
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveMetaFields removes meta fields like "@id" from a JSON message
|
||||
// by using a simple regular expression. (An alternate way to do this
|
||||
// would be to delete them from the raw, map[string]interface{}
|
||||
// representation as they are indexed, then iterate the index we made
|
||||
// and add them back after encoding as JSON, but this is simpler.)
|
||||
func RemoveMetaFields(rawJSON []byte) []byte {
|
||||
return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte {
|
||||
// matches with a comma on both sides (when "@id" property is
|
||||
// not the first or last in the object) need to keep exactly
|
||||
// one comma for correct JSON syntax
|
||||
comma := []byte{','}
|
||||
if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
|
||||
return comma
|
||||
}
|
||||
return []byte{}
|
||||
})
|
||||
}
|
||||
|
||||
// AdminHandler is like http.Handler except ServeHTTP may return an error.
|
||||
//
|
||||
// If any handler encounters an error, it should be returned for proper
|
||||
// handling.
|
||||
type AdminHandler interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request) error
|
||||
}
|
||||
|
||||
// AdminHandlerFunc is a convenience type like http.HandlerFunc.
|
||||
type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
// ServeHTTP implements the Handler interface.
|
||||
func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
return f(w, r)
|
||||
}
|
||||
|
||||
// APIError is a structured error that every API
|
||||
// handler should return for consistency in logging
|
||||
// and client responses. If Message is unset, then
|
||||
// Err.Error() will be serialized in its place.
|
||||
type APIError struct {
|
||||
Code int `json:"-"`
|
||||
Err error `json:"-"`
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultAdminListen is the address for the admin
|
||||
// listener, if none is specified at startup.
|
||||
DefaultAdminListen = "localhost:2019"
|
||||
|
||||
// ErrInternalRedir indicates an internal redirect
|
||||
// and is useful when admin API handlers rewrite
|
||||
// the request; in that case, authentication and
|
||||
// authorization needs to happen again for the
|
||||
// rewritten request.
|
||||
ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
|
||||
|
||||
// DefaultAdminConfig is the default configuration
|
||||
// for the administration endpoint.
|
||||
DefaultAdminConfig = &AdminConfig{
|
||||
Listen: DefaultAdminListen,
|
||||
}
|
||||
)
|
||||
|
||||
// idRegexp is used to match ID fields and their associated values
|
||||
// in the config. It also matches adjacent commas so that syntax
|
||||
// can be preserved no matter where in the object the field appears.
|
||||
// It supports string and most numeric values.
|
||||
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `":\s?(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
|
||||
|
||||
const (
|
||||
rawConfigKey = "config"
|
||||
idKey = "@id"
|
||||
)
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
var adminServer *http.Server
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.8.windows-amd64.zip
|
||||
- 7z x go1.8.windows-amd64.zip -y -oC:\ > NUL
|
||||
- go version
|
||||
- go env
|
||||
- go get -t ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get github.com/gordonklaus/ineffassign
|
||||
- set PATH=%GOPATH%\bin;%PATH%
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- go vet ./...
|
||||
- go test -race ./...
|
||||
- ineffassign .
|
||||
|
||||
after_test:
|
||||
- golint ./...
|
||||
|
||||
deploy: off
|
||||
@@ -0,0 +1,34 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
}
|
||||
|
||||
os.Setenv("CADDYPATH", "testpath")
|
||||
if actual, expected := AssetsPath(), "testpath"; actual != expected {
|
||||
t.Errorf("Expected path to be %v, got: %v", expected, actual)
|
||||
}
|
||||
os.Setenv("CADDYPATH", "")
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
# Mutilated beyond recognition from the example at:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
|
||||
|
||||
trigger:
|
||||
- v2
|
||||
|
||||
schedules:
|
||||
- cron: "0 0 * * *"
|
||||
displayName: Daily midnight fuzzing
|
||||
branches:
|
||||
include:
|
||||
- v2
|
||||
always: true
|
||||
|
||||
variables:
|
||||
GOROOT: $(gorootDir)/go
|
||||
GOPATH: $(system.defaultWorkingDirectory)/gopath
|
||||
GOBIN: $(GOPATH)/bin
|
||||
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
|
||||
# TODO: Remove once it's enabled by default
|
||||
GO111MODULE: on
|
||||
|
||||
jobs:
|
||||
- job: crossPlatformTest
|
||||
displayName: "Cross-Platform Tests"
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# The low performance is partly due to PowerShell's attempt to update the progress bar. Disabling it speeds up the process.
|
||||
# Reference: https://github.com/PowerShell/PowerShell/issues/2138
|
||||
- powershell: |
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
Write-Host "Downloading Go..."
|
||||
(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)"
|
||||
7z x "$(LATEST_GO).windows-amd64.zip" -o"$(gorootDir)"
|
||||
condition: eq( variables['Agent.OS'], 'Windows_NT' )
|
||||
displayName: Install Go on Windows
|
||||
|
||||
- bash: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.19.1
|
||||
displayName: Install golangci-lint
|
||||
|
||||
- script: |
|
||||
go get github.com/axw/gocov/gocov
|
||||
go get github.com/AlekSi/gocov-xml
|
||||
go get -u github.com/jstemmer/go-junit-report
|
||||
displayName: Install test and coverage analysis tools
|
||||
|
||||
- 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 ./...
|
||||
mkdir test-results
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Get dependencies
|
||||
|
||||
# its behavior is governed by .golangci.yml
|
||||
- script: |
|
||||
(golangci-lint run --out-format junit-xml) > test-results/lint-result.xml
|
||||
exit 0
|
||||
workingDirectory: '$(modulePath)'
|
||||
continueOnError: true
|
||||
displayName: Run lint check
|
||||
|
||||
- script: |
|
||||
(go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||
workingDirectory: '$(modulePath)'
|
||||
continueOnError: true
|
||||
displayName: Run tests
|
||||
|
||||
- script: |
|
||||
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
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Prepare coverage reports
|
||||
|
||||
- script: |
|
||||
(cat ./test-results/test-result.out | go-junit-report) > test-results/test-result.xml
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Prepare test report
|
||||
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish test coverage report
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: $(modulePath)/coverage/coverage.xml
|
||||
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish unit test
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: $(modulePath)/test-results/test-result.xml
|
||||
testRunTitle: $(agent.OS) Unit Test
|
||||
mergeTestResults: false
|
||||
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish lint results
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: $(modulePath)/test-results/lint-result.xml
|
||||
testRunTitle: $(agent.OS) Lint
|
||||
mergeTestResults: false
|
||||
|
||||
- bash: |
|
||||
exit 1
|
||||
condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues')
|
||||
displayName: Coerce correct build result
|
||||
|
||||
- job: fuzzing
|
||||
displayName: 'Fuzzing'
|
||||
# Only run this job on schedules or PRs for non-forks.
|
||||
condition: or(eq(variables['System.PullRequest.IsFork'], 'False'), eq(variables['Build.Reason'], 'Schedule') )
|
||||
strategy:
|
||||
matrix:
|
||||
linux:
|
||||
imageName: ubuntu-16.04
|
||||
gorootDir: /usr/local
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
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
|
||||
|
||||
- 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: |
|
||||
# Install Clang-7.0 because other versions seem to be missing the file libclang_rt.fuzzer-x86_64.a
|
||||
sudo add-apt-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main"
|
||||
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
|
||||
sudo apt update && sudo apt install -y clang-7 lldb-7 lld-7
|
||||
|
||||
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.74/fuzzit_Linux_x86_64
|
||||
chmod a+x fuzzit
|
||||
mv fuzzit $(GOBIN)
|
||||
displayName: Download go-fuzz tools and the Fuzzit CLI, and move Fuzzit CLI to GOBIN
|
||||
condition: and(eq(variables['System.PullRequest.IsFork'], 'False') , eq( variables['Agent.OS'], 'Linux' ))
|
||||
|
||||
- bash: |
|
||||
declare -A fuzzers_funcs=(\
|
||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
|
||||
["./caddyconfig/caddyfile/parse_fuzz.go"]="FuzzParseCaddyfile" \
|
||||
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
|
||||
["./replacer_fuzz.go"]="FuzzReplacer" \
|
||||
)
|
||||
|
||||
declare -A fuzzers_targets=(\
|
||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
|
||||
["./caddyconfig/caddyfile/parse_fuzz.go"]="parse-caddyfile" \
|
||||
["./listeners_fuzz.go"]="parse-network-address" \
|
||||
["./replacer_fuzz.go"]="replacer" \
|
||||
)
|
||||
|
||||
fuzz_type="local-regression"
|
||||
if [[ $(Build.Reason) == "Schedule" ]]; then
|
||||
fuzz_type="fuzzing"
|
||||
fi
|
||||
echo "Fuzzing type: $fuzz_type"
|
||||
|
||||
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]}" -libfuzzer -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.a" $FUZZER_DIRECTORY
|
||||
echo "Generating fuzzer binary of func ${fuzzers_funcs[$f]} which resides in $f"
|
||||
clang-7 -fsanitize=fuzzer "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.a" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}"
|
||||
fuzzit create job caddyserver/${fuzzers_targets[$f]} $FUZZER_DIRECTORY/${fuzzers_targets[$f]} --api-key ${FUZZIT_API_KEY} --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
|
||||
echo "Completed $f"
|
||||
done
|
||||
env:
|
||||
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Generate fuzzers & submit them to Fuzzit
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Caddy build script. Automates proper versioning.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# $ ./build.bash [output_filename] [git_repo]
|
||||
#
|
||||
# Outputs compiled program in current directory.
|
||||
# Default git repo is current directory.
|
||||
# Builds always take place from current directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: ${output_filename:="${1:-}"}
|
||||
: ${output_filename:=""}
|
||||
|
||||
: ${git_repo:="${2:-}"}
|
||||
: ${git_repo:="."}
|
||||
|
||||
pkg=github.com/mholt/caddy/caddy/caddymain
|
||||
ldflags=()
|
||||
|
||||
# Timestamp of build
|
||||
name="${pkg}.buildDate"
|
||||
value=$(date -u +"%a %b %d %H:%M:%S %Z %Y")
|
||||
ldflags+=("-X" "\"${name}=${value}\"")
|
||||
|
||||
# Current tag, if HEAD is on a tag
|
||||
name="${pkg}.gitTag"
|
||||
set +e
|
||||
value="$(git -C "${git_repo}" describe --exact-match HEAD 2>/dev/null)"
|
||||
set -e
|
||||
ldflags+=("-X" "\"${name}=${value}\"")
|
||||
|
||||
# Nearest tag on branch
|
||||
name="${pkg}.gitNearestTag"
|
||||
value="$(git -C "${git_repo}" describe --abbrev=0 --tags HEAD)"
|
||||
ldflags+=("-X" "\"${name}=${value}\"")
|
||||
|
||||
# Commit SHA
|
||||
name="${pkg}.gitCommit"
|
||||
value="$(git -C "${git_repo}" rev-parse --short HEAD)"
|
||||
ldflags+=("-X" "\"${name}=${value}\"")
|
||||
|
||||
# Summary of uncommitted changes
|
||||
name="${pkg}.gitShortStat"
|
||||
value="$(git -C "${git_repo}" diff-index --shortstat HEAD)"
|
||||
ldflags+=("-X" "\"${name}=${value}\"")
|
||||
|
||||
# List of modified files
|
||||
name="${pkg}.gitFilesModified"
|
||||
value="$(git -C "${git_repo}" diff-index --name-only HEAD)"
|
||||
ldflags+=("-X" "\"${name}=${value}\"")
|
||||
|
||||
go build -ldflags "${ldflags[*]}" -o "${output_filename}"
|
||||
@@ -0,0 +1,260 @@
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
// plug in the HTTP server type
|
||||
_ "github.com/mholt/caddy/caddyhttp"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
// This is where other plugins get plugged in (imported)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.TrapSignals()
|
||||
setVersion()
|
||||
|
||||
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
|
||||
flag.BoolVar(&caddytls.DisableHTTPChallenge, "disable-http-challenge", caddytls.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
|
||||
flag.BoolVar(&caddytls.DisableTLSSNIChallenge, "disable-tls-sni-challenge", caddytls.DisableTLSSNIChallenge, "Disable the ACME TLS-SNI challenge")
|
||||
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
|
||||
flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
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(&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()
|
||||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = appVersion
|
||||
acme.UserAgent = appName + "/" + appVersion
|
||||
|
||||
// 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:
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: 100,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
fmt.Printf("%s %s\n", appName, appVersion)
|
||||
if devBuild && gitShortStat != "" {
|
||||
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if plugins {
|
||||
fmt.Println(caddy.DescribePlugins())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set CPU cap
|
||||
err := setCPU(cpu)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// Executes Startup events
|
||||
caddy.EmitEvent(caddy.StartupEvent)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(caddyfileinput)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// setVersion figures out the version information
|
||||
// based on variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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%).
|
||||
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)
|
||||
} 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
|
||||
}
|
||||
|
||||
const appName = "Caddy"
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
logfile string
|
||||
revoke string
|
||||
version bool
|
||||
plugins bool
|
||||
validate bool
|
||||
)
|
||||
|
||||
// Build information obtained with the help of -ldflags
|
||||
var (
|
||||
appVersion = "(untracked dev build)" // inferred at startup
|
||||
devBuild = true // inferred at startup
|
||||
|
||||
buildDate string // date -u
|
||||
gitTag string // git describe --exact-match HEAD 2> /dev/null
|
||||
gitNearestTag string // git describe --abbrev=0 --tags HEAD
|
||||
gitCommit string // git rev-parse HEAD
|
||||
gitShortStat string // git diff-index --shortstat
|
||||
gitFilesModified string // git diff-index --name-only HEAD
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"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
|
||||
} {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// By moving the application's package main logic into
|
||||
// a package other than main, it becomes much easier to
|
||||
// wrap caddy for custom builds that are go-gettable.
|
||||
// https://forum.caddyserver.com/t/my-wish-for-0-9-go-gettable-custom-builds/59?u=matt
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/mholt/caddy/caddy/caddymain"
|
||||
|
||||
var run = caddymain.Run // replaced for tests
|
||||
|
||||
func main() {
|
||||
run()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func TestIsLoopback(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{"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},
|
||||
} {
|
||||
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},
|
||||
{"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerAddrEqual(t *testing.T) {
|
||||
ln1, err := net.Listen("tcp", "[::]:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln1.Close()
|
||||
ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln2.Close()
|
||||
ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
for i, test := range []struct {
|
||||
ln net.Listener
|
||||
addr string
|
||||
expect bool
|
||||
}{
|
||||
{ln1, ":1234", false},
|
||||
{ln1, "0.0.0.0:1234", false},
|
||||
{ln1, "0.0.0.0", false},
|
||||
{ln1, ":" + ln1port, true},
|
||||
{ln1, "0.0.0.0:" + ln1port, true},
|
||||
{ln2, ":" + ln2port, false},
|
||||
{ln2, "127.0.0.1:1234", false},
|
||||
{ln2, "127.0.0.1", false},
|
||||
{ln2, "127.0.0.1:" + ln2port, true},
|
||||
} {
|
||||
if got, want := listenerAddrEqual(test.ln, test.addr), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// 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"
|
||||
"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, bytes.NewReader(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.
|
||||
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)
|
||||
@@ -1,371 +0,0 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure. An empty
|
||||
// Dispenser is invalid; call NewDispenser to make a proper instance.
|
||||
type Dispenser struct {
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser filled with the given tokens.
|
||||
func NewDispenser(tokens []Token) *Dispenser {
|
||||
return &Dispenser{
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Prev moves to the previous token. It does the inverse
|
||||
// of Next(), except this function may decrement the cursor
|
||||
// to -1 so that the next call to Next() points to the
|
||||
// first token; this allows dispensing to "start over". This
|
||||
// method returns true if the cursor ends up pointing to a
|
||||
// valid token.
|
||||
func (d *Dispenser) Prev() bool {
|
||||
if d.cursor > -1 {
|
||||
d.cursor--
|
||||
return d.cursor > -1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line and if it is not a block opening (open curly
|
||||
// brace). Returns true if an argument token was
|
||||
// loaded; false otherwise. If false, all tokens on
|
||||
// the line have been consumed except for potentially
|
||||
// a block opening. It handles imported tokens
|
||||
// correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if !d.nextOnSameLine() {
|
||||
return false
|
||||
}
|
||||
if d.Val() == "{" {
|
||||
// roll back; a block opening is not an argument
|
||||
d.cursor--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nextOnSameLine advances the cursor if the next
|
||||
// token is on the same line of the same file.
|
||||
func (d *Dispenser) nextOnSameLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block nested more than initialNestingLevel.
|
||||
// In other words, a loop over NextBlock() will iterate
|
||||
// all tokens in the block assuming the next token is an
|
||||
// open curly brace, until the matching closing brace.
|
||||
// The open and closing brace tokens for the outer-most
|
||||
// block will be consumed internally and omitted from
|
||||
// the iteration.
|
||||
//
|
||||
// Proper use of this method looks like this:
|
||||
//
|
||||
// for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
// }
|
||||
//
|
||||
// However, in simple cases where it is known that the
|
||||
// Dispenser is new and has not already traversed state
|
||||
// by a loop over NextBlock(), this will do:
|
||||
//
|
||||
// for d.NextBlock(0) {
|
||||
// }
|
||||
//
|
||||
// As with other token parsing logic, a loop over
|
||||
// NextBlock() should be contained within a loop over
|
||||
// Next(), as it is usually prudent to skip the initial
|
||||
// token.
|
||||
func (d *Dispenser) NextBlock(initialNestingLevel int) bool {
|
||||
if d.nesting > initialNestingLevel {
|
||||
if !d.Next() {
|
||||
return false // should be EOF error
|
||||
}
|
||||
if d.Val() == "}" {
|
||||
d.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
|
||||
}
|
||||
|
||||
// NewFromNextTokens 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) NewFromNextTokens() *Dispenser {
|
||||
tkns := []Token{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 to include
|
||||
// all the tokens including surrounding curly braces
|
||||
// for a new dispenser to have
|
||||
d.Prev()
|
||||
tkns = append(tkns, d.Token())
|
||||
d.Next()
|
||||
openedBlock = true
|
||||
}
|
||||
tkns = append(tkns, d.Token())
|
||||
}
|
||||
if openedBlock {
|
||||
// include closing brace accordingly
|
||||
tkns = append(tkns, d.Token())
|
||||
}
|
||||
return NewDispenser(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
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// 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.
|
||||
|
||||
// +build gofuzz
|
||||
// +build gofuzz_libfuzzer
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func FuzzParseCaddyfile(data []byte) (score int) {
|
||||
sb, err := Parse("Caddyfile", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
// if both an error is received and some ServerBlocks,
|
||||
// then the parse was able to parse partially. Mark this
|
||||
// result as interesting to push the fuzzer further through the parser.
|
||||
if sb != nil && len(sb) > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
// and then adds a key to that object named fieldName with the value fieldVal.
|
||||
// This is useful for JSON-encoding module values where the module name has to
|
||||
// be described within the object by a certain key; for example,
|
||||
// "responder": "file_server" for a file server HTTP responder. The val must
|
||||
// encode into a map[string]interface{} (i.e. it must be a struct or map),
|
||||
// and any errors are converted into warnings, so this can be conveniently
|
||||
// used when filling a struct. For correct code, there should be no errors.
|
||||
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.
|
||||
func RegisterAdapter(name string, adapter Adapter) error {
|
||||
if _, ok := configAdapters[name]; ok {
|
||||
return fmt.Errorf("%s: already registered", name)
|
||||
}
|
||||
configAdapters[name] = adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
var configAdapters = make(map[string]Adapter)
|
||||
@@ -1,334 +0,0 @@
|
||||
// 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"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/mholt/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
|
||||
// beceause 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) (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)
|
||||
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 {
|
||||
sbmap[addr] = append(sbmap[addr], serverBlock{
|
||||
block: caddyfile.ServerBlock{
|
||||
Keys: keys,
|
||||
Segments: sblock.block.Segments,
|
||||
},
|
||||
pile: sblock.pile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
var sbaddrs []sbAddrAssociation
|
||||
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) ([]string, error) {
|
||||
addr, err := ParseAddress(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing key: %v", err)
|
||||
}
|
||||
addr = addr.Normalize()
|
||||
|
||||
lnPort := DefaultPort
|
||||
if addr.Port != "" {
|
||||
// port explicitly defined
|
||||
lnPort = addr.Port
|
||||
} else if certmagic.HostQualifies(addr.Host) {
|
||||
// automatic HTTPS
|
||||
lnPort = strconv.Itoa(certmagic.HTTPSPort)
|
||||
}
|
||||
|
||||
// the bind directive specifies hosts, but is optional
|
||||
var lnHosts []string
|
||||
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 {
|
||||
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
|
||||
}
|
||||
|
||||
// now turn map into list
|
||||
var listenersList []string
|
||||
for lnStr := range listeners {
|
||||
listenersList = append(listenersList, lnStr)
|
||||
}
|
||||
// sort.Strings(listenersList) // TODO: is sorting necessary?
|
||||
|
||||
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) {
|
||||
httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
input := str
|
||||
|
||||
// Split input into components (prepend with // to force host portion by default)
|
||||
if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
|
||||
str = "//" + str
|
||||
}
|
||||
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return Address{}, err
|
||||
}
|
||||
|
||||
// separate host and port
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(u.Host + ":")
|
||||
if err != nil {
|
||||
host = u.Host
|
||||
}
|
||||
}
|
||||
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if u.Scheme == "http" {
|
||||
port = httpPort
|
||||
} else if u.Scheme == "https" {
|
||||
port = httpsPort
|
||||
}
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) {
|
||||
return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
|
||||
}
|
||||
|
||||
// TODO: which of the methods on Address are even used?
|
||||
|
||||
// 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
|
||||
if !caseSensitivePath {
|
||||
path = strings.ToLower(path)
|
||||
}
|
||||
|
||||
// ensure host is normalized if it's an IP address
|
||||
host := a.Host
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
host = ip.String()
|
||||
}
|
||||
|
||||
return Address{
|
||||
Original: a.Original,
|
||||
Scheme: strings.ToLower(a.Scheme),
|
||||
Host: strings.ToLower(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
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultPort is the default port to use.
|
||||
DefaultPort = "2015"
|
||||
|
||||
caseSensitivePath = false // TODO: Used?
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
// 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.
|
||||
|
||||
// +build gofuzz
|
||||
// +build gofuzz_libfuzzer
|
||||
|
||||
package httpcaddyfile
|
||||
|
||||
func FuzzParseAddress(data []byte) int {
|
||||
addr, err := ParseAddress(string(data))
|
||||
if err != nil {
|
||||
if addr == (Address{}) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port, path string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "", "localhost", "", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", "", false},
|
||||
{`localhost:`, "", "localhost", "", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false},
|
||||
{`:1234`, "", "", "1234", "", false},
|
||||
{`[::1]`, "", "::1", "", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", "", false},
|
||||
{`:`, "", "", "", "", false},
|
||||
{`: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`, "", "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", "", false},
|
||||
{`https://localhost`, "https", "localhost", "443", "", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false},
|
||||
{`http://[::1]`, "http", "::1", "80", "", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", "", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
|
||||
{``, "", "", "", "", false},
|
||||
{`::1`, "", "::1", "", "", true},
|
||||
{`localhost::`, "", "localhost::", "", "", true},
|
||||
{`#$%@`, "", "", "", "", true},
|
||||
{`host/path`, "", "host", "", "/path", false},
|
||||
{`http://host/`, "http", "host", "80", "/", false},
|
||||
{`//asdf`, "", "asdf", "", "", false},
|
||||
{`:1234/asdf`, "", "", "1234", "/asdf", false},
|
||||
{`http://host/path`, "http", "host", "80", "/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", i, test.input)
|
||||
}
|
||||
|
||||
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: "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: ":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
|
||||
}
|
||||
expect := tc.expect
|
||||
if !caseSensitivePath {
|
||||
// every other part of the address should be lowercased when normalized,
|
||||
// so simply lower-case the whole thing to do case-insensitive comparison
|
||||
// of the path as well
|
||||
expect = strings.ToLower(expect)
|
||||
}
|
||||
if actual := addr.Normalize().Key(); actual != expect {
|
||||
t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDirective("bind", parseBind)
|
||||
RegisterDirective("root", parseRoot)
|
||||
RegisterDirective("tls", parseTLS)
|
||||
RegisterHandlerDirective("redir", parseRedir)
|
||||
RegisterHandlerDirective("respond", parseRespond)
|
||||
}
|
||||
|
||||
func parseBind(h Helper) ([]ConfigValue, error) {
|
||||
var lnHosts []string
|
||||
for h.Next() {
|
||||
lnHosts = append(lnHosts, h.RemainingArgs()...)
|
||||
}
|
||||
return h.NewBindAddresses(lnHosts), nil
|
||||
}
|
||||
|
||||
func parseRoot(h Helper) ([]ConfigValue, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
matcherSet, ok, err := h.MatcherToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
// no matcher token; oops
|
||||
h.Dispenser.Prev()
|
||||
}
|
||||
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
root := h.Val()
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
varsHandler := caddyhttp.VarsMiddleware{"root": root}
|
||||
route := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", nil),
|
||||
},
|
||||
}
|
||||
if matcherSet != nil {
|
||||
route.MatcherSetsRaw = []caddy.ModuleMap{matcherSet}
|
||||
}
|
||||
|
||||
return h.NewVarsRoute(route), nil
|
||||
}
|
||||
|
||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
var configVals []ConfigValue
|
||||
|
||||
var cp *caddytls.ConnectionPolicy
|
||||
var fileLoader caddytls.FileLoader
|
||||
var folderLoader caddytls.FolderLoader
|
||||
var mgr caddytls.ACMEManagerMaker
|
||||
var off bool
|
||||
|
||||
// fill in global defaults, if configured
|
||||
if email := h.Option("email"); email != nil {
|
||||
mgr.Email = email.(string)
|
||||
}
|
||||
if acmeCA := h.Option("acme_ca"); acmeCA != nil {
|
||||
mgr.CA = acmeCA.(string)
|
||||
}
|
||||
|
||||
for h.Next() {
|
||||
// file certificate loader
|
||||
firstLine := h.RemainingArgs()
|
||||
switch len(firstLine) {
|
||||
case 0:
|
||||
case 1:
|
||||
if firstLine[0] == "off" {
|
||||
off = true
|
||||
} else {
|
||||
mgr.Email = firstLine[0]
|
||||
}
|
||||
case 2:
|
||||
fileLoader = append(fileLoader, caddytls.CertKeyFilePair{
|
||||
Certificate: firstLine[0],
|
||||
Key: firstLine[1],
|
||||
// TODO: add tags, for enterprise module's certificate selection
|
||||
})
|
||||
default:
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
var hasBlock bool
|
||||
for h.NextBlock(0) {
|
||||
hasBlock = true
|
||||
|
||||
switch h.Val() {
|
||||
// connection policy
|
||||
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])
|
||||
}
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
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])
|
||||
}
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
cp.ProtocolMax = args[1]
|
||||
}
|
||||
case "ciphers":
|
||||
for h.NextArg() {
|
||||
if _, ok := caddytls.SupportedCipherSuites[h.Val()]; !ok {
|
||||
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||
}
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
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())
|
||||
}
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
cp.Curves = append(cp.Curves, h.Val())
|
||||
}
|
||||
case "alpn":
|
||||
args := h.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
cp.ALPN = args
|
||||
|
||||
// certificate folder loader
|
||||
case "load":
|
||||
folderLoader = append(folderLoader, h.RemainingArgs()...)
|
||||
|
||||
// automation policy
|
||||
case "ca":
|
||||
arg := h.RemainingArgs()
|
||||
if len(arg) != 1 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
mgr.CA = arg[0]
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// certificate loaders
|
||||
if len(fileLoader) > 0 {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.certificate_loader",
|
||||
Value: fileLoader,
|
||||
})
|
||||
// ensure server uses HTTPS by setting non-nil conn policy
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
}
|
||||
if len(folderLoader) > 0 {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.certificate_loader",
|
||||
Value: folderLoader,
|
||||
})
|
||||
// ensure server uses HTTPS by setting non-nil conn policy
|
||||
if cp == nil {
|
||||
cp = new(caddytls.ConnectionPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
// connection policy
|
||||
if cp != nil {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.connection_policy",
|
||||
Value: cp,
|
||||
})
|
||||
}
|
||||
|
||||
// automation policy
|
||||
if off {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.off",
|
||||
Value: true,
|
||||
})
|
||||
} else if !reflect.DeepEqual(mgr, caddytls.ACMEManagerMaker{}) {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.automation_manager",
|
||||
Value: mgr,
|
||||
})
|
||||
}
|
||||
|
||||
return configVals, nil
|
||||
}
|
||||
|
||||
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 = "307"
|
||||
}
|
||||
var body string
|
||||
if code == "meta" {
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
// 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"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// defaultDirectiveOrder specifies the order
|
||||
// to apply directives in HTTP routes.
|
||||
var defaultDirectiveOrder = []string{
|
||||
"rewrite",
|
||||
"strip_prefix",
|
||||
"strip_suffix",
|
||||
"uri_replace",
|
||||
"try_files",
|
||||
"basicauth",
|
||||
"headers",
|
||||
"request_header",
|
||||
"encode",
|
||||
"templates",
|
||||
"redir",
|
||||
"respond",
|
||||
"reverse_proxy",
|
||||
"php_fastcgi",
|
||||
"file_server",
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
// Helper is a type which helps setup a value from
|
||||
// Caddyfile tokens.
|
||||
type Helper struct {
|
||||
*caddyfile.Dispenser
|
||||
options map[string]interface{}
|
||||
warnings *[]caddyconfig.Warning
|
||||
matcherDefs map[string]caddy.ModuleMap
|
||||
parentBlock caddyfile.ServerBlock
|
||||
}
|
||||
|
||||
// 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 current token is (possibly) a matcher, and
|
||||
// if so, returns the matcher set along with a true value. If the current
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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}}
|
||||
}
|
||||
|
||||
// NewVarsRoute returns config values relevant to adding a
|
||||
// "vars" wrapper route to the config.
|
||||
func (h Helper) NewVarsRoute(route caddyhttp.Route) []ConfigValue {
|
||||
return []ConfigValue{{Class: "var", Value: route}}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// serverBlock pairs a Caddyfile server block
|
||||
// with a "pile" of config values, keyed by class
|
||||
// name.
|
||||
type serverBlock struct {
|
||||
block caddyfile.ServerBlock
|
||||
pile map[string][]ConfigValue // config values obtained from directives
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
var registeredDirectives = make(map[string]UnmarshalFunc)
|
||||
@@ -1,647 +0,0 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"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/mholt/certmagic"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
|
||||
}
|
||||
|
||||
// ServerType can set up a config from an HTTP Caddyfile.
|
||||
type ServerType struct {
|
||||
}
|
||||
|
||||
// Setup makes a config from the tokens.
|
||||
func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||
options map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error) {
|
||||
var warnings []caddyconfig.Warning
|
||||
|
||||
var serverBlocks []serverBlock
|
||||
for _, sblock := range originalServerBlocks {
|
||||
serverBlocks = append(serverBlocks, serverBlock{
|
||||
block: sblock,
|
||||
pile: make(map[string][]ConfigValue),
|
||||
})
|
||||
}
|
||||
|
||||
// global configuration
|
||||
if len(serverBlocks) > 0 && len(serverBlocks[0].block.Keys) == 0 {
|
||||
sb := serverBlocks[0]
|
||||
for _, segment := range sb.block.Segments {
|
||||
dir := segment.Directive()
|
||||
var val interface{}
|
||||
var err error
|
||||
disp := caddyfile.NewDispenser(segment)
|
||||
// TODO: make this switch into a map
|
||||
switch dir {
|
||||
case "http_port":
|
||||
val, err = parseOptHTTPPort(disp)
|
||||
case "https_port":
|
||||
val, err = parseOptHTTPSPort(disp)
|
||||
case "handler_order":
|
||||
val, err = parseOptHandlerOrder(disp)
|
||||
case "experimental_http3":
|
||||
val, err = parseOptExperimentalHTTP3(disp)
|
||||
case "storage":
|
||||
val, err = parseOptStorage(disp)
|
||||
case "acme_ca":
|
||||
val, err = parseOptACMECA(disp)
|
||||
case "email":
|
||||
val, err = parseOptEmail(disp)
|
||||
case "admin":
|
||||
val, err = parseOptAdmin(disp)
|
||||
default:
|
||||
return nil, warnings, fmt.Errorf("unrecognized parameter name: %s", dir)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, warnings, fmt.Errorf("%s: %v", dir, err)
|
||||
}
|
||||
options[dir] = val
|
||||
}
|
||||
serverBlocks = serverBlocks[1:]
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
// replace shorthand placeholders (which are
|
||||
// convenient when writing a Caddyfile) with
|
||||
// their actual placeholder identifiers or
|
||||
// variable names
|
||||
replacer := strings.NewReplacer(
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{host}", "{http.request.host}",
|
||||
"{hostport}", "{http.request.hostport}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{scheme}", "{http.request.scheme}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{query}", "{http.request.uri.query_string}",
|
||||
)
|
||||
for _, segment := range sb.block.Segments {
|
||||
for i := 0; i < len(segment); i++ {
|
||||
segment[i].Text = replacer.Replace(segment[i].Text)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sb.block.Keys) == 0 {
|
||||
return nil, warnings, fmt.Errorf("server block without any key is global configuration, and if used, it must be first")
|
||||
}
|
||||
|
||||
// extract matcher definitions
|
||||
d := sb.block.DispenseDirective("matcher")
|
||||
matcherDefs, err := parseMatcherDefinitions(d)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for _, segment := range sb.block.Segments {
|
||||
dir := segment.Directive()
|
||||
if dir == "matcher" {
|
||||
// TODO: This is a special case because we pre-processed it; handle this better
|
||||
continue
|
||||
}
|
||||
if dirFunc, ok := registeredDirectives[dir]; ok {
|
||||
results, err := dirFunc(Helper{
|
||||
Dispenser: caddyfile.NewDispenser(segment),
|
||||
options: options,
|
||||
warnings: &warnings,
|
||||
matcherDefs: matcherDefs,
|
||||
parentBlock: sb.block,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||
}
|
||||
for _, result := range results {
|
||||
result.directive = dir
|
||||
sb.pile[result.Class] = append(sb.pile[result.Class], result)
|
||||
}
|
||||
} else {
|
||||
tkn := segment[0]
|
||||
return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// map
|
||||
sbmap, err := st.mapAddressToServerBlocks(serverBlocks)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
// reduce
|
||||
pairings := st.consolidateAddrMappings(sbmap)
|
||||
|
||||
// each pairing of listener addresses to list of server
|
||||
// blocks is basically a server definition
|
||||
servers, err := st.serversFromPairings(pairings, options, &warnings)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
// now that each server is configured, make the HTTP app
|
||||
httpApp := caddyhttp.App{
|
||||
HTTPPort: tryInt(options["http_port"], &warnings),
|
||||
HTTPSPort: tryInt(options["https_port"], &warnings),
|
||||
Servers: servers,
|
||||
}
|
||||
|
||||
// now for the TLS app! (TODO: refactor into own func)
|
||||
tlsApp := caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||
for _, p := range pairings {
|
||||
for i, sblock := range p.serverBlocks {
|
||||
// tls automation policies
|
||||
if mmVals, ok := sblock.pile["tls.automation_manager"]; ok {
|
||||
for _, mmVal := range mmVals {
|
||||
mm := mmVal.Value.(caddytls.ManagerMaker)
|
||||
sblockHosts, err := st.autoHTTPSHosts(sblock)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
if len(sblockHosts) > 0 {
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
|
||||
Hosts: sblockHosts,
|
||||
ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID.Name(), &warnings),
|
||||
})
|
||||
} else {
|
||||
warnings = append(warnings, caddyconfig.Warning{
|
||||
Message: fmt.Sprintf("Server block %d %v has no names that qualify for automatic HTTPS, so no TLS automation policy will be added.", i, sblock.block.Keys),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tls certificate loaders
|
||||
if clVals, ok := sblock.pile["tls.certificate_loader"]; ok {
|
||||
for _, clVal := range clVals {
|
||||
loader := clVal.Value.(caddytls.CertificateLoader)
|
||||
loaderName := caddy.GetModuleName(loader)
|
||||
tlsApp.CertificatesRaw[loaderName] = caddyconfig.JSON(loader, &warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if global ACME CA or email were set, append a catch-all automation
|
||||
// policy that ensures they will be used if no tls directive was used
|
||||
acmeCA, hasACMECA := options["acme_ca"]
|
||||
email, hasEmail := options["email"]
|
||||
if hasACMECA || hasEmail {
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
if !hasACMECA {
|
||||
acmeCA = ""
|
||||
}
|
||||
if !hasEmail {
|
||||
email = ""
|
||||
}
|
||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
|
||||
ManagementRaw: caddyconfig.JSONModuleObject(caddytls.ACMEManagerMaker{
|
||||
CA: acmeCA.(string),
|
||||
Email: email.(string),
|
||||
}, "module", "acme", &warnings),
|
||||
})
|
||||
}
|
||||
if tlsApp.Automation != nil {
|
||||
// consolidate automation policies that are the exact same
|
||||
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
|
||||
}
|
||||
|
||||
// if experimental HTTP/3 is enabled, enable it on each server
|
||||
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
|
||||
for _, srv := range httpApp.Servers {
|
||||
srv.ExperimentalHTTP3 = true
|
||||
}
|
||||
}
|
||||
|
||||
// annnd the top-level config, then we're done!
|
||||
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
|
||||
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
|
||||
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
|
||||
}
|
||||
if !reflect.DeepEqual(tlsApp, caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
|
||||
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
|
||||
}
|
||||
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
|
||||
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
|
||||
"module",
|
||||
storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
|
||||
&warnings)
|
||||
}
|
||||
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
|
||||
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
|
||||
}
|
||||
|
||||
return cfg, warnings, nil
|
||||
}
|
||||
|
||||
// hostsFromServerBlockKeys returns a list of all the
|
||||
// hostnames found in the keys of the server block sb.
|
||||
// The list may not be in a consistent order.
|
||||
func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]string, error) {
|
||||
// first get each unique hostname
|
||||
hostMap := make(map[string]struct{})
|
||||
for _, sblockKey := range sb.Keys {
|
||||
addr, err := ParseAddress(sblockKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing server block key: %v", err)
|
||||
}
|
||||
addr = addr.Normalize()
|
||||
if addr.Host == "" {
|
||||
continue
|
||||
}
|
||||
hostMap[addr.Host] = struct{}{}
|
||||
}
|
||||
|
||||
// convert map to slice
|
||||
sblockHosts := make([]string, 0, len(hostMap))
|
||||
for host := range hostMap {
|
||||
sblockHosts = append(sblockHosts, host)
|
||||
}
|
||||
|
||||
return sblockHosts, nil
|
||||
}
|
||||
|
||||
// serversFromPairings creates the servers for each pairing of addresses
|
||||
// to server blocks. Each pairing is essentially a server definition.
|
||||
func (st *ServerType) serversFromPairings(
|
||||
pairings []sbAddrAssociation,
|
||||
options map[string]interface{},
|
||||
warnings *[]caddyconfig.Warning,
|
||||
) (map[string]*caddyhttp.Server, error) {
|
||||
servers := make(map[string]*caddyhttp.Server)
|
||||
|
||||
for i, p := range pairings {
|
||||
srv := &caddyhttp.Server{
|
||||
Listen: p.addresses,
|
||||
}
|
||||
|
||||
for _, sblock := range p.serverBlocks {
|
||||
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err)
|
||||
}
|
||||
|
||||
// if there are user-defined variables, then siteVarSubroute will
|
||||
// wrap the handlerSubroute; otherwise handlerSubroute will be the
|
||||
// site's primary subroute.
|
||||
siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute)
|
||||
|
||||
// tls: connection policies and toggle auto HTTPS
|
||||
|
||||
autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := sblock.pile["tls.off"]; ok && len(autoHTTPSQualifiedHosts) > 0 {
|
||||
// tls off: disable TLS (and automatic HTTPS) for server block's names
|
||||
if srv.AutoHTTPS == nil {
|
||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
||||
}
|
||||
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...)
|
||||
} else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
||||
// tls connection policies
|
||||
for _, cpVal := range cpVals {
|
||||
cp := cpVal.Value.(*caddytls.ConnectionPolicy)
|
||||
|
||||
// make sure the policy covers all hostnames from the block
|
||||
hosts, err := st.hostsFromServerBlockKeys(sblock.block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: are matchers needed if every hostname of the config is matched?
|
||||
if len(hosts) > 0 {
|
||||
cp.MatchersRaw = caddy.ModuleMap{
|
||||
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
|
||||
}
|
||||
}
|
||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
||||
}
|
||||
// TODO: consolidate equal conn policies
|
||||
}
|
||||
|
||||
// vars: special routes that will have to wrap the normal handlers
|
||||
// so that these variables can be used across their matchers too
|
||||
for _, cfgVal := range sblock.pile["var"] {
|
||||
siteVarSubroute.Routes = append(siteVarSubroute.Routes, cfgVal.Value.(caddyhttp.Route))
|
||||
}
|
||||
|
||||
// set up each handler directive - the order of the handlers
|
||||
// as they are added to the routes depends on user preference
|
||||
dirRoutes := sblock.pile["route"]
|
||||
handlerOrder, ok := options["handler_order"].([]string)
|
||||
if !ok {
|
||||
handlerOrder = defaultDirectiveOrder
|
||||
}
|
||||
if len(handlerOrder) == 1 && handlerOrder[0] == "appearance" {
|
||||
handlerOrder = nil
|
||||
}
|
||||
if handlerOrder != nil {
|
||||
dirPositions := make(map[string]int)
|
||||
for i, dir := range handlerOrder {
|
||||
dirPositions[dir] = i
|
||||
}
|
||||
sort.SliceStable(dirRoutes, func(i, j int) bool {
|
||||
iDir, jDir := dirRoutes[i].directive, dirRoutes[j].directive
|
||||
return dirPositions[iDir] < dirPositions[jDir]
|
||||
})
|
||||
}
|
||||
for _, r := range dirRoutes {
|
||||
handlerSubroute.Routes = append(handlerSubroute.Routes, r.Value.(caddyhttp.Route))
|
||||
}
|
||||
|
||||
// the route that contains the site's handlers will
|
||||
// be assumed to be the sub-route for this site...
|
||||
siteSubroute := handlerSubroute
|
||||
|
||||
// ... unless, of course, there are variables that might
|
||||
// be used by the site's matchers or handlers, in which
|
||||
// case we need to nest the handlers in a sub-sub-route,
|
||||
// and the variables go in the sub-route so the variables
|
||||
// get evaluated first
|
||||
if len(siteVarSubroute.Routes) > 0 {
|
||||
subSubRoute := caddyhttp.Subroute{Routes: siteSubroute.Routes}
|
||||
siteSubroute.Routes = append(
|
||||
siteVarSubroute.Routes,
|
||||
caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes)
|
||||
|
||||
srv.Routes = append(srv.Routes, caddyhttp.Route{
|
||||
MatcherSetsRaw: matcherSetsEnc,
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
srv.Routes = consolidateRoutes(srv.Routes)
|
||||
|
||||
servers[fmt.Sprintf("srv%d", i)] = srv
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) {
|
||||
// get the hosts for this server block...
|
||||
hosts, err := st.hostsFromServerBlockKeys(sb.block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ...and of those, which ones qualify for auto HTTPS
|
||||
var autoHTTPSQualifiedHosts []string
|
||||
for _, h := range hosts {
|
||||
if certmagic.HostQualifies(h) {
|
||||
autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h)
|
||||
}
|
||||
}
|
||||
return autoHTTPSQualifiedHosts, nil
|
||||
}
|
||||
|
||||
// consolidateRoutes combines routes with the same properties
|
||||
// (same matchers, same Terminal and Group settings) for a
|
||||
// cleaner overall output.
|
||||
func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList {
|
||||
for i := 0; i < len(routes)-1; i++ {
|
||||
if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) &&
|
||||
routes[i].Terminal == routes[i+1].Terminal &&
|
||||
routes[i].Group == routes[i+1].Group {
|
||||
// keep the handlers in the same order, then splice out repetitive route
|
||||
routes[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...)
|
||||
routes = append(routes[:i+1], routes[i+2:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// 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 reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) {
|
||||
aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...)
|
||||
aps = append(aps[:j], aps[j+1:]...)
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return aps
|
||||
}
|
||||
|
||||
func matcherSetFromMatcherToken(
|
||||
tkn caddyfile.Token,
|
||||
matcherDefs map[string]caddy.ModuleMap,
|
||||
warnings *[]caddyconfig.Warning,
|
||||
) (caddy.ModuleMap, bool, error) {
|
||||
// matcher tokens can be wildcards, simple path matchers,
|
||||
// or refer to a pre-defined matcher by some name
|
||||
if tkn.Text == "*" {
|
||||
// match all requests == no matchers, so nothing to do
|
||||
return nil, true, nil
|
||||
} else if strings.HasPrefix(tkn.Text, "/") || strings.HasPrefix(tkn.Text, "=/") {
|
||||
// convenient way to specify a single path match
|
||||
return caddy.ModuleMap{
|
||||
"path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
|
||||
}, true, nil
|
||||
} else if strings.HasPrefix(tkn.Text, "match:") {
|
||||
// pre-defined matcher
|
||||
matcherName := strings.TrimPrefix(tkn.Text, "match:")
|
||||
m, ok := matcherDefs[matcherName]
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("unrecognized matcher name: %+v", matcherName)
|
||||
}
|
||||
return m, true, nil
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]caddy.ModuleMap, error) {
|
||||
type hostPathPair struct {
|
||||
hostm caddyhttp.MatchHost
|
||||
pathm caddyhttp.MatchPath
|
||||
}
|
||||
|
||||
// keep routes with common host and path matchers together
|
||||
var matcherPairs []*hostPathPair
|
||||
|
||||
for _, key := range sblock.Keys {
|
||||
addr, err := ParseAddress(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err)
|
||||
}
|
||||
addr = addr.Normalize()
|
||||
|
||||
// choose a matcher pair that should be shared by this
|
||||
// server block; if none exists yet, create one
|
||||
var chosenMatcherPair *hostPathPair
|
||||
for _, mp := range matcherPairs {
|
||||
if (len(mp.pathm) == 0 && addr.Path == "") ||
|
||||
(len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) {
|
||||
chosenMatcherPair = mp
|
||||
break
|
||||
}
|
||||
}
|
||||
if chosenMatcherPair == nil {
|
||||
chosenMatcherPair = new(hostPathPair)
|
||||
if addr.Path != "" {
|
||||
chosenMatcherPair.pathm = []string{addr.Path}
|
||||
}
|
||||
matcherPairs = append(matcherPairs, chosenMatcherPair)
|
||||
}
|
||||
|
||||
// add this server block's keys to the matcher
|
||||
// pair if it doesn't already exist
|
||||
if addr.Host != "" {
|
||||
var found bool
|
||||
for _, h := range chosenMatcherPair.hostm {
|
||||
if h == addr.Host {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate each pairing of host and path matchers and
|
||||
// put them into a map for JSON encoding
|
||||
var matcherSets []map[string]caddyhttp.RequestMatcher
|
||||
for _, mp := range matcherPairs {
|
||||
matcherSet := make(map[string]caddyhttp.RequestMatcher)
|
||||
if len(mp.hostm) > 0 {
|
||||
matcherSet["host"] = mp.hostm
|
||||
}
|
||||
if len(mp.pathm) > 0 {
|
||||
matcherSet["path"] = mp.pathm
|
||||
}
|
||||
if len(matcherSet) > 0 {
|
||||
matcherSets = append(matcherSets, matcherSet)
|
||||
}
|
||||
}
|
||||
|
||||
// finally, encode each of the matcher sets
|
||||
var matcherSetsEnc []caddy.ModuleMap
|
||||
for _, ms := range matcherSets {
|
||||
msEncoded, err := encodeMatcherSet(ms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server block %v: %v", sblock.Keys, err)
|
||||
}
|
||||
matcherSetsEnc = append(matcherSetsEnc, msEncoded)
|
||||
}
|
||||
|
||||
return matcherSetsEnc, nil
|
||||
}
|
||||
|
||||
func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]caddy.ModuleMap, error) {
|
||||
matchers := make(map[string]caddy.ModuleMap)
|
||||
for d.Next() {
|
||||
definitionName := d.Val()
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
matcherName := d.Val()
|
||||
mod, err := caddy.GetModule("http.matchers." + matcherName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rm, ok := unm.(caddyhttp.RequestMatcher)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
|
||||
}
|
||||
if _, ok := matchers[definitionName]; !ok {
|
||||
matchers[definitionName] = make(caddy.ModuleMap)
|
||||
}
|
||||
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
|
||||
}
|
||||
}
|
||||
return matchers, nil
|
||||
}
|
||||
|
||||
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
|
||||
msEncoded := make(caddy.ModuleMap)
|
||||
for matcherName, val := range matchers {
|
||||
jsonBytes, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err)
|
||||
}
|
||||
msEncoded[matcherName] = jsonBytes
|
||||
}
|
||||
return msEncoded, nil
|
||||
}
|
||||
|
||||
// tryInt tries to convert val to an integer. If it fails,
|
||||
// it downgrades the error to a warning and returns 0.
|
||||
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
|
||||
intVal, ok := val.(int)
|
||||
if val != nil && !ok && warnings != nil {
|
||||
*warnings = append(*warnings, caddyconfig.Warning{Message: "not an integer type"})
|
||||
}
|
||||
return intVal
|
||||
}
|
||||
|
||||
type matcherSetAndTokens struct {
|
||||
matcherSet caddy.ModuleMap
|
||||
tokens []caddyfile.Token
|
||||
}
|
||||
|
||||
// sbAddrAssocation is a mapping from a list of
|
||||
// addresses to a list of server blocks that are
|
||||
// served on those addresses.
|
||||
type sbAddrAssociation struct {
|
||||
addresses []string
|
||||
serverBlocks []serverBlock
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ caddyfile.ServerType = (*ServerType)(nil)
|
||||
@@ -1,148 +0,0 @@
|
||||
// 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"
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func parseOptHTTPPort(d *caddyfile.Dispenser) (int, 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) (int, 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 parseOptExperimentalHTTP3(d *caddyfile.Dispenser) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func parseOptHandlerOrder(d *caddyfile.Dispenser) ([]string, error) {
|
||||
if !d.Next() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
order := d.RemainingArgs()
|
||||
if len(order) == 1 && order[0] == "appearance" {
|
||||
return []string{"appearance"}, nil
|
||||
}
|
||||
if len(order) > 0 && d.NextBlock(0) {
|
||||
return nil, d.Err("cannot open block if there are arguments")
|
||||
}
|
||||
for d.NextBlock(0) {
|
||||
order = append(order, d.Val())
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
}
|
||||
if len(order) == 0 {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
|
||||
if !d.Next() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
args := d.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
modName := args[0]
|
||||
mod, err := caddy.GetModule("caddy.storage." + modName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting storage module '%s': %v", modName, err)
|
||||
}
|
||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
||||
}
|
||||
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storage, ok := unm.(caddy.StorageConverter)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("module %s is not a StorageConverter", mod.ID)
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func parseOptACMECA(d *caddyfile.Dispenser) (string, 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 parseOptEmail(d *caddyfile.Dispenser) (string, 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) (string, error) {
|
||||
if d.Next() {
|
||||
var listenAddress string
|
||||
d.AllArgs(&listenAddress)
|
||||
|
||||
if listenAddress == "" {
|
||||
listenAddress = caddy.DefaultAdminListen
|
||||
}
|
||||
|
||||
return listenAddress, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
caddyfile "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expectWarn bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
input: `http://localhost
|
||||
matcher debug {
|
||||
query showdebug=1
|
||||
}
|
||||
`,
|
||||
expectWarn: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `http://localhost
|
||||
matcher debug {
|
||||
query bad format
|
||||
}
|
||||
`,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// 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 json5adapter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/ilibs/json5"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddyconfig.RegisterAdapter("json5", Adapter{})
|
||||
}
|
||||
|
||||
// Adapter adapts JSON5 to Caddy JSON.
|
||||
type Adapter struct{}
|
||||
|
||||
// Adapt converts the JSON5 config in body to Caddy JSON.
|
||||
func (a Adapter) Adapt(body []byte, options map[string]interface{}) (result []byte, warnings []caddyconfig.Warning, err error) {
|
||||
var decoded interface{}
|
||||
err = json5.Unmarshal(body, &decoded)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result, err = json.Marshal(decoded)
|
||||
return
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ caddyconfig.Adapter = (*Adapter)(nil)
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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 jsoncadapter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/muhammadmuzzammil1998/jsonc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddyconfig.RegisterAdapter("jsonc", Adapter{})
|
||||
}
|
||||
|
||||
// Adapter adapts JSON-C to Caddy JSON.
|
||||
type Adapter struct{}
|
||||
|
||||
// Adapt converts the JSON-C config in body to Caddy JSON.
|
||||
func (a Adapter) Adapt(body []byte, options map[string]interface{}) (result []byte, warnings []caddyconfig.Warning, err error) {
|
||||
result = jsonc.ToJSON(body)
|
||||
|
||||
// any errors in the JSON will be
|
||||
// reported during config load, but
|
||||
// we can at least warn here that
|
||||
// it is not valid JSON
|
||||
if !json.Valid(result) {
|
||||
warnings = append(warnings, caddyconfig.Warning{
|
||||
Message: "Resulting JSON is invalid.",
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ caddyconfig.Adapter = (*Adapter)(nil)
|
||||
@@ -0,0 +1,246 @@
|
||||
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 error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Parse error: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
}
|
||||
Executable → Regular
+8
-32
@@ -1,22 +1,6 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -27,7 +11,7 @@ func TestDispenser_Val_Next(t *testing.T) {
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
if val := d.Val(); val != "" {
|
||||
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
|
||||
@@ -65,7 +49,7 @@ func TestDispenser_NextArg(t *testing.T) {
|
||||
input := `dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.Next() != shouldLoad {
|
||||
@@ -112,7 +96,7 @@ func TestDispenser_NextLine(t *testing.T) {
|
||||
input := `host:port
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.NextLine() != shouldLoad {
|
||||
@@ -145,10 +129,10 @@ func TestDispenser_NextBlock(t *testing.T) {
|
||||
}
|
||||
foobar2 {
|
||||
}`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
|
||||
if loaded := d.NextBlock(0); loaded != shouldLoad {
|
||||
if loaded := d.NextBlock(); loaded != shouldLoad {
|
||||
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
@@ -175,7 +159,7 @@ func TestDispenser_Args(t *testing.T) {
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 arg7
|
||||
dir4`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
@@ -242,7 +226,7 @@ func TestDispenser_RemainingArgs(t *testing.T) {
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 { arg7
|
||||
dir4`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
@@ -279,7 +263,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
input := `dir1 {
|
||||
}
|
||||
dir2 arg1 arg2`
|
||||
d := newTestDispenser(input)
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
d.cursor = 1 // {
|
||||
|
||||
@@ -306,11 +290,3 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestDispenser(input string) *Dispenser {
|
||||
tokens, err := allTokens("Testfile", strings.NewReader(input))
|
||||
if err != nil && err != io.EOF {
|
||||
log.Fatalf("getting all tokens from input: %v", err)
|
||||
}
|
||||
return NewDispenser(tokens)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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"`
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
Executable → Regular
+19
-46
@@ -1,17 +1,3 @@
|
||||
// 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 (
|
||||
@@ -26,10 +12,9 @@ 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
|
||||
skippedLines int
|
||||
reader *bufio.Reader
|
||||
token Token
|
||||
line int
|
||||
}
|
||||
|
||||
// Token represents a single parsable unit.
|
||||
@@ -92,29 +77,27 @@ func (l *lexer) next() bool {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !escaped && ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if escaped {
|
||||
// all is literal in quoted area,
|
||||
// so only escape quotes
|
||||
if ch != '"' {
|
||||
val = append(val, '\\')
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
if ch == '"' {
|
||||
if !escaped {
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
} else if ch == '"' {
|
||||
quoted = false
|
||||
return makeToken()
|
||||
}
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line += 1 + l.skippedLines
|
||||
l.skippedLines = 0
|
||||
l.line++
|
||||
}
|
||||
if escaped {
|
||||
// only escape quotes
|
||||
if ch != '"' {
|
||||
val = append(val, '\\')
|
||||
}
|
||||
}
|
||||
val = append(val, ch)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -123,13 +106,7 @@ func (l *lexer) next() bool {
|
||||
continue
|
||||
}
|
||||
if ch == '\n' {
|
||||
if escaped {
|
||||
l.skippedLines++
|
||||
escaped = false
|
||||
} else {
|
||||
l.line += 1 + l.skippedLines
|
||||
l.skippedLines = 0
|
||||
}
|
||||
l.line++
|
||||
comment = false
|
||||
}
|
||||
if len(val) > 0 {
|
||||
@@ -141,6 +118,7 @@ func (l *lexer) next() bool {
|
||||
if ch == '#' {
|
||||
comment = true
|
||||
}
|
||||
|
||||
if comment {
|
||||
continue
|
||||
}
|
||||
@@ -153,11 +131,6 @@ func (l *lexer) next() bool {
|
||||
}
|
||||
}
|
||||
|
||||
if escaped {
|
||||
val = append(val, '\\')
|
||||
escaped = false
|
||||
}
|
||||
|
||||
val = append(val, ch)
|
||||
}
|
||||
}
|
||||
Executable → Regular
+1
-68
@@ -1,21 +1,6 @@
|
||||
// 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 (
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -95,50 +80,6 @@ func TestLexer(t *testing.T) {
|
||||
{Line: 1, Text: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "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: "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: "line1\\\nescaped\nline2\nline3",
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "line1"},
|
||||
{Line: 1, Text: "escaped"},
|
||||
{Line: 3, Text: "line2"},
|
||||
{Line: 4, Text: "line3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "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: `"unescapable\ in quotes"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `unescapable\ in quotes`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
expected: []Token{
|
||||
@@ -151,12 +92,6 @@ func TestLexer(t *testing.T) {
|
||||
{Line: 1, Text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `un\escapable`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `un\escapable`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
break inside" {
|
||||
@@ -209,9 +144,7 @@ func TestLexer(t *testing.T) {
|
||||
|
||||
func tokenize(input string) (tokens []Token) {
|
||||
l := lexer{}
|
||||
if err := l.load(strings.NewReader(input)); err != nil {
|
||||
log.Printf("[ERROR] load failed: %v", err)
|
||||
}
|
||||
l.load(strings.NewReader(input))
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
Executable → Regular
+102
-202
@@ -1,17 +1,3 @@
|
||||
// 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 (
|
||||
@@ -28,19 +14,15 @@ 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) ([]ServerBlock, error) {
|
||||
tokens, err := allTokens(filename, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := parser{Dispenser: NewDispenser(tokens)}
|
||||
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
|
||||
return p.parseAll()
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(filename string, input io.Reader) ([]Token, error) {
|
||||
func allTokens(input io.Reader) ([]Token, error) {
|
||||
l := new(lexer)
|
||||
err := l.load(input)
|
||||
if err != nil {
|
||||
@@ -48,18 +30,16 @@ func allTokens(filename string, input io.Reader) ([]Token, error) {
|
||||
}
|
||||
var tokens []Token
|
||||
for l.next() {
|
||||
l.token.File = filename
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
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) {
|
||||
@@ -70,19 +50,17 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Keys) > 0 || len(p.block.Segments) > 0 {
|
||||
if len(p.block.Keys) > 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{}
|
||||
p.block = ServerBlock{Tokens: make(map[string][]Token)}
|
||||
|
||||
return p.begin()
|
||||
}
|
||||
|
||||
@@ -103,24 +81,6 @@ func (p *parser) begin() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, name := p.isSnippet(); ok {
|
||||
if p.definedSnippets == nil {
|
||||
p.definedSnippets = map[string][]Token{}
|
||||
}
|
||||
if _, found := p.definedSnippets[name]; found {
|
||||
return p.Errf("redeclaration of previously declared snippet %s", name)
|
||||
}
|
||||
// consume all tokens til matched close brace
|
||||
tokens, err := p.snippetTokens()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.definedSnippets[name] = tokens
|
||||
// empty block keys so we don't save this block as a real server.
|
||||
p.block.Keys = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.blockContents()
|
||||
}
|
||||
|
||||
@@ -189,7 +149,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 {
|
||||
@@ -208,7 +168,6 @@ func (p *parser) directives() error {
|
||||
for p.Next() {
|
||||
// end of server block
|
||||
if p.Val() == "}" {
|
||||
// p.nesting has already been decremented
|
||||
break
|
||||
}
|
||||
|
||||
@@ -222,15 +181,11 @@ func (p *parser) directives() error {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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)
|
||||
// normal case: parse a directive on this line
|
||||
if err := p.directive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -252,57 +207,69 @@ func (p *parser) doImport() error {
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
}
|
||||
|
||||
// make path relative to Caddyfile rather than current working directory (issue #867)
|
||||
// and then use glob to get list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.filename)
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s", p.Dispenser.filename)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
matches, err = filepath.Glob(globPattern)
|
||||
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.Contains(globPattern, "*") {
|
||||
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
|
||||
// collect all the imported tokens
|
||||
var importedTokens []Token
|
||||
|
||||
// first check snippets. That is a simple, non-recursive replacement
|
||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||
importedTokens = p.definedSnippets[importPattern]
|
||||
} else {
|
||||
// make path relative to the file of the _token_ being processed rather
|
||||
// than current working directory (issue #867) and then use glob to get
|
||||
// list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.File())
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
|
||||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
|
||||
// See issue #2096 - a pattern with many glob expansions can hang for too long
|
||||
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
||||
}
|
||||
matches, err = filepath.Glob(globPattern)
|
||||
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.ContainsAny(globPattern, "*?[]") {
|
||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
var importLine int
|
||||
importDir := filepath.Dir(importFile)
|
||||
for i, token := range newTokens {
|
||||
if token.Text == "import" {
|
||||
importLine = token.Line
|
||||
continue
|
||||
}
|
||||
if token.Line == importLine {
|
||||
var abs string
|
||||
if filepath.IsAbs(token.Text) {
|
||||
abs = token.Text
|
||||
} else if !filepath.IsAbs(importFile) {
|
||||
abs = filepath.Join(filepath.Dir(absFile), token.Text)
|
||||
} else {
|
||||
abs = filepath.Join(importDir, token.Text)
|
||||
}
|
||||
newTokens[i] = Token{
|
||||
Text: abs,
|
||||
Line: token.Line,
|
||||
File: token.File,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect all the imported tokens
|
||||
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
@@ -328,17 +295,13 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
||||
}
|
||||
|
||||
importedTokens, err := allTokens(importFile, file)
|
||||
importedTokens, err := allTokens(file)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
||||
}
|
||||
|
||||
// Tack the file path onto these tokens so errors show the imported file's name
|
||||
// (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", importFile, err)
|
||||
}
|
||||
// Tack the filename onto these tokens so errors show the imported file's name
|
||||
filename := filepath.Base(importFile)
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].File = filename
|
||||
}
|
||||
@@ -353,42 +316,35 @@ 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 {
|
||||
// evaluate any env vars in directive token
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
dir := p.Val()
|
||||
nesting := 0
|
||||
|
||||
// a segment is a list of tokens associated with this directive
|
||||
var segment Segment
|
||||
// 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)
|
||||
}
|
||||
|
||||
// the directive itself is appended as a relevant token
|
||||
segment = append(segment, p.Token())
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
p.nesting++
|
||||
} else if p.isNewLine() && p.nesting == 0 {
|
||||
nesting++
|
||||
} else if p.isNewLine() && nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
} else if p.Val() == "}" && p.nesting > 0 {
|
||||
p.nesting--
|
||||
} else if p.Val() == "}" && p.nesting == 0 {
|
||||
} else if p.Val() == "}" && nesting > 0 {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
} else if p.Val() == "import" && p.isNewLine() {
|
||||
if err := p.doImport(); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
segment = append(segment, p.Token())
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
}
|
||||
|
||||
p.block.Segments = append(p.block.Segments, segment)
|
||||
|
||||
if p.nesting > 0 {
|
||||
if nesting > 0 {
|
||||
return p.EOFErr()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -414,6 +370,19 @@ 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 {
|
||||
@@ -427,13 +396,8 @@ func replaceEnvVars(s string) string {
|
||||
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) {
|
||||
endIndex := strings.Index(s, refEnd)
|
||||
if endIndex != -1 {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
@@ -444,73 +408,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *parser) isSnippet() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
|
||||
return true, strings.TrimSuffix(keys[0][1:], ")")
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// read and store everything in a block for later replay.
|
||||
func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// snippet must have curlies.
|
||||
err := p.openCurlyBrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nesting := 1 // count our own nesting in snippets
|
||||
tokens := []Token{}
|
||||
for p.Next() {
|
||||
if p.Val() == "}" {
|
||||
nesting--
|
||||
if nesting == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.Val() == "{" {
|
||||
nesting++
|
||||
}
|
||||
tokens = append(tokens, p.tokens[p.cursor])
|
||||
}
|
||||
// make sure we're matched up
|
||||
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.
|
||||
// ServerBlock associates any number of keys (usually addresses
|
||||
// of some sort) with tokens (grouped by directive name).
|
||||
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 ""
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
||||
Executable → Regular
+84
-261
@@ -1,17 +1,3 @@
|
||||
// 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 (
|
||||
@@ -25,7 +11,7 @@ import (
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens, err := allTokens("TestAllTokens", input)
|
||||
tokens, err := allTokens(input)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
@@ -53,67 +39,84 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys []string
|
||||
numTokens []int // number of tokens to expect in each segment
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []string{
|
||||
"localhost",
|
||||
}, []int{}},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []string{
|
||||
"localhost",
|
||||
}, []int{1}},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []string{
|
||||
"localhost:1234",
|
||||
}, []int{3},
|
||||
},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, []int{1}},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []string{
|
||||
"localhost:1234",
|
||||
}, []int{3, 1}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, []int{3}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, []int{3}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, []int{3}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []string{
|
||||
"http://localhost",
|
||||
}, []int{}},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []string{
|
||||
"host1:80",
|
||||
"http://host2.com",
|
||||
}, []int{3, 2}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
@@ -121,7 +124,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
"http://host1.com",
|
||||
"http://host2.com",
|
||||
"https://host3.com",
|
||||
}, []int{}},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
@@ -130,7 +133,10 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
dir2`, false, []string{
|
||||
"http://host1.com:1234",
|
||||
"https://host2.com",
|
||||
}, []int{6, 1}},
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
@@ -140,25 +146,34 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
foo bar
|
||||
}`, false, []string{
|
||||
"127.0.0.1",
|
||||
}, []int{5, 5}},
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []string{
|
||||
"localhost",
|
||||
}, []int{3}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, []int{3}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []string{
|
||||
"localhost",
|
||||
}, []int{}},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
@@ -168,38 +183,37 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
}
|
||||
dir2 foo bar`, false, []string{
|
||||
"localhost",
|
||||
}, []int{7, 3}},
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []string{}, []int{}},
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import testdata/import_test1.txt`, false, []string{
|
||||
"localhost",
|
||||
}, []int{2, 3, 1}},
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test2.txt`, false, []string{
|
||||
"host1",
|
||||
}, []int{1, 2}},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, []int{}},
|
||||
{`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{}},
|
||||
|
||||
// test cases found by fuzzing!
|
||||
{`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{}},
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
@@ -222,16 +236,15 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Segments) != len(test.numTokens) {
|
||||
t.Errorf("Test %d: Expected %d segments, had %d",
|
||||
i, len(test.numTokens), len(result.Segments))
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
|
||||
for 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))
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -251,12 +264,12 @@ func TestRecursiveImport(t *testing.T) {
|
||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
||||
return false
|
||||
}
|
||||
if len(got.Segments) != 2 {
|
||||
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
|
||||
if len(got.Tokens) != 2 {
|
||||
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
|
||||
return false
|
||||
}
|
||||
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Segments)
|
||||
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["dir2"]) != 2 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Tokens)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -333,68 +346,6 @@ func TestRecursiveImport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectiveImport(t *testing.T) {
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
isExpected := func(got ServerBlock) bool {
|
||||
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
|
||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
||||
return false
|
||||
}
|
||||
if len(got.Segments) != 2 {
|
||||
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
|
||||
return false
|
||||
}
|
||||
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Segments)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
directiveFile, err := filepath.Abs("testdata/directive_import_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
|
||||
prop2 2`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(directiveFile)
|
||||
|
||||
// import from existing file
|
||||
result, err := testParseOne(`localhost
|
||||
dir1
|
||||
proxy {
|
||||
import testdata/directive_import_test
|
||||
transparent
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !isExpected(result) {
|
||||
t.Error("directive import failed")
|
||||
}
|
||||
|
||||
// import from nonexistent file
|
||||
_, err = testParseOne(`localhost
|
||||
dir1
|
||||
proxy {
|
||||
import testdata/nonexistent_file
|
||||
transparent
|
||||
}`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when importing a nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
@@ -476,7 +427,6 @@ 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}`)
|
||||
@@ -485,13 +435,6 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, 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()
|
||||
@@ -519,21 +462,21 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
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].Segments[0][1].Text, "foobar"; 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].Segments[0][1].Text, "servername.com/foobar"; expected != actual {
|
||||
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].Segments[0][1].Text, "{%ADDRESS}"; expected != actual {
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
@@ -547,133 +490,13 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Segments[0][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].Segments[0][1].Text, "{{ .Name }} foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
errors stderr
|
||||
}
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Segments)
|
||||
}
|
||||
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].Segments) != 2 {
|
||||
t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0])
|
||||
}
|
||||
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].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) {
|
||||
file, err := ioutil.TempFile("", t.Name())
|
||||
if err != nil {
|
||||
panic(err) // get a stack trace so we know where this was called from.
|
||||
}
|
||||
if _, err := file.WriteString(str); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
|
||||
fileName := writeStringToTempFileOrDie(t, `
|
||||
http://example.com {
|
||||
# This isn't an import directive, it's just an arg with value 'import'
|
||||
basicauth / import password
|
||||
}
|
||||
`)
|
||||
// Parse the root file that imports the other one.
|
||||
p := testParser(`import ` + fileName)
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Segments)
|
||||
}
|
||||
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:
|
||||
// basicauth / import /path/to/test/dir/password
|
||||
// referencing a file that (probably) doesn't exist and changing the
|
||||
// password!
|
||||
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippetAcrossMultipleFiles(t *testing.T) {
|
||||
// Make the derived Caddyfile that expects (common) to be defined.
|
||||
fileName := writeStringToTempFileOrDie(t, `
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
|
||||
// Parse the root file that defines (common) and then imports the other one.
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
}
|
||||
import ` + fileName + `
|
||||
`)
|
||||
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Segments)
|
||||
}
|
||||
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].Segments) != 1 {
|
||||
t.Fatalf("Server block should have tokens from import")
|
||||
}
|
||||
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
return parser{Dispenser: newTestDispenser(input)}
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
|
||||
return p
|
||||
}
|
||||
Vendored
Executable → Regular
Vendored
Executable → Regular
Vendored
Executable → Regular
Vendored
Executable → Regular
Vendored
Executable → Regular
@@ -0,0 +1,171 @@
|
||||
// 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"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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+"\"")
|
||||
return http.StatusUnauthorized, nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
passwHash.Write([]byte(passw))
|
||||
passwSum := passwHash.Sum(nil)
|
||||
return func(pw string) bool {
|
||||
pwHash := sha1.New()
|
||||
pwHash.Write([]byte(pw))
|
||||
pwSum := pwHash.Sum(nil)
|
||||
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/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
|
||||
}
|
||||
|
||||
tests := []testType{
|
||||
{"/testing", http.StatusOK, "okuser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "baduser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "badpass"},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS"},
|
||||
{"/testing", http.StatusUnauthorized, "", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", ""},
|
||||
{"/testing", http.StatusUnauthorized, "", ""},
|
||||
}
|
||||
|
||||
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 {
|
||||
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/mholt/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
|
||||
}{
|
||||
{"/t", http.StatusOK, "t:p1"},
|
||||
{"/t/t", http.StatusOK, "t:p1"},
|
||||
{"/t/t", http.StatusOK, "t1:p2"},
|
||||
{"/a", http.StatusOK, "t1:p2"},
|
||||
{"/t/t", http.StatusUnauthorized, "t1:p3"},
|
||||
{"/t", http.StatusUnauthorized, "t1:p2"},
|
||||
}
|
||||
|
||||
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 {
|
||||
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.Skipf("Error creating temp file (%v), 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
return PlainMatcher(passw), nil
|
||||
}
|
||||
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package bind
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("bind", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setupBind,
|
||||
})
|
||||
}
|
||||
|
||||
func setupBind(c *caddy.Controller) error {
|
||||
config := httpserver.GetConfig(c)
|
||||
for c.Next() {
|
||||
if !c.Args(&config.ListenHost) {
|
||||
return c.ArgErr()
|
||||
}
|
||||
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package bind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
// 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/dustin/go-humanize"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
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 browsable
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
// 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 choses 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 staticfiles.IndexPages {
|
||||
if name == indexName {
|
||||
hasIndexFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
name += "/"
|
||||
dirCount++
|
||||
} else {
|
||||
fileCount++
|
||||
}
|
||||
|
||||
if config.Fs.IsHidden(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
|
||||
fileinfos = append(fileinfos, FileInfo{
|
||||
IsDir: f.IsDir(),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: url.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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
staticfiles.RedirectToDir(w, r)
|
||||
return 0, 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
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
package browse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
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,
|
||||
0,
|
||||
"http://www.example.com/photos/",
|
||||
},
|
||||
{
|
||||
"/photos",
|
||||
http.StatusMovedPermanently,
|
||||
0,
|
||||
"/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)
|
||||
u, _ := url.Parse(tc.url)
|
||||
ctx := context.WithValue(req.Context(), staticfiles.URLPathCtxKey, u.Path)
|
||||
req = req.WithContext(ctx)
|
||||
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 != 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,104 @@
|
||||
// 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 browse
|
||||
|
||||
package fileserver
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
const defaultBrowseTemplate = `<!DOCTYPE html>
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("browse", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new Browse middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := browseParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := Browse{
|
||||
Configs: configs,
|
||||
IgnoreIndexes: false,
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
b.Next = next
|
||||
return b
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func browseParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
appendCfg := func(bc Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == bc.PathScope {
|
||||
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
|
||||
}
|
||||
}
|
||||
configs = append(configs, bc)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var bc Config
|
||||
|
||||
// First argument is directory to allow browsing; default is site root
|
||||
if c.NextArg() {
|
||||
bc.PathScope = c.Val()
|
||||
} else {
|
||||
bc.PathScope = "/"
|
||||
}
|
||||
|
||||
bc.Fs = staticfiles.FileServer{
|
||||
Root: http.Dir(cfg.Root),
|
||||
Hide: httpserver.GetConfig(c).HiddenFiles,
|
||||
}
|
||||
|
||||
// Second argument would be the template file to use
|
||||
var tplText string
|
||||
if c.NextArg() {
|
||||
tplBytes, err := ioutil.ReadFile(c.Val())
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
} else {
|
||||
tplText = defaultTemplate
|
||||
}
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
bc.Template = tpl
|
||||
|
||||
// Save configuration
|
||||
err = appendCfg(bc)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// The default template to use when serving up directory listings
|
||||
const defaultTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{html .Name}}</title>
|
||||
@@ -26,7 +110,6 @@ const defaultBrowseTemplate = `<!DOCTYPE html>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -47,12 +130,12 @@ header,
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
width: 5%;
|
||||
padding-left: 5%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
width: 5%;
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -67,22 +150,16 @@ h1 {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: #000;
|
||||
margin: 0 4px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1 a:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
@@ -139,25 +216,19 @@ th svg {
|
||||
}
|
||||
|
||||
td {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
width: 80%;
|
||||
td:first-child {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
td:nth-child(3),
|
||||
th:nth-child(3) {
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:nth-child(2) svg {
|
||||
td:first-child svg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -204,60 +275,61 @@ footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
td:first-child {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
th:nth-child(3),
|
||||
td:nth-child(3) {
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
padding-right: 5%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#filter {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload='filter()'>
|
||||
<body>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
|
||||
<defs>
|
||||
<!-- Folder -->
|
||||
<g id="folder" fill-rule="nonzero" fill="none">
|
||||
<path d="M285.22 37.55h-142.6L110.9 0H31.7C14.25 0 0 16.9 0 37.55v75.1h316.92V75.1c0-20.65-14.26-37.55-31.7-37.55z" fill="#FFA000"/>
|
||||
<path d="M285.22 36H31.7C14.25 36 0 50.28 0 67.74v158.7c0 17.47 14.26 31.75 31.7 31.75H285.2c17.44 0 31.7-14.3 31.7-31.75V67.75c0-17.47-14.26-31.75-31.7-31.75z" fill="#FFCA28"/>
|
||||
</g>
|
||||
<g id="folder-shortcut" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="folder-shortcut-group" fill-rule="nonzero">
|
||||
<g id="folder-shortcut-shape">
|
||||
<path d="M285.224876,37.5486902 L142.612438,37.5486902 L110.920785,0 L31.6916529,0 C14.2612438,0 0,16.8969106 0,37.5486902 L0,112.646071 L316.916529,112.646071 L316.916529,75.0973805 C316.916529,54.4456008 302.655285,37.5486902 285.224876,37.5486902 Z" id="Shape" fill="#FFA000"></path>
|
||||
<path d="M285.224876,36 L31.6916529,36 C14.2612438,36 0,50.2838568 0,67.7419039 L0,226.451424 C0,243.909471 14.2612438,258.193328 31.6916529,258.193328 L285.224876,258.193328 C302.655285,258.193328 316.916529,243.909471 316.916529,226.451424 L316.916529,67.7419039 C316.916529,50.2838568 302.655285,36 285.224876,36 Z" id="Shape" fill="#FFCA28"></path>
|
||||
<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84">
|
||||
<stop stop-color="#b3ddfd" offset="0"/>
|
||||
<stop stop-color="#69c" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26">
|
||||
<stop stop-color="#ace" offset="0"/>
|
||||
<stop stop-color="#369" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="folder" transform="translate(-266.06 -193.36)">
|
||||
<g transform="matrix(.066019 0 0 .066019 264.2 170.93)">
|
||||
<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)">
|
||||
<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/>
|
||||
<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/>
|
||||
</g>
|
||||
<path d="M126.154134,250.559184 C126.850974,251.883673 127.300549,253.006122 127.772602,254.106122 C128.469442,255.206122 128.919016,256.104082 129.638335,257.002041 C130.559962,258.326531 131.728855,259 133.100057,259 C134.493737,259 135.415364,258.55102 136.112204,257.67551 C136.809044,257.002041 137.258619,255.902041 137.258619,254.577551 C137.258619,253.904082 137.258619,252.804082 137.033832,251.457143 C136.786566,249.908163 136.561779,249.032653 136.561779,248.583673 C136.089726,242.814286 135.864939,237.920408 135.864939,233.273469 C135.864939,225.057143 136.786566,217.514286 138.180246,210.846939 C139.798713,204.202041 141.889234,198.634694 144.429328,193.763265 C147.216689,188.869388 150.678411,184.873469 154.836973,181.326531 C158.995535,177.779592 163.626149,174.883673 168.481552,172.661224 C173.336954,170.438776 179.113983,168.665306 185.587852,167.340816 C192.061722,166.218367 198.760378,165.342857 205.481514,164.669388 C212.18017,164.220408 219.598146,163.995918 228.162535,163.995918 L246.055591,163.995918 L246.055591,195.514286 C246.055591,197.736735 246.752431,199.510204 248.370899,201.059184 C250.214153,202.608163 252.079886,203.506122 254.372715,203.506122 C256.463236,203.506122 258.531277,202.608163 260.172223,201.059184 L326.102289,137.797959 C327.720757,136.24898 328.642384,134.47551 328.642384,132.253061 C328.642384,130.030612 327.720757,128.257143 326.102289,126.708163 L260.172223,63.4469388 C258.553756,61.8979592 256.463236,61 254.395194,61 C252.079886,61 250.236632,61.8979592 248.393377,63.4469388 C246.77491,64.9959184 246.07807,66.7693878 246.07807,68.9918367 L246.07807,100.510204 L228.162535,100.510204 C166.863084,100.510204 129.166282,117.167347 115.274437,150.459184 C110.666301,161.54898 108.350993,175.310204 108.350993,191.742857 C108.350993,205.279592 113.903236,223.912245 124.760454,247.438776 C125.00772,248.112245 125.457294,249.010204 126.154134,250.559184 Z" id="Shape" fill="#FFFFFF" transform="translate(218.496689, 160.000000) scale(-1, 1) translate(-218.496689, -160.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- File -->
|
||||
<g id="file" stroke="#000" stroke-width="25" fill="#FFF" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 24.12v274.76c0 6.16 5.87 11.12 13.17 11.12H239c7.3 0 13.17-4.96 13.17-11.12V136.15S132.6 13 128.37 13H26.17C18.87 13 13 17.96 13 24.12z"/>
|
||||
<path d="M129.37 13L129 113.9c0 10.58 7.26 19.1 16.27 19.1H249L129.37 13z"/>
|
||||
</g>
|
||||
<g id="file-shortcut" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="file-shortcut-group" transform="translate(13.000000, 13.000000)">
|
||||
<g id="file-shortcut-shape" stroke="#000000" stroke-width="25" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M0,11.1214886 L0,285.878477 C0,292.039924 5.87498876,296.999983 13.1728373,296.999983 L225.997983,296.999983 C233.295974,296.999983 239.17082,292.039942 239.17082,285.878477 L239.17082,123.145388 C239.17082,123.145388 119.58541,2.84217094e-14 115.369423,2.84217094e-14 L13.1728576,2.84217094e-14 C5.87500907,-1.71479982e-05 0,4.96022995 0,11.1214886 Z" id="rect1171"></path>
|
||||
<path d="M116.37005,0 L116,100.904964 C116,111.483663 123.258008,120 132.273377,120 L236,120 L116.37005,0 L116.37005,0 Z" id="rect1794"></path>
|
||||
<linearGradient id="a">
|
||||
<stop stop-color="#cbcbcb" offset="0"/>
|
||||
<stop stop-color="#f0f0f0" offset=".34923"/>
|
||||
<stop stop-color="#e2e2e2" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/>
|
||||
<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/>
|
||||
<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96">
|
||||
<stop stop-color="#fff" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="file" transform="translate(-278.15 -216.59)">
|
||||
<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)">
|
||||
<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/>
|
||||
<g>
|
||||
<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/>
|
||||
<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/>
|
||||
<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/>
|
||||
</g>
|
||||
<path d="M47.803141,294.093878 C48.4999811,295.177551 48.9495553,296.095918 49.4216083,296.995918 C50.1184484,297.895918 50.5680227,298.630612 51.2873415,299.365306 C52.2089688,300.44898 53.3778619,301 54.7490634,301 C56.1427436,301 57.0643709,300.632653 57.761211,299.916327 C58.4580511,299.365306 58.9076254,298.465306 58.9076254,297.381633 C58.9076254,296.830612 58.9076254,295.930612 58.6828382,294.828571 C58.4355724,293.561224 58.2107852,292.844898 58.2107852,292.477551 C57.7387323,287.757143 57.5139451,283.753061 57.5139451,279.95102 C57.5139451,273.228571 58.4355724,267.057143 59.8292526,261.602041 C61.44772,256.165306 63.5382403,251.610204 66.0783349,247.62449 C68.8656954,243.620408 72.3274172,240.35102 76.4859792,237.44898 C80.6445412,234.546939 85.2751561,232.177551 90.1305582,230.359184 C94.9859603,228.540816 100.76299,227.089796 107.236859,226.006122 C113.710728,225.087755 120.409385,224.371429 127.13052,223.820408 C133.829177,223.453061 141.247152,223.269388 149.811542,223.269388 L167.704598,223.269388 L167.704598,249.057143 C167.704598,250.87551 168.401438,252.326531 170.019905,253.593878 C171.86316,254.861224 173.728893,255.595918 176.021722,255.595918 C178.112242,255.595918 180.180284,254.861224 181.82123,253.593878 L247.751296,201.834694 C249.369763,200.567347 250.291391,199.116327 250.291391,197.297959 C250.291391,195.479592 249.369763,194.028571 247.751296,192.761224 L181.82123,141.002041 C180.202763,139.734694 178.112242,139 176.044201,139 C173.728893,139 171.885639,139.734694 170.042384,141.002041 C168.423917,142.269388 167.727077,143.720408 167.727077,145.538776 L167.727077,171.326531 L149.811542,171.326531 C88.5120908,171.326531 50.8152886,184.955102 36.9234437,212.193878 C32.3153075,221.267347 30,232.526531 30,245.971429 C30,257.046939 35.5522422,272.291837 46.4094607,291.540816 C46.6567266,292.091837 47.1063009,292.826531 47.803141,294.093878 Z" id="Shape-Copy" fill="#000000" fill-rule="nonzero" transform="translate(140.145695, 220.000000) scale(-1, 1) translate(-140.145695, -220.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
@@ -293,7 +365,6 @@ footer {
|
||||
<table aria-describedby="summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
||||
<a href="?sort=namedirfirst&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
@@ -329,13 +400,11 @@ footer {
|
||||
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a>
|
||||
{{- end}}
|
||||
</th>
|
||||
<th class="hideable"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- if .CanGoUp}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="..">
|
||||
<span class="goup">Go up</span>
|
||||
@@ -343,18 +412,16 @@ footer {
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td class="hideable">—</td>
|
||||
<td class="hideable"></td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
{{- range .Items}}
|
||||
<tr class="file">
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{html .URL}}">
|
||||
{{- if .IsDir}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 317 259"><use xlink:href="#folder{{if .IsSymlink}}-shortcut{{end}}"></use></svg>
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
|
||||
{{- else}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 265 323"><use xlink:href="#file{{if .IsSymlink}}-shortcut{{end}}"></use></svg>
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
|
||||
{{- end}}
|
||||
<span class="name">{{html .Name}}</span>
|
||||
</a>
|
||||
@@ -365,7 +432,6 @@ footer {
|
||||
<td data-order="{{.Size}}">{{.HumanSize}}</td>
|
||||
{{- end}}
|
||||
<td class="hideable"><time datetime="{{.HumanModTime "2006-01-02T15:04:05Z"}}">{{.HumanModTime "01/02/2006 03:04:05 PM -07:00"}}</time></td>
|
||||
<td class="hideable"></td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
</tbody>
|
||||
@@ -377,8 +443,6 @@ footer {
|
||||
</footer>
|
||||
<script>
|
||||
var filterEl = document.getElementById('filter');
|
||||
filterEl.focus();
|
||||
|
||||
function filter() {
|
||||
var q = filterEl.value.trim().toLowerCase();
|
||||
var elems = document.querySelectorAll('tr.file');
|
||||
@@ -408,7 +472,7 @@ footer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
e.textContent = d.toLocaleString();
|
||||
}
|
||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||
timeList.forEach(localizeDatetime);
|
||||
@@ -0,0 +1,81 @@
|
||||
package browse
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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 detectaction 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
@@ -0,0 +1 @@
|
||||
<h1>Header</h1>
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<!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>
|
||||
@@ -0,0 +1 @@
|
||||
Should be hidden
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test 2</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
|
||||
// plug in the standard directives
|
||||
_ "github.com/mholt/caddy/caddyhttp/basicauth"
|
||||
_ "github.com/mholt/caddy/caddyhttp/bind"
|
||||
_ "github.com/mholt/caddy/caddyhttp/browse"
|
||||
_ "github.com/mholt/caddy/caddyhttp/errors"
|
||||
_ "github.com/mholt/caddy/caddyhttp/expvar"
|
||||
_ "github.com/mholt/caddy/caddyhttp/extensions"
|
||||
_ "github.com/mholt/caddy/caddyhttp/fastcgi"
|
||||
_ "github.com/mholt/caddy/caddyhttp/gzip"
|
||||
_ "github.com/mholt/caddy/caddyhttp/header"
|
||||
_ "github.com/mholt/caddy/caddyhttp/index"
|
||||
_ "github.com/mholt/caddy/caddyhttp/internalsrv"
|
||||
_ "github.com/mholt/caddy/caddyhttp/log"
|
||||
_ "github.com/mholt/caddy/caddyhttp/markdown"
|
||||
_ "github.com/mholt/caddy/caddyhttp/maxrequestbody"
|
||||
_ "github.com/mholt/caddy/caddyhttp/mime"
|
||||
_ "github.com/mholt/caddy/caddyhttp/pprof"
|
||||
_ "github.com/mholt/caddy/caddyhttp/proxy"
|
||||
_ "github.com/mholt/caddy/caddyhttp/push"
|
||||
_ "github.com/mholt/caddy/caddyhttp/redirect"
|
||||
_ "github.com/mholt/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/mholt/caddy/caddyhttp/root"
|
||||
_ "github.com/mholt/caddy/caddyhttp/status"
|
||||
_ "github.com/mholt/caddy/caddyhttp/templates"
|
||||
_ "github.com/mholt/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/mholt/caddy/caddyhttp/websocket"
|
||||
_ "github.com/mholt/caddy/startupshutdown"
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/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 := 31 // importing caddyhttp plugs in this many plugins
|
||||
s := caddy.DescribePlugins()
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
|
||||
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Package errors implements an HTTP error handling middleware.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), 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("%s [NOTICE %d %s] could not load error page: %v",
|
||||
time.Now().Format(timeFormat), 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("%s [NOTICE %d %s] could not respond with %s: %v",
|
||||
time.Now().Format(timeFormat), 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 := "/caddy/"
|
||||
pkgPathPos := strings.Index(file, delim)
|
||||
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
|
||||
file = file[pkgPathPos+len(delim):]
|
||||
}
|
||||
|
||||
panicMsg := fmt.Sprintf("%s [PANIC %s] %s:%d - %v", time.Now().Format(timeFormat), 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)
|
||||
}
|
||||
}
|
||||
|
||||
const timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
@@ -0,0 +1,258 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/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
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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()
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
where := c.Val()
|
||||
|
||||
if httpserver.IsLogRollerSubdirective(what) {
|
||||
var err error
|
||||
err = httpserver.ParseRoller(handler.Log.Roller, what, where)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration may be in a block
|
||||
err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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 }`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt", Roller: &httpserver.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
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,
|
||||
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{},
|
||||
}},
|
||||
// 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{}}},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package expvar
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/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
|
||||
@@ -0,0 +1,46 @@
|
||||
package expvar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/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
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package expvar
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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"
|
||||
@@ -0,0 +1,42 @@
|
||||
package expvar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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/mholt/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 path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' {
|
||||
for _, ext := range e.Extensions {
|
||||
if resourceExists(e.Root, urlpath+ext) {
|
||||
r.URL.Path = urlpath + ext
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return e.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// resourceExists returns true if the file specified at
|
||||
// root + path exists; false otherwise.
|
||||
func resourceExists(root, path string) bool {
|
||||
_, err := os.Stat(root + path)
|
||||
// technically we should use os.IsNotExist(err)
|
||||
// but we don't handle any other kinds of errors anyway
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dialer interface {
|
||||
Dial() (Client, error)
|
||||
Close(Client) error
|
||||
}
|
||||
|
||||
// basicDialer is a basic dialer that wraps default fcgi functions.
|
||||
type basicDialer struct {
|
||||
network string
|
||||
address string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (b basicDialer) Dial() (Client, error) {
|
||||
return DialTimeout(b.network, b.address, b.timeout)
|
||||
}
|
||||
|
||||
func (b basicDialer) Close(c Client) error { return c.Close() }
|
||||
|
||||
// persistentDialer keeps a pool of fcgi connections.
|
||||
// connections are not closed after use, rather added back to the pool for reuse.
|
||||
type persistentDialer struct {
|
||||
size int
|
||||
network string
|
||||
address string
|
||||
timeout time.Duration
|
||||
pool []Client
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (p *persistentDialer) Dial() (Client, error) {
|
||||
p.Lock()
|
||||
// connection is available, return first one.
|
||||
if len(p.pool) > 0 {
|
||||
client := p.pool[0]
|
||||
p.pool = p.pool[1:]
|
||||
p.Unlock()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
p.Unlock()
|
||||
|
||||
// no connection available, create new one
|
||||
return DialTimeout(p.network, p.address, p.timeout)
|
||||
}
|
||||
|
||||
func (p *persistentDialer) Close(client Client) error {
|
||||
p.Lock()
|
||||
if len(p.pool) < p.size {
|
||||
// pool is not full yet, add connection for reuse
|
||||
p.pool = append(p.pool, client)
|
||||
p.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p.Unlock()
|
||||
|
||||
// otherwise, close the connection.
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
type loadBalancingDialer struct {
|
||||
current int64
|
||||
dialers []dialer
|
||||
}
|
||||
|
||||
func (m *loadBalancingDialer) Dial() (Client, error) {
|
||||
nextDialerIndex := atomic.AddInt64(&m.current, 1) % int64(len(m.dialers))
|
||||
currentDialer := m.dialers[nextDialerIndex]
|
||||
|
||||
client, err := currentDialer.Dial()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dialerAwareClient{Client: client, dialer: currentDialer}, nil
|
||||
}
|
||||
|
||||
func (m *loadBalancingDialer) Close(c Client) error {
|
||||
// Close the client according to dialer behaviour
|
||||
if da, ok := c.(*dialerAwareClient); ok {
|
||||
return da.dialer.Close(c)
|
||||
}
|
||||
|
||||
return errors.New("Cannot close client")
|
||||
}
|
||||
|
||||
type dialerAwareClient struct {
|
||||
Client
|
||||
dialer dialer
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadbalancingDialer(t *testing.T) {
|
||||
// given
|
||||
runs := 100
|
||||
mockDialer1 := new(mockDialer)
|
||||
mockDialer2 := new(mockDialer)
|
||||
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}}
|
||||
|
||||
// when
|
||||
for i := 0; i < runs; i++ {
|
||||
client, err := dialer.Dial()
|
||||
dialer.Close(client)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected error to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
if mockDialer1.dialCalled != mockDialer2.dialCalled && mockDialer1.dialCalled != 50 {
|
||||
t.Errorf("Expected dialer to call Dial() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.dialCalled, mockDialer2.dialCalled)
|
||||
}
|
||||
|
||||
if mockDialer1.closeCalled != mockDialer2.closeCalled && mockDialer1.closeCalled != 50 {
|
||||
t.Errorf("Expected dialer to call Close() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.closeCalled, mockDialer2.closeCalled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancingDialerShouldReturnDialerAwareClient(t *testing.T) {
|
||||
// given
|
||||
mockDialer1 := new(mockDialer)
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}}
|
||||
|
||||
// when
|
||||
client, err := dialer.Dial()
|
||||
|
||||
// then
|
||||
if err != nil {
|
||||
t.Errorf("Expected error to be nil")
|
||||
}
|
||||
|
||||
if awareClient, ok := client.(*dialerAwareClient); !ok {
|
||||
t.Error("Expected dialer to wrap client")
|
||||
} else {
|
||||
if awareClient.dialer != mockDialer1 {
|
||||
t.Error("Expected wrapped client to have reference to dialer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancingDialerShouldUnderlyingReturnDialerError(t *testing.T) {
|
||||
// given
|
||||
mockDialer1 := new(errorReturningDialer)
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}}
|
||||
|
||||
// when
|
||||
_, err := dialer.Dial()
|
||||
|
||||
// then
|
||||
if err.Error() != "Error during dial" {
|
||||
t.Errorf("Expected 'Error during dial', got: '%s'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancingDialerShouldCloseClient(t *testing.T) {
|
||||
// given
|
||||
mockDialer1 := new(mockDialer)
|
||||
mockDialer2 := new(mockDialer)
|
||||
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}}
|
||||
client, _ := dialer.Dial()
|
||||
|
||||
// when
|
||||
err := dialer.Close(client)
|
||||
|
||||
// then
|
||||
if err != nil {
|
||||
t.Error("Expected error not to occur")
|
||||
}
|
||||
|
||||
// load balancing starts from index 1
|
||||
if mockDialer2.client != client {
|
||||
t.Errorf("Expected Close() to be called on referenced dialer")
|
||||
}
|
||||
}
|
||||
|
||||
type mockDialer struct {
|
||||
dialCalled int
|
||||
closeCalled int
|
||||
client Client
|
||||
}
|
||||
|
||||
type mockClient struct {
|
||||
Client
|
||||
}
|
||||
|
||||
func (m *mockDialer) Dial() (Client, error) {
|
||||
m.dialCalled++
|
||||
return mockClient{Client: &FCGIClient{}}, nil
|
||||
}
|
||||
|
||||
func (m *mockDialer) Close(c Client) error {
|
||||
m.client = c
|
||||
m.closeCalled++
|
||||
return nil
|
||||
}
|
||||
|
||||
type errorReturningDialer struct {
|
||||
client Client
|
||||
}
|
||||
|
||||
func (m *errorReturningDialer) Dial() (Client, error) {
|
||||
return mockClient{Client: &FCGIClient{}}, errors.New("Error during dial")
|
||||
}
|
||||
|
||||
func (m *errorReturningDialer) Close(c Client) error {
|
||||
m.client = c
|
||||
return errors.New("Error during close")
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// 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 and the path must be allowed.
|
||||
if !httpserver.Path(r.URL.Path).Matches(rule.Path) || !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
|
||||
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, 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
|
||||
fcgiBackend, err := rule.dialer.Dial()
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
return http.StatusGatewayTimeout, err
|
||||
}
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
defer fcgiBackend.Close()
|
||||
fcgiBackend.SetReadTimeout(rule.ReadTimeout)
|
||||
fcgiBackend.SetSendTimeout(rule.SendTimeout)
|
||||
|
||||
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)
|
||||
case "OPTIONS":
|
||||
resp, err = fcgiBackend.Options(env)
|
||||
default:
|
||||
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
}
|
||||
|
||||
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 stderr := fcgiBackend.StdErr(); stderr.Len() != 0 {
|
||||
// Remove trailing newline, error logger already does this.
|
||||
err = LogError(strings.TrimSuffix(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
|
||||
|
||||
// Get absolute path of requested resource
|
||||
absPath := filepath.Join(rule.Root, fpath)
|
||||
|
||||
// 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
|
||||
scriptFilename := absPath
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
scriptName = strings.TrimSuffix(scriptName, pathInfo)
|
||||
|
||||
// Get the request URI from context. The request URI might be as it came in over the wire,
|
||||
// or it might have been rewritten internally by the rewrite middleware (see issue #256).
|
||||
// If it was rewritten, there will be a context value with the original URL,
|
||||
// which is needed to get the correct RequestURI value for PHP apps.
|
||||
reqURI := r.URL.RequestURI()
|
||||
if origURI, _ := r.Context().Value(httpserver.URIxRewriteCtxKey).(string); origURI != "" {
|
||||
reqURI = origURI
|
||||
}
|
||||
|
||||
// Retrieve name of remote user that was set by some downstream middleware,
|
||||
// possibly basicauth.
|
||||
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string) // Blank if not set
|
||||
|
||||
// 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,
|
||||
"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": reqURI,
|
||||
"SCRIPT_FILENAME": scriptFilename,
|
||||
"SCRIPT_NAME": scriptName,
|
||||
}
|
||||
|
||||
// compliance with the CGI specification 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"
|
||||
}
|
||||
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
// Add env variables from config
|
||||
for _, envVar := range rule.EnvVars {
|
||||
// replace request placeholders in environment variables
|
||||
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
|
||||
|
||||
// The address of the FastCGI server. Required.
|
||||
Address string
|
||||
|
||||
// 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 reading from the FastCGI server.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// The duration used to set a deadline when sending to the FastCGI server.
|
||||
SendTimeout time.Duration
|
||||
|
||||
// FCGI dialer
|
||||
dialer dialer
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 listener.Close()
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
w.Write([]byte(body))
|
||||
}))
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
Address: listener.Addr().String(),
|
||||
dialer: basicDialer{network: network, address: address},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// connectionCounter in fact is a listener with an added counter to keep track
|
||||
// of the number of accepted connections.
|
||||
type connectionCounter struct {
|
||||
net.Listener
|
||||
sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
func (l *connectionCounter) Accept() (net.Conn, error) {
|
||||
l.Lock()
|
||||
l.counter++
|
||||
l.Unlock()
|
||||
return l.Listener.Accept()
|
||||
}
|
||||
|
||||
// TestPersistent ensures that persistent
|
||||
// as well as the non-persistent fastCGI servers
|
||||
// send the answers corresnponding to the correct request.
|
||||
// It also checks the number of tcp connections used.
|
||||
func TestPersistent(t *testing.T) {
|
||||
numberOfRequests := 32
|
||||
|
||||
for _, poolsize := range []int{0, 1, 5, numberOfRequests} {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create listener for test: %v", err)
|
||||
}
|
||||
|
||||
listener := &connectionCounter{l, *new(sync.Mutex), 0}
|
||||
|
||||
// this fcgi server replies with the request URL
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := "This answers a request to " + r.URL.Path
|
||||
bodyLenStr := strconv.Itoa(len(body))
|
||||
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
w.Write([]byte(body))
|
||||
}))
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{{Path: "/", Address: listener.Addr().String(), dialer: &persistentDialer{size: poolsize, network: network, address: address}}},
|
||||
}
|
||||
|
||||
var semaphore sync.WaitGroup
|
||||
serialMutex := new(sync.Mutex)
|
||||
|
||||
serialCounter := 0
|
||||
parallelCounter := 0
|
||||
// make some serial followed by some
|
||||
// parallel requests to challenge the handler
|
||||
for _, serialize := range []bool{true, false, false, false} {
|
||||
if serialize {
|
||||
serialCounter++
|
||||
} else {
|
||||
parallelCounter++
|
||||
}
|
||||
semaphore.Add(numberOfRequests)
|
||||
|
||||
for i := 0; i < numberOfRequests; i++ {
|
||||
go func(i int, serialize bool) {
|
||||
defer semaphore.Done()
|
||||
if serialize {
|
||||
serialMutex.Lock()
|
||||
defer serialMutex.Unlock()
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/"+strconv.Itoa(i), nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create request: %v", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
status, err := handler.ServeHTTP(w, r)
|
||||
|
||||
if status != 0 {
|
||||
t.Errorf("Handler(pool: %v) return status %v", poolsize, status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Handler(pool: %v) Error: %v", poolsize, err)
|
||||
}
|
||||
want := "This answers a request to /" + strconv.Itoa(i)
|
||||
if got := w.Body.String(); got != want {
|
||||
t.Errorf("Expected response from handler(pool: %v) to be '%s', got: '%s'", poolsize, want, got)
|
||||
}
|
||||
}(i, serialize)
|
||||
} //next request
|
||||
semaphore.Wait()
|
||||
} // next set of requests (serial/parallel)
|
||||
|
||||
listener.Close()
|
||||
t.Logf("The pool: %v test used %v tcp connections to answer %v * %v serial and %v * %v parallel requests.", poolsize, listener.counter, serialCounter, numberOfRequests, parallelCounter, numberOfRequests)
|
||||
} // next handler (persistent/non-persistent)
|
||||
}
|
||||
|
||||
func TestRuleParseAddress(t *testing.T) {
|
||||
getClientTestTable := []struct {
|
||||
rule *Rule
|
||||
expectednetwork string
|
||||
expectedaddress string
|
||||
}{
|
||||
{&Rule{Address: "tcp://172.17.0.1:9000"}, "tcp", "172.17.0.1:9000"},
|
||||
{&Rule{Address: "fastcgi://localhost:9000"}, "tcp", "localhost:9000"},
|
||||
{&Rule{Address: "172.17.0.15"}, "tcp", "172.17.0.15"},
|
||||
{&Rule{Address: "/my/unix/socket"}, "unix", "/my/unix/socket"},
|
||||
{&Rule{Address: "unix:/second/unix/socket"}, "unix", "/second/unix/socket"},
|
||||
}
|
||||
|
||||
for _, entry := range getClientTestTable {
|
||||
if actualnetwork, _ := parseAddress(entry.rule.Address); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", entry.rule.Address, actualnetwork, entry.expectednetwork)
|
||||
}
|
||||
if _, actualaddress := parseAddress(entry.rule.Address); actualaddress != entry.expectedaddress {
|
||||
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", entry.rule.Address, 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{}
|
||||
url, err := url.Parse("http://localhost:2015/fgci_test.php?test=blabla")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error:", err.Error())
|
||||
}
|
||||
|
||||
var newReq = func() *http.Request {
|
||||
return &http.Request{
|
||||
Method: "GET",
|
||||
URL: url,
|
||||
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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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=blabla",
|
||||
"REQUEST_METHOD": "GET",
|
||||
"HTTP_HOST": "localhost:2015",
|
||||
}
|
||||
}
|
||||
|
||||
// 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=blabla"
|
||||
envExpected["CUSTOM_QUERY"] = "custom=true&test=blabla"
|
||||
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 listener.Close()
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
Address: listener.Addr().String(),
|
||||
dialer: basicDialer{network: network, address: address},
|
||||
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 fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(test.sleep)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
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 listener.Close()
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
Address: listener.Addr().String(),
|
||||
dialer: basicDialer{network: network, address: address},
|
||||
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 fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
+77
-72
@@ -1,21 +1,8 @@
|
||||
// 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.
|
||||
|
||||
// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
|
||||
// (which is forked from https://code.google.com/p/go-fastcgi-client/).
|
||||
// (which is forked from https://code.google.com/p/go-fastcgi-client/)
|
||||
|
||||
// This fork contains several fixes and improvements by Matt Holt and
|
||||
// other contributors to the Caddy project.
|
||||
// other contributors to this project.
|
||||
|
||||
// Copyright 2012 Junqing Tan <ivan@mysqlab.net> and The Go Authors
|
||||
// Use of this source code is governed by a BSD-style
|
||||
@@ -26,7 +13,6 @@ package fastcgi
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -121,6 +107,18 @@ const (
|
||||
maxPad = 255
|
||||
)
|
||||
|
||||
// Client interface
|
||||
type Client interface {
|
||||
Get(pair map[string]string) (response *http.Response, err error)
|
||||
Head(pair map[string]string) (response *http.Response, err error)
|
||||
Options(pairs map[string]string) (response *http.Response, err error)
|
||||
Post(pairs map[string]string, method string, bodyType string, body io.Reader, contentLength int64) (response *http.Response, err error)
|
||||
Close() error
|
||||
StdErr() bytes.Buffer
|
||||
SetReadTimeout(time.Duration) error
|
||||
SetSendTimeout(time.Duration) error
|
||||
}
|
||||
|
||||
type header struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
@@ -152,7 +150,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
return
|
||||
}
|
||||
if rec.h.Version != 1 {
|
||||
err = errors.New("fcgi: invalid header version")
|
||||
err = errInvalidHeaderVersion
|
||||
return
|
||||
}
|
||||
if rec.h.Type == EndRequest {
|
||||
@@ -174,67 +172,64 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
// FCGIClient implements a FastCGI client, which is a standard for
|
||||
// interfacing external applications with Web servers.
|
||||
type FCGIClient struct {
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
keepAlive bool
|
||||
reqID uint16
|
||||
mutex sync.Mutex
|
||||
conn net.Conn
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
keepAlive bool
|
||||
reqID uint16
|
||||
readTimeout time.Duration
|
||||
sendTimeout time.Duration
|
||||
}
|
||||
|
||||
// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
|
||||
// and a context.
|
||||
// DialTimeout connects to the fcgi responder at the specified network address, using default net.Dialer.
|
||||
// See func net.Dial for a description of the network and address parameters.
|
||||
func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
|
||||
var conn net.Conn
|
||||
conn, err = dialer.DialContext(ctx, network, address)
|
||||
func DialTimeout(network string, address string, timeout time.Duration) (fcgi *FCGIClient, err error) {
|
||||
conn, err := net.DialTimeout(network, address, timeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fcgi = &FCGIClient{
|
||||
rwc: conn,
|
||||
keepAlive: false,
|
||||
reqID: 1,
|
||||
}
|
||||
fcgi = &FCGIClient{conn: conn, keepAlive: false, reqID: 1}
|
||||
|
||||
return
|
||||
return fcgi, nil
|
||||
}
|
||||
|
||||
// DialContext is like Dial but passes ctx to dialer.Dial.
|
||||
func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) {
|
||||
// TODO: why not set timeout here?
|
||||
return DialWithDialerContext(ctx, network, address, net.Dialer{})
|
||||
// Close closes fcgi connnection.
|
||||
func (c *FCGIClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// Dial connects to the fcgi responder at the specified network address, using default net.Dialer.
|
||||
// See func net.Dial for a description of the network and address parameters.
|
||||
func Dial(network, address string) (fcgi *FCGIClient, err error) {
|
||||
return DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
// Close closes fcgi connection
|
||||
func (c *FCGIClient) Close() {
|
||||
c.rwc.Close()
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
|
||||
func (c *FCGIClient) writeRecord(recType uint8, content []byte) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.buf.Reset()
|
||||
c.h.init(recType, c.reqID, len(content))
|
||||
|
||||
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.buf.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.rwc.Write(c.buf.Bytes())
|
||||
return err
|
||||
|
||||
if c.sendTimeout != 0 {
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(c.sendTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write(c.buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
|
||||
@@ -350,13 +345,14 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
|
||||
|
||||
if len(p) > 0 {
|
||||
if len(w.buf) == 0 {
|
||||
|
||||
// filter outputs for error log
|
||||
for {
|
||||
rec := &record{}
|
||||
var buf []byte
|
||||
buf, err = rec.read(w.c.rwc)
|
||||
if err != nil {
|
||||
buf, err = rec.read(w.c.conn)
|
||||
if err == errInvalidHeaderVersion {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
// standard error output
|
||||
@@ -380,10 +376,15 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// StdErr returns stderr stream
|
||||
func (c *FCGIClient) StdErr() bytes.Buffer {
|
||||
return c.stderr
|
||||
}
|
||||
|
||||
// Do made the request and returns a io.Reader that translates the data read
|
||||
// from fcgi responder out of fcgi packet before returning it.
|
||||
func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
|
||||
err = c.writeBeginRequest(uint16(Responder), 0)
|
||||
err = c.writeBeginRequest(uint16(Responder), FCGIKeepConn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -395,7 +396,7 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
|
||||
|
||||
body := newWriter(c, Stdin)
|
||||
if req != nil {
|
||||
_, _ = io.Copy(body, req)
|
||||
io.Copy(body, req)
|
||||
}
|
||||
body.Close()
|
||||
|
||||
@@ -406,11 +407,11 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
// that closes FCGIClient connection.
|
||||
type clientCloser struct {
|
||||
*FCGIClient
|
||||
f *FCGIClient
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (f clientCloser) Close() error { return f.rwc.Close() }
|
||||
func (c clientCloser) Close() error { return c.f.Close() }
|
||||
|
||||
// Request returns a HTTP Response with Header and Body
|
||||
// from fcgi responder
|
||||
@@ -424,6 +425,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
|
||||
tp := textproto.NewReader(rb)
|
||||
resp = new(http.Response)
|
||||
|
||||
if c.readTimeout != 0 {
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the response headers.
|
||||
mimeHeader, err := tp.ReadMIMEHeader()
|
||||
if err != nil && err != io.EOF {
|
||||
@@ -458,12 +465,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
|
||||
}
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
|
||||
func (c *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "GET"
|
||||
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
||||
p["CONTENT_LENGTH"] = "0"
|
||||
|
||||
return c.Request(p, body)
|
||||
return c.Request(p, nil)
|
||||
}
|
||||
|
||||
// Head issues a HEAD request to the fcgi responder.
|
||||
@@ -559,20 +566,18 @@ func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[str
|
||||
// SetReadTimeout sets the read timeout for future calls that read from the
|
||||
// fcgi responder. A zero value for t means no timeout will be set.
|
||||
func (c *FCGIClient) SetReadTimeout(t time.Duration) error {
|
||||
if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
|
||||
return conn.SetReadDeadline(time.Now().Add(t))
|
||||
}
|
||||
c.readTimeout = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteTimeout sets the write timeout for future calls that send data to
|
||||
// SetSendTimeout sets the read timeout for future calls that send data to
|
||||
// the fcgi responder. A zero value for t means no timeout will be set.
|
||||
func (c *FCGIClient) SetWriteTimeout(t time.Duration) error {
|
||||
if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
|
||||
return conn.SetWriteDeadline(time.Now().Add(t))
|
||||
}
|
||||
func (c *FCGIClient) SetSendTimeout(t time.Duration) error {
|
||||
c.sendTimeout = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks whether chunked is part of the encodings stack
|
||||
func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
|
||||
|
||||
var errInvalidHeaderVersion = errors.New("fcgi: invalid header version")
|
||||
+17
-43
@@ -1,17 +1,3 @@
|
||||
// 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.
|
||||
|
||||
// NOTE: These tests were adapted from the original
|
||||
// repository from which this package was forked.
|
||||
// The tests are slow (~10s) and in dire need of rewriting.
|
||||
@@ -44,7 +30,7 @@ import (
|
||||
// test fcgi protocol includes:
|
||||
// Get, Post, Post in multipart/form-data, and Post with files
|
||||
// each key should be the md5 of the value or the file uploaded
|
||||
// specify remote fcgi responder ip:port to test with php
|
||||
// sepicify remote fcgi responer ip:port to test with php
|
||||
// test failed if the remote fcgi(script) failed md5 verification
|
||||
// and output "FAILED" in response
|
||||
const (
|
||||
@@ -59,9 +45,7 @@ type FastCGIServer struct{}
|
||||
|
||||
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if err := req.ParseMultipartForm(100000000); err != nil {
|
||||
log.Printf("[ERROR] failed to parse: %v", err)
|
||||
}
|
||||
req.ParseMultipartForm(100000000)
|
||||
|
||||
stat := "PASSED"
|
||||
fmt.Fprintln(resp, "-")
|
||||
@@ -70,15 +54,15 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
length := 0
|
||||
for k0, v0 := range req.Form {
|
||||
h := md5.New()
|
||||
_, _ = io.WriteString(h, v0[0])
|
||||
_md5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
io.WriteString(h, v0[0])
|
||||
md5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
length += len(k0)
|
||||
length += len(v0[0])
|
||||
|
||||
// echo error when key != _md5(val)
|
||||
if _md5 != k0 {
|
||||
fmt.Fprintln(resp, "server:err ", _md5, k0)
|
||||
// echo error when key != md5(val)
|
||||
if md5 != k0 {
|
||||
fmt.Fprintln(resp, "server:err ", md5, k0)
|
||||
stat = "FAILED"
|
||||
}
|
||||
}
|
||||
@@ -119,7 +103,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
|
||||
fcgi, err := Dial("tcp", ipPort)
|
||||
fcgi, err := DialTimeout("tcp", ipPort, 0)
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return
|
||||
@@ -142,8 +126,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
}
|
||||
resp, err = fcgi.PostForm(fcgiParams, values)
|
||||
} else {
|
||||
rd := bytes.NewReader(data)
|
||||
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
|
||||
resp, err = fcgi.Get(fcgiParams)
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -199,12 +182,8 @@ func generateRandFile(size int) (p string, m string) {
|
||||
for i := 0; i < size/16; i++ {
|
||||
buf := make([]byte, 16)
|
||||
binary.PutVarint(buf, rand.Int63())
|
||||
if _, err := fo.Write(buf); err != nil {
|
||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||
}
|
||||
if _, err := h.Write(buf); err != nil {
|
||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||
}
|
||||
fo.Write(buf)
|
||||
h.Write(buf)
|
||||
}
|
||||
m = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return
|
||||
@@ -220,13 +199,12 @@ func DisabledTest(t *testing.T) {
|
||||
go func() {
|
||||
listener, err := net.Listen("tcp", ipPort)
|
||||
if err != nil {
|
||||
// handle error
|
||||
log.Println("listener creation failed: ", err)
|
||||
}
|
||||
|
||||
srv := new(FastCGIServer)
|
||||
if err := fcgi.Serve(listener, srv); err != nil {
|
||||
log.Print("[ERROR] failed to start server: ", err)
|
||||
}
|
||||
fcgi.Serve(listener, srv)
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -251,7 +229,7 @@ func DisabledTest(t *testing.T) {
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 256)
|
||||
h := md5.New()
|
||||
_, _ = io.WriteString(h, v0)
|
||||
io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
data += k0 + "=" + url.QueryEscape(v0) + "&"
|
||||
}
|
||||
@@ -268,7 +246,7 @@ func DisabledTest(t *testing.T) {
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 4096)
|
||||
h := md5.New()
|
||||
_, _ = io.WriteString(h, v0)
|
||||
io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
p1[k0] = v0
|
||||
}
|
||||
@@ -292,10 +270,6 @@ func DisabledTest(t *testing.T) {
|
||||
delete(f0, "m0")
|
||||
sendFcgi(1, fcgiParams, nil, nil, f0)
|
||||
|
||||
if err := os.Remove(path0); err != nil {
|
||||
log.Println("[ERROR] failed to remove path: ", err)
|
||||
}
|
||||
if err := os.Remove(path1); err != nil {
|
||||
log.Println("[ERROR] failed to remove path: ", err)
|
||||
}
|
||||
os.Remove(path0)
|
||||
os.Remove(path1)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
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],
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}
|
||||
upstreams := []string{args[1]}
|
||||
|
||||
if len(args) == 3 {
|
||||
if err := fastcgiPreset(args[2], &rule); err != nil {
|
||||
return rules, err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
var pool int
|
||||
var connectTimeout = 60 * time.Second
|
||||
var dialers []dialer
|
||||
var poolSize = -1
|
||||
|
||||
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":
|
||||
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 "pool":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
pool, err = strconv.Atoi(c.Val())
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
if pool >= 0 {
|
||||
poolSize = pool
|
||||
} else {
|
||||
return rules, c.Errf("positive integer expected, found %d", pool)
|
||||
}
|
||||
case "connect_timeout":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for _, rawAddress := range upstreams {
|
||||
network, address := parseAddress(rawAddress)
|
||||
if poolSize >= 0 {
|
||||
dialers = append(dialers, &persistentDialer{
|
||||
size: poolSize,
|
||||
network: network,
|
||||
address: address,
|
||||
timeout: connectTimeout,
|
||||
})
|
||||
} else {
|
||||
dialers = append(dialers, basicDialer{
|
||||
network: network,
|
||||
address: address,
|
||||
timeout: connectTimeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rule.dialer = &loadBalancingDialer{dialers: dialers}
|
||||
rule.Address = strings.Join(upstreams, ",")
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *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
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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")
|
||||
}
|
||||
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *persistentDialer) Equals(q *persistentDialer) bool {
|
||||
if p.size != q.size {
|
||||
return false
|
||||
}
|
||||
if p.network != q.network {
|
||||
return false
|
||||
}
|
||||
if p.address != q.address {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.pool) != len(q.pool) {
|
||||
return false
|
||||
}
|
||||
for i, client := range p.pool {
|
||||
if client != q.pool[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// ignore mutex state
|
||||
return true
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
rootPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("Can't determine current working directory; got '%v'", err)
|
||||
}
|
||||
|
||||
defaultAddress := "127.0.0.1:9001"
|
||||
network, address := parseAddress(defaultAddress)
|
||||
t.Logf("Address '%v' was parsed to network '%v' and address '%v'", defaultAddress, network, address)
|
||||
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{"index.php"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi /blog 127.0.0.1:9000 php {
|
||||
root /tmp
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: "/tmp",
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{"index.php"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi /blog 127.0.0.1:9000 php {
|
||||
upstream 127.0.0.1:9001
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000,127.0.0.1:9001",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}, basicDialer{network: "tcp", address: "127.0.0.1:9001", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{"index.php"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi /blog 127.0.0.1:9000 {
|
||||
upstream 127.0.0.1:9001
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000,127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}, basicDialer{network: "tcp", address: "127.0.0.1:9001", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
split .html
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
split .html
|
||||
except /admin /user
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
IgnoredSubPaths: []string{"/admin", "/user"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
pool 0
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{&persistentDialer{size: 0, network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:8080 {
|
||||
upstream 127.0.0.1:9000
|
||||
pool 5
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:8080,127.0.0.1:9000",
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{&persistentDialer{size: 5, network: "tcp", address: "127.0.0.1:8080", timeout: 60 * time.Second}, &persistentDialer{size: 5, network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
split .php
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
connect_timeout 5s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 5 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{
|
||||
`fastcgi / ` + defaultAddress + ` { connect_timeout BADVALUE }`,
|
||||
true,
|
||||
[]Rule{},
|
||||
},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
read_timeout 5s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 5 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{
|
||||
`fastcgi / ` + defaultAddress + ` { read_timeout BADVALUE }`,
|
||||
true,
|
||||
[]Rule{},
|
||||
},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
send_timeout 5s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 5 * time.Second,
|
||||
}}},
|
||||
{
|
||||
`fastcgi / ` + defaultAddress + ` { send_timeout BADVALUE }`,
|
||||
true,
|
||||
[]Rule{},
|
||||
},
|
||||
{`fastcgi / {
|
||||
|
||||
}`,
|
||||
true, []Rule{},
|
||||
},
|
||||
}
|
||||
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.Root != test.expectedFastcgiConfig[j].Root {
|
||||
t.Errorf("Test %d expected %dth FastCGI Root to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Root, actualFastcgiConfig.Root)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
|
||||
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
|
||||
}
|
||||
|
||||
if reflect.TypeOf(actualFastcgiConfig.dialer) != reflect.TypeOf(test.expectedFastcgiConfig[j].dialer) {
|
||||
t.Errorf("Test %d expected %dth FastCGI dialer to be of type %T, but got %T",
|
||||
i, j, test.expectedFastcgiConfig[j].dialer, actualFastcgiConfig.dialer)
|
||||
} else {
|
||||
if !areDialersEqual(actualFastcgiConfig.dialer, test.expectedFastcgiConfig[j].dialer, t) {
|
||||
t.Errorf("Test %d expected %dth FastCGI dialer to be %v, but got %v",
|
||||
i, j, test.expectedFastcgiConfig[j].dialer, actualFastcgiConfig.dialer)
|
||||
}
|
||||
}
|
||||
|
||||
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 fmt.Sprint(actualFastcgiConfig.ReadTimeout) != fmt.Sprint(test.expectedFastcgiConfig[j].ReadTimeout) {
|
||||
t.Errorf("Test %d expected %dth FastCGI ReadTimeout to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].ReadTimeout, actualFastcgiConfig.ReadTimeout)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.SendTimeout) != fmt.Sprint(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 areDialersEqual(current, expected dialer, t *testing.T) bool {
|
||||
|
||||
switch actual := current.(type) {
|
||||
case *loadBalancingDialer:
|
||||
if expected, ok := expected.(*loadBalancingDialer); ok {
|
||||
for i := 0; i < len(actual.dialers); i++ {
|
||||
if !areDialersEqual(actual.dialers[i], expected.dialers[i], t) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
case basicDialer:
|
||||
return current == expected
|
||||
case *persistentDialer:
|
||||
if expected, ok := expected.(*persistentDialer); ok {
|
||||
return actual.Equals(expected)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("Unknown dialer type %T", current)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Package gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("gzip", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// gzipWriter modifies underlying writer at init,
|
||||
// use a discard writer instead to leave ResponseWriter in
|
||||
// original form.
|
||||
gzipWriter, err := newWriter(c, ioutil.Discard)
|
||||
if err != nil {
|
||||
// should not happen
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer gzipWriter.Close()
|
||||
gz := &gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}
|
||||
|
||||
var rw http.ResponseWriter
|
||||
// if no response filter is used
|
||||
if len(c.ResponseFilters) == 0 {
|
||||
// replace discard writer with ResponseWriter
|
||||
gzipWriter.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)
|
||||
}
|
||||
|
||||
// newWriter create a new Gzip Writer based on the compression level.
|
||||
// If the level is valid (i.e. between 1 and 9), it uses the level.
|
||||
// Otherwise, it uses default compression level.
|
||||
func newWriter(c Config, w io.Writer) (*gzip.Writer, error) {
|
||||
if c.Level >= gzip.BestSpeed && c.Level <= gzip.BestCompression {
|
||||
return gzip.NewWriterLevel(w, c.Level)
|
||||
}
|
||||
return gzip.NewWriter(w), nil
|
||||
}
|
||||
|
||||
// gzipResponeWriter wraps the underlying Write method
|
||||
// with a gzip.Writer to compress the output.
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
statusCodeWritten bool
|
||||
}
|
||||
|
||||
// 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")
|
||||
w.ResponseWriter.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
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||
func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := w.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, httpserver.NonHijackerError{Underlying: w.ResponseWriter}
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or panics.
|
||||
func (w *gzipResponseWriter) Flush() {
|
||||
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic(httpserver.NonFlusherError{Underlying: w.ResponseWriter}) // should be recovered at the beginning of middleware stack
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
func (w *gzipResponseWriter) CloseNotify() <-chan bool {
|
||||
if cn, ok := w.ResponseWriter.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter})
|
||||
}
|
||||
|
||||
func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
if pusher, hasPusher := w.ResponseWriter.(http.Pusher); hasPusher {
|
||||
return pusher.Push(target, opts)
|
||||
}
|
||||
|
||||
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var _ http.Pusher = (*gzipResponseWriter)(nil)
|
||||
var _ http.Flusher = (*gzipResponseWriter)(nil)
|
||||
var _ http.CloseNotifier = (*gzipResponseWriter)(nil)
|
||||
var _ http.Hijacker = (*gzipResponseWriter)(nil)
|
||||
@@ -0,0 +1,119 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/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")
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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", r.Header.Get("Content-Encoding"))
|
||||
}
|
||||
if w.Header().Get("Vary") != "Accept-Encoding" {
|
||||
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", r.Header.Get("Vary"))
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/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"}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/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), r, false})
|
||||
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)))
|
||||
w.Write([]byte(ts.body))
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
r := urlRequest("/")
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
|
||||
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")
|
||||
w.Write([]byte("gzipped"))
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
r := urlRequest("/")
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
server.ServeHTTP(w, r)
|
||||
resp := w.Body.String()
|
||||
|
||||
if resp != "gzipped" {
|
||||
t.Errorf("Expected output not to be gzipped")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/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
@@ -0,0 +1,308 @@
|
||||
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.
|
||||
@@ -0,0 +1,151 @@
|
||||
// Package header provides middleware that appends headers to
|
||||
// requests based on a set of configuration rules that define
|
||||
// which routes receive which headers.
|
||||
package header
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Headers is middleware that adds headers to the responses
|
||||
// for requests matching a certain path.
|
||||
type Headers struct {
|
||||
Next httpserver.Handler
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the httpserver.Handler interface and serves requests,
|
||||
// setting headers on the response according to the configured rules.
|
||||
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
rww := &responseWriterWrapper{ResponseWriter: w}
|
||||
for _, rule := range h.Rules {
|
||||
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||
for name := range rule.Headers {
|
||||
|
||||
// One can either delete a header, add multiple values to a header, or simply
|
||||
// set a header.
|
||||
|
||||
if strings.HasPrefix(name, "-") {
|
||||
rww.delHeader(strings.TrimLeft(name, "-"))
|
||||
} else if strings.HasPrefix(name, "+") {
|
||||
for _, value := range rule.Headers[name] {
|
||||
rww.Header().Add(strings.TrimLeft(name, "+"), replacer.Replace(value))
|
||||
}
|
||||
} else {
|
||||
for _, value := range rule.Headers[name] {
|
||||
rww.Header().Set(name, replacer.Replace(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return h.Next.ServeHTTP(rww, r)
|
||||
}
|
||||
|
||||
type (
|
||||
// Rule groups a slice of HTTP headers by a URL pattern.
|
||||
Rule struct {
|
||||
Path string
|
||||
Headers http.Header
|
||||
}
|
||||
)
|
||||
|
||||
// headerOperation represents an operation on the header
|
||||
type headerOperation func(http.Header)
|
||||
|
||||
// responseWriterWrapper wraps the real ResponseWriter.
|
||||
// It defers header operations until writeHeader
|
||||
type responseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
ops []headerOperation
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) Header() http.Header {
|
||||
return rww.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
|
||||
if !rww.wroteHeader {
|
||||
rww.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return rww.ResponseWriter.Write(d)
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) WriteHeader(status int) {
|
||||
if rww.wroteHeader {
|
||||
return
|
||||
}
|
||||
rww.wroteHeader = true
|
||||
// capture the original headers
|
||||
h := rww.Header()
|
||||
|
||||
// perform our revisions
|
||||
for _, op := range rww.ops {
|
||||
op(h)
|
||||
}
|
||||
|
||||
rww.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// delHeader deletes the existing header according to the key
|
||||
// Also it will delete that header added later.
|
||||
func (rww *responseWriterWrapper) delHeader(key string) {
|
||||
// remove the existing one if any
|
||||
rww.Header().Del(key)
|
||||
|
||||
// register a future deletion
|
||||
rww.ops = append(rww.ops, func(h http.Header) {
|
||||
h.Del(key)
|
||||
})
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||
func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, httpserver.NonHijackerError{Underlying: rww.ResponseWriter}
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or panics.
|
||||
func (rww *responseWriterWrapper) Flush() {
|
||||
if f, ok := rww.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic(httpserver.NonFlusherError{Underlying: rww.ResponseWriter}) // should be recovered at the beginning of middleware stack
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
// It panics if the underlying ResponseWriter is not a CloseNotifier.
|
||||
func (rww *responseWriterWrapper) CloseNotify() <-chan bool {
|
||||
if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic(httpserver.NonCloseNotifierError{Underlying: rww.ResponseWriter})
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) Push(target string, opts *http.PushOptions) error {
|
||||
if pusher, hasPusher := rww.ResponseWriter.(http.Pusher); hasPusher {
|
||||
return pusher.Push(target, opts)
|
||||
}
|
||||
|
||||
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var _ http.Pusher = (*responseWriterWrapper)(nil)
|
||||
var _ http.Flusher = (*responseWriterWrapper)(nil)
|
||||
var _ http.CloseNotifier = (*responseWriterWrapper)(nil)
|
||||
var _ http.Hijacker = (*responseWriterWrapper)(nil)
|
||||
@@ -0,0 +1,95 @@
|
||||
package header
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not determine hostname: %v", err)
|
||||
}
|
||||
for i, test := range []struct {
|
||||
from string
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"/a", "Foo", "Bar"},
|
||||
{"/a", "Bar", ""},
|
||||
{"/a", "Baz", ""},
|
||||
{"/a", "Server", ""},
|
||||
{"/a", "ServerName", hostname},
|
||||
{"/b", "Foo", ""},
|
||||
{"/b", "Bar", "Removed in /a"},
|
||||
} {
|
||||
he := Headers{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Bar", "Removed in /a")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return 0, nil
|
||||
}),
|
||||
Rules: []Rule{
|
||||
{Path: "/a", Headers: http.Header{
|
||||
"Foo": []string{"Bar"},
|
||||
"ServerName": []string{"{hostname}"},
|
||||
"-Bar": []string{""},
|
||||
"-Server": []string{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
// preset header
|
||||
rec.Header().Set("Server", "Caddy")
|
||||
|
||||
he.ServeHTTP(rec, req)
|
||||
|
||||
if got := rec.Header().Get(test.name); got != test.value {
|
||||
t.Errorf("Test %d: Expected %s header to be %q but was %q",
|
||||
i, test.name, test.value, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleHeaders(t *testing.T) {
|
||||
he := Headers{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprint(w, "This is a test")
|
||||
return 0, nil
|
||||
}),
|
||||
Rules: []Rule{
|
||||
{Path: "/a", Headers: http.Header{
|
||||
"+Link": []string{"</images/image.png>; rel=preload", "</css/main.css>; rel=preload"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/a", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
he.ServeHTTP(rec, req)
|
||||
|
||||
desiredHeaders := []string{"</css/main.css>; rel=preload", "</images/image.png>; rel=preload"}
|
||||
actualHeaders := rec.HeaderMap[http.CanonicalHeaderKey("Link")]
|
||||
sort.Strings(actualHeaders)
|
||||
|
||||
if !reflect.DeepEqual(desiredHeaders, actualHeaders) {
|
||||
t.Errorf("Expected header to contain: %v but got: %v", desiredHeaders, actualHeaders)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("header", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new Headers middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Headers{Next: next, Rules: rules}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func headersParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
|
||||
for c.NextLine() {
|
||||
var head Rule
|
||||
head.Headers = http.Header{}
|
||||
var isNewPattern bool
|
||||
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
pattern := c.Val()
|
||||
|
||||
// See if we already have a definition for this Path pattern...
|
||||
for _, h := range rules {
|
||||
if h.Path == pattern {
|
||||
head = h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ...otherwise, this is a new pattern
|
||||
if head.Path == "" {
|
||||
head.Path = pattern
|
||||
isNewPattern = true
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
// A block of headers was opened...
|
||||
name := c.Val()
|
||||
value := ""
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) > 1 {
|
||||
return rules, c.ArgErr()
|
||||
} else if len(args) == 1 {
|
||||
value = args[0]
|
||||
}
|
||||
|
||||
head.Headers.Add(name, value)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// ... or single header was defined as an argument instead.
|
||||
|
||||
name := c.Val()
|
||||
value := c.Val()
|
||||
|
||||
if c.NextArg() {
|
||||
value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers.Add(name, value)
|
||||
}
|
||||
|
||||
if isNewPattern {
|
||||
rules = append(rules, head)
|
||||
} else {
|
||||
for i := 0; i < len(rules); i++ {
|
||||
if rules[i].Path == pattern {
|
||||
rules[i] = head
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user