mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
776 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 79de2a5de2 |
@@ -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
|
||||
+20
-4
@@ -1,14 +1,30 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4.2
|
||||
- 1.5.1
|
||||
- 1.6.2
|
||||
- tip
|
||||
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
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:
|
||||
- go get -d ./...
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
- 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
|
||||
|
||||
script:
|
||||
- diff <(echo -n) <(gofmt -s -d .)
|
||||
- ineffassign .
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
|
||||
after_script:
|
||||
- golint ./...
|
||||
|
||||
+91
-13
@@ -1,32 +1,110 @@
|
||||
## Contributing to Caddy
|
||||
|
||||
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
|
||||
|
||||
This project gladly accepts contributions and we encourage interested users to get involved!
|
||||
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!
|
||||
|
||||
|
||||
#### For small tweaks, bug fixes, and tests
|
||||
### Join us in the forum
|
||||
|
||||
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time. Thank you for helping out in simple ways! Bug fixes should be under test to assert correct behavior.
|
||||
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.
|
||||
|
||||
|
||||
#### Ideas, questions, bug reports
|
||||
### Bug reports
|
||||
|
||||
You should totally [open an issue](https://github.com/mholt/caddy/issues) with your ideas, questions, and bug reports, if one does not already exist for it. Bug reports should state expected behavior and contain clear instructions for reproducing the problem.
|
||||
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!
|
||||
|
||||
|
||||
#### New features
|
||||
### Minor improvements and new tests
|
||||
|
||||
Before submitting a pull request, please open an issue first to discuss it and claim it. This prevents overlapping efforts and keeps the project in-line with its goals. If you prefer to discuss the feature privately, you can reach other developers on Slack or you may email me directly. (My email address is below.)
|
||||
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.
|
||||
|
||||
And don't forget to write tests for new features!
|
||||
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.
|
||||
|
||||
|
||||
#### Vulnerabilities
|
||||
### Proposals, suggestions, ideas, new features
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### 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.
|
||||
Thanks for your help! Caddy would not be what it is today without your
|
||||
contributions.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
(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 answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. 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. What did you expect to see?
|
||||
|
||||
|
||||
#### 6. What did you see instead (give full error messages and/or log)?
|
||||
|
||||
|
||||
#### 7. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
||||
|
||||
@@ -1,133 +1,158 @@
|
||||
[](https://caddyserver.com)
|
||||
<a href="https://caddyserver.com"><img src="https://caddyserver.com/resources/images/caddy-lower.png" alt="Caddy" width="350"></a>
|
||||
|
||||
[](https://godoc.org/github.com/mholt/caddy) [](https://travis-ci.org/mholt/caddy)
|
||||
[](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)
|
||||
|
||||
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is a capable alternative to other popular and easy to use web servers.
|
||||
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.
|
||||
|
||||
The most notable features are HTTP/2, Virtual Hosts, TLS + SNI, and easy configuration with a [Caddyfile](https://caddyserver.com/docs/caddyfile). Usually, you have one Caddyfile per site. Most directives for the Caddyfile invoke a layer of middleware which can be [used in your own Go programs](https://github.com/mholt/caddy/wiki/Using-Caddy-Middleware-in-Your-Own-Programs).
|
||||
|
||||
[Download](https://github.com/mholt/caddy/releases) · [User Guide](https://caddyserver.com/docs)
|
||||
[Releases](https://github.com/mholt/caddy/releases) ·
|
||||
[User Guide](https://caddyserver.com/docs) ·
|
||||
[Community](https://forum.caddyserver.com)
|
||||
|
||||
|
||||
## Menu
|
||||
|
||||
|
||||
### Menu
|
||||
|
||||
- [Getting Caddy](#getting-caddy)
|
||||
- [Running from Source](#running-from-source)
|
||||
- [Features](#features)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Running from Source](#running-from-source)
|
||||
- [Running in Production](#running-in-production)
|
||||
- [Contributing](#contributing)
|
||||
- [About the Project](#about-the-project)
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
## Getting Caddy
|
||||
|
||||
Caddy binaries have no dependencies and are available for nearly every platform.
|
||||
|
||||
[Latest release](https://github.com/mholt/caddy/releases/latest)
|
||||
|
||||
|
||||
## Running from Source
|
||||
|
||||
Note: You will need **[Go 1.4](https://golang.org/dl)** or newer
|
||||
|
||||
1. `$ go get github.com/mholt/caddy`
|
||||
2. `cd` into your website's directory
|
||||
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
|
||||
|
||||
If you're tinkering, you can also use `go run main.go`.
|
||||
|
||||
By default, Caddy serves the current directory at [localhost:2015](http://localhost:2015). You can place a Caddyfile to configure Caddy for serving your site.
|
||||
|
||||
Caddy accepts some flags from the command line. Run `caddy -h` to view the help for flags. You can also pipe a Caddyfile into the caddy command.
|
||||
|
||||
**Running as root:** We advise against this; use setcap instead, like so: `setcap cap_net_bind_service=+ep ./caddy` This will allow you to listen on ports below 1024 (like 80 and 443).
|
||||
|
||||
|
||||
#### Docker Container
|
||||
|
||||
Caddy is available as a Docker container from any of these sources:
|
||||
|
||||
- [abiosoft/caddy](https://registry.hub.docker.com/u/abiosoft/caddy/)
|
||||
- [darron/caddy](https://registry.hub.docker.com/u/darron/caddy/)
|
||||
- [joshix/caddy](https://registry.hub.docker.com/u/joshix/caddy/)
|
||||
- [jumanjiman/caddy](https://registry.hub.docker.com/u/jumanjiman/caddy/)
|
||||
- [zenithar/nano-caddy](https://registry.hub.docker.com/u/zenithar/nano-caddy/)
|
||||
|
||||
|
||||
#### 3rd-party libraries
|
||||
|
||||
Although Caddy's binaries are completely static, Caddy relies on some excellent libraries. [Godoc.org](https://godoc.org/github.com/mholt/caddy) shows the packages that each Caddy package imports.
|
||||
|
||||
- **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)
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
The website has [full documentation](https://caddyserver.com/docs) but this will get you started in about 30 seconds:
|
||||
Caddy binaries have no dependencies and are available for every platform.
|
||||
Install Caddy any one of these ways:
|
||||
|
||||
Place a file named "Caddyfile" with your site. Paste this into it and save:
|
||||
- **[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`
|
||||
|
||||
```
|
||||
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).
|
||||
|
||||
To customize how your site is served, create a file named Caddyfile by your
|
||||
site and paste this into it:
|
||||
|
||||
```plain
|
||||
localhost
|
||||
|
||||
gzip
|
||||
browse
|
||||
ext .html
|
||||
websocket /echo cat
|
||||
log ../access.log
|
||||
ext .html
|
||||
log /var/log/access.log
|
||||
proxy /api 127.0.0.1:7005
|
||||
header /api Access-Control-Allow-Origin *
|
||||
```
|
||||
|
||||
Run `caddy` from that directory, and it will automatically use that Caddyfile to configure itself.
|
||||
When you run `caddy` in that directory, it will automatically find and use
|
||||
that Caddyfile to configure itself.
|
||||
|
||||
That simple file enables compression, allows directory browsing (for folders without an index file), serves clean URLs, hosts an echo server for WebSocket connections at /echo, logs accesses to access.log, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from some API.
|
||||
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.
|
||||
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
#### Defining multiple sites
|
||||
To host multiple sites and do more with the Caddyfile, please see the
|
||||
[Caddyfile documentation](https://caddyserver.com/docs/caddyfile).
|
||||
|
||||
You can run multiple sites from the same Caddyfile, too:
|
||||
Note that production sites are served over
|
||||
[HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
|
||||
```
|
||||
http://mysite.com,
|
||||
http://www.mysite.com {
|
||||
redir https://mysite.com
|
||||
}
|
||||
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.
|
||||
|
||||
https://mysite.com {
|
||||
tls mysite.crt mysite.key
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
Note that the secure host will automatically be served with HTTP/2 if the client supports it.
|
||||
|
||||
For more documentation, please view [the website](https://caddyserver.com/docs). You may also be interested in the [developer guide](https://github.com/mholt/caddy/wiki) on this project's GitHub wiki.
|
||||
**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`
|
||||
|
||||
|
||||
|
||||
## Running from Source
|
||||
|
||||
Note: You will need **[Go 1.6](https://golang.org/dl/)** or newer.
|
||||
|
||||
1. `go get github.com/mholt/caddy/caddy`
|
||||
2. `cd` into your website's directory
|
||||
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
|
||||
|
||||
Caddy's `main()` is in the caddy subfolder. To recompile Caddy, use
|
||||
`build.bash` found in that folder.
|
||||
|
||||
|
||||
|
||||
## Running in Production
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
|
||||
**[Join our community](https://forum.caddyserver.com) where you can chat with
|
||||
other Caddy users and developers!**
|
||||
|
||||
This project would not be what it is without your help. Please see the [contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md) if you haven't already.
|
||||
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).
|
||||
|
||||
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)!
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
Special thanks to [](https://www.digitalocean.com) for hosting the Caddy project.
|
||||
Special thanks to
|
||||
[](https://www.digitalocean.com)
|
||||
for hosting the Caddy project.
|
||||
|
||||
|
||||
|
||||
## About the Project
|
||||
|
||||
## About the project
|
||||
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 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, lighttpd, Websocketd, and Vagrant, and provides a pleasant mixture of features from each of them.
|
||||
**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).
|
||||
|
||||
|
||||
*Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
// Package app holds application-global state to make it accessible
|
||||
// by other packages in the application.
|
||||
//
|
||||
// This package differs from config in that the things in app aren't
|
||||
// really related to server configuration.
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// Name is the program name
|
||||
Name = "Caddy"
|
||||
|
||||
// Version is the program version
|
||||
Version = "0.7.6"
|
||||
)
|
||||
|
||||
var (
|
||||
// Servers is a list of all the currently-listening servers
|
||||
Servers []*server.Server
|
||||
|
||||
// ServersMutex protects the Servers slice during changes
|
||||
ServersMutex sync.Mutex
|
||||
|
||||
// Wg is used to wait for all servers to shut down
|
||||
Wg sync.WaitGroup
|
||||
|
||||
// Http2 indicates whether HTTP2 is enabled or not
|
||||
Http2 bool // TODO: temporary flag until http2 is standard
|
||||
|
||||
// Quiet mode hides non-error initialization output
|
||||
Quiet bool
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
CGO_ENABLED: 0
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.2.windows-amd64.zip
|
||||
- 7z x go1.6.2.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 ./...
|
||||
- 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", "")
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
// Package caddy implements the Caddy server manager.
|
||||
//
|
||||
// To use this package:
|
||||
//
|
||||
// 1. Set the AppName and AppVersion variables.
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile.
|
||||
// Pass in the name of the server type (like "http").
|
||||
// 3. Call caddy.Start() to start Caddy. You get back
|
||||
// an Instance, on which you can call Restart() to
|
||||
// restart it or Stop() to stop it.
|
||||
//
|
||||
// You should call Wait() on your instance to wait for
|
||||
// all servers to quit before your process exits.
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// AppName is the name of the application.
|
||||
AppName string
|
||||
|
||||
// AppVersion is the version of the application.
|
||||
AppVersion string
|
||||
|
||||
// Quiet mode will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
|
||||
// PidFile is the path to the pidfile to create.
|
||||
PidFile string
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
|
||||
// isUpgrade will be set to true if this process
|
||||
// was started as part of an upgrade, where a parent
|
||||
// Caddy process started this one.
|
||||
isUpgrade bool
|
||||
)
|
||||
|
||||
// Instance contains the state of servers created as a result of
|
||||
// calling Start and can be used to access or control those servers.
|
||||
type Instance struct {
|
||||
// serverType is the name of the instance's server type
|
||||
serverType string
|
||||
|
||||
// caddyfileInput is the input configuration text used for this process
|
||||
caddyfileInput Input
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg *sync.WaitGroup
|
||||
|
||||
// context is the context created for this instance.
|
||||
context Context
|
||||
|
||||
// servers is the list of servers with their listeners.
|
||||
servers []serverListener
|
||||
|
||||
// these callbacks execute when certain events occur
|
||||
onFirstStartup []func() error // starting, not as part of a restart
|
||||
onStartup []func() error // starting, even as part of a restart
|
||||
onRestart []func() error // before restart commences
|
||||
onShutdown []func() error // stopping, even as part of a restart
|
||||
onFinalShutdown []func() error // stopping, not as part of a restart
|
||||
}
|
||||
|
||||
// Stop stops all servers contained in i. It does NOT
|
||||
// execute shutdown callbacks.
|
||||
func (i *Instance) Stop() error {
|
||||
// stop the servers
|
||||
for _, s := range i.servers {
|
||||
if gs, ok := s.server.(GracefulServer); ok {
|
||||
if err := gs.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", gs.Address(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splice i out of instance list, causing it to be garbage-collected
|
||||
instancesMu.Lock()
|
||||
for j, other := range instances {
|
||||
if other == i {
|
||||
instances = append(instances[:j], instances[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
instancesMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShutdownCallbacks executes all the shutdown callbacks of i,
|
||||
// including ones that are scheduled only for the final shutdown
|
||||
// of i. An error returned from one does not stop execution of
|
||||
// the rest. All the non-nil errors will be returned.
|
||||
func (i *Instance) ShutdownCallbacks() []error {
|
||||
var errs []error
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
err := shutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, finalShutdownFunc := range i.onFinalShutdown {
|
||||
err := finalShutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Restart replaces the servers in i with new servers created from
|
||||
// executing the newCaddyfile. Upon success, it returns the new
|
||||
// instance to replace i. Upon failure, i will not be replaced.
|
||||
func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
log.Println("[INFO] Reloading")
|
||||
|
||||
i.wg.Add(1)
|
||||
defer i.wg.Done()
|
||||
|
||||
// run restart callbacks
|
||||
for _, fn := range i.onRestart {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
if newCaddyfile == nil {
|
||||
newCaddyfile = i.caddyfileInput
|
||||
}
|
||||
|
||||
// Add file descriptors of all the sockets that are capable of it
|
||||
restartFds := make(map[string]restartTriple)
|
||||
for _, s := range i.servers {
|
||||
gs, srvOk := s.server.(GracefulServer)
|
||||
ln, lnOk := s.listener.(Listener)
|
||||
pc, pcOk := s.packet.(PacketConn)
|
||||
if srvOk {
|
||||
if lnOk && pcOk {
|
||||
restartFds[gs.Address()] = restartTriple{server: gs, listener: ln, packet: pc}
|
||||
continue
|
||||
}
|
||||
if lnOk {
|
||||
restartFds[gs.Address()] = restartTriple{server: gs, listener: ln}
|
||||
continue
|
||||
}
|
||||
if pcOk {
|
||||
restartFds[gs.Address()] = restartTriple{server: gs, packet: pc}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create new instance; if the restart fails, it is simply discarded
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg}
|
||||
|
||||
// attempt to start new instance
|
||||
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
// success! stop the old instance
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
err := shutdownFunc()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
i.Stop()
|
||||
|
||||
log.Println("[INFO] Reloading complete")
|
||||
|
||||
return newInst, nil
|
||||
}
|
||||
|
||||
// SaveServer adds s and its associated listener ln to the
|
||||
// internally-kept list of servers that is running. For
|
||||
// saved servers, graceful restarts will be provided.
|
||||
func (i *Instance) SaveServer(s Server, ln net.Listener) {
|
||||
i.servers = append(i.servers, serverListener{server: s, listener: ln})
|
||||
}
|
||||
|
||||
// HasListenerWithAddress returns whether this package is
|
||||
// tracking a server using a listener with the address
|
||||
// addr.
|
||||
func HasListenerWithAddress(addr string) bool {
|
||||
instancesMu.Lock()
|
||||
defer instancesMu.Unlock()
|
||||
for _, inst := range instances {
|
||||
for _, sln := range inst.servers {
|
||||
if listenerAddrEqual(sln.listener, addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listenerAddrEqual compares a listener's address with
|
||||
// addr. Extra care is taken to match addresses with an
|
||||
// empty hostname portion, as listeners tend to report
|
||||
// [::]:80, for example, when the matching address that
|
||||
// created the listener might be simply :80.
|
||||
func listenerAddrEqual(ln net.Listener, addr string) bool {
|
||||
lnAddr := ln.Addr().String()
|
||||
hostname, port, err := net.SplitHostPort(addr)
|
||||
if err != nil || hostname != "" {
|
||||
return lnAddr == addr
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("::", port) {
|
||||
return true
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TCPServer is a type that can listen and serve connections.
|
||||
// A TCPServer must associate with exactly zero or one net.Listeners.
|
||||
type TCPServer interface {
|
||||
// Listen starts listening by creating a new listener
|
||||
// and returning it. It does not start accepting
|
||||
// connections. For UDP-only servers, this method
|
||||
// can be a no-op that returns (nil, nil).
|
||||
Listen() (net.Listener, error)
|
||||
|
||||
// Serve starts serving using the provided listener.
|
||||
// Serve must start the server loop nearly immediately,
|
||||
// or at least not return any errors before the server
|
||||
// loop begins. Serve blocks indefinitely, or in other
|
||||
// words, until the server is stopped. For UDP-only
|
||||
// servers, this method can be a no-op that returns nil.
|
||||
Serve(net.Listener) error
|
||||
}
|
||||
|
||||
// UDPServer is a type that can listen and serve packets.
|
||||
// A UDPServer must associate with exactly zero or one net.PacketConns.
|
||||
type UDPServer interface {
|
||||
// ListenPacket starts listening by creating a new packetconn
|
||||
// and returning it. It does not start accepting connections.
|
||||
// TCP-only servers may leave this method blank and return
|
||||
// (nil, nil).
|
||||
ListenPacket() (net.PacketConn, error)
|
||||
|
||||
// ServePacket starts serving using the provided packetconn.
|
||||
// ServePacket must start the server loop nearly immediately,
|
||||
// or at least not return any errors before the server
|
||||
// loop begins. ServePacket blocks indefinitely, or in other
|
||||
// words, until the server is stopped. For TCP-only servers,
|
||||
// this method can be a no-op that returns nil.
|
||||
ServePacket(net.PacketConn) error
|
||||
}
|
||||
|
||||
// Server is a type that can listen and serve. It supports both
|
||||
// TCP and UDP, although the UDPServer interface can be used
|
||||
// for more than just UDP.
|
||||
//
|
||||
// If the server uses TCP, it should implement TCPServer completely.
|
||||
// If it uses UDP or some other protocol, it should implement
|
||||
// UDPServer completely. If it uses both, both interfaces should be
|
||||
// fully implemented. Any unimplemented methods should be made as
|
||||
// no-ops that simply return nil values.
|
||||
type Server interface {
|
||||
TCPServer
|
||||
UDPServer
|
||||
}
|
||||
|
||||
// Stopper is a type that can stop serving. The stop
|
||||
// does not necessarily have to be graceful.
|
||||
type Stopper interface {
|
||||
// Stop stops the server. It blocks until the
|
||||
// server is completely stopped.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// GracefulServer is a Server and Stopper, the stopping
|
||||
// of which is graceful (whatever that means for the kind
|
||||
// of server being implemented). It must be able to return
|
||||
// the address it is configured to listen on so that its
|
||||
// listener can be paired with it upon graceful restarts.
|
||||
// The net.Listener that a GracefulServer creates must
|
||||
// implement the Listener interface for restarts to be
|
||||
// graceful (assuming the listener is for TCP).
|
||||
type GracefulServer interface {
|
||||
Server
|
||||
Stopper
|
||||
|
||||
// Address returns the address the server should
|
||||
// listen on; it is used to pair the server to
|
||||
// its listener during a graceful/zero-downtime
|
||||
// restart. Thus when implementing this method,
|
||||
// you must not access a listener to get the
|
||||
// address; you must store the address the
|
||||
// server is to serve on some other way.
|
||||
Address() string
|
||||
}
|
||||
|
||||
// Listener is a net.Listener with an underlying file descriptor.
|
||||
// A server's listener should implement this interface if it is
|
||||
// to support zero-downtime reloads.
|
||||
type Listener interface {
|
||||
net.Listener
|
||||
File() (*os.File, error)
|
||||
}
|
||||
|
||||
// PacketConn is a net.PacketConn with an underlying file descriptor.
|
||||
// A server's packetconn should implement this interface if it is
|
||||
// to support zero-downtime reloads (in sofar this holds true for datagram
|
||||
// connections).
|
||||
type PacketConn interface {
|
||||
net.PacketConn
|
||||
File() (*os.File, error)
|
||||
}
|
||||
|
||||
// AfterStartup is an interface that can be implemented
|
||||
// by a server type that wants to run some code after all
|
||||
// servers for the same Instance have started.
|
||||
type AfterStartup interface {
|
||||
OnStartupComplete()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile by calling the plugged in
|
||||
// Caddyfile loader methods. An error is returned if more than
|
||||
// one loader returns a non-nil Caddyfile input. If no loaders
|
||||
// load a Caddyfile, the default loader is used. If no default
|
||||
// loader is registered or it returns nil, the server type's
|
||||
// default Caddyfile is loaded. If the server type does not
|
||||
// specify any default Caddyfile value, then an empty Caddyfile
|
||||
// is returned. Consequently, this function never returns a nil
|
||||
// value as long as there are no errors.
|
||||
func LoadCaddyfile(serverType string) (Input, error) {
|
||||
// Ask plugged-in loaders for a Caddyfile
|
||||
cdyfile, err := loadCaddyfileInput(serverType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput(serverType)
|
||||
}
|
||||
|
||||
// Still nil? Geez.
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{ServerTypeName: serverType}
|
||||
}
|
||||
|
||||
return cdyfile, nil
|
||||
}
|
||||
|
||||
// Wait blocks until all of i's servers have stopped.
|
||||
func (i *Instance) Wait() {
|
||||
i.wg.Wait()
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// NOTE: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the Caddyfile used to create i.
|
||||
func (i *Instance) Caddyfile() Input {
|
||||
return i.caddyfileInput
|
||||
}
|
||||
|
||||
// Start starts Caddy with the given Caddyfile.
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
func Start(cdyfile Input) (*Instance, error) {
|
||||
writePidFile()
|
||||
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
|
||||
return inst, startWithListenerFds(cdyfile, inst, nil)
|
||||
}
|
||||
|
||||
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{}
|
||||
}
|
||||
|
||||
stypeName := cdyfile.ServerType()
|
||||
|
||||
stype, err := getServerType(stypeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inst.caddyfileInput = cdyfile
|
||||
|
||||
sblocks, err := loadServerBlocks(stypeName, cdyfile.Path(), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inst.context = stype.NewContext()
|
||||
if inst.context == nil {
|
||||
return fmt.Errorf("server type %s produced a nil Context", stypeName)
|
||||
}
|
||||
|
||||
sblocks, err = inst.context.InspectServerBlocks(cdyfile.Path(), sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = executeDirectives(inst, cdyfile.Path(), stype.Directives, sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slist, err := inst.context.MakeServers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run startup callbacks
|
||||
if restartFds == nil {
|
||||
for _, firstStartupFunc := range inst.onFirstStartup {
|
||||
err := firstStartupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, startupFunc := range inst.onStartup {
|
||||
err := startupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = startServers(slist, inst, restartFds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instancesMu.Lock()
|
||||
instances = append(instances, inst)
|
||||
instancesMu.Unlock()
|
||||
|
||||
// run any AfterStartup callbacks if this is not
|
||||
// part of a restart; then show file descriptor notice
|
||||
if restartFds == nil {
|
||||
for _, srvln := range inst.servers {
|
||||
if srv, ok := srvln.server.(AfterStartup); ok {
|
||||
srv.OnStartupComplete()
|
||||
}
|
||||
}
|
||||
if !Quiet {
|
||||
for _, srvln := range inst.servers {
|
||||
if !IsLoopback(srvln.listener.Addr().String()) {
|
||||
checkFdlimit()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDirectives(inst *Instance, filename string,
|
||||
directives []string, sblocks []caddyfile.ServerBlock) error {
|
||||
|
||||
// map of server block ID to map of directive name to whatever.
|
||||
storages := make(map[int]map[string]interface{})
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
// We loop with the directives on the outer loop so we execute
|
||||
// a directive for all server blocks before going to the next directive.
|
||||
// This is important mainly due to the parsing callbacks (below).
|
||||
for _, dir := range directives {
|
||||
for i, sb := range sblocks {
|
||||
var once sync.Once
|
||||
if _, ok := storages[i]; !ok {
|
||||
storages[i] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for j, key := range sb.Keys {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir]; ok {
|
||||
controller := &Controller{
|
||||
instance: inst,
|
||||
Key: key,
|
||||
Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockKeyIndex: j,
|
||||
ServerBlockKeys: sb.Keys,
|
||||
ServerBlockStorage: storages[i][dir],
|
||||
}
|
||||
|
||||
setup, err := DirectiveAction(inst.serverType, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setup(controller)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storages[i][dir] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See if there are any callbacks to execute after this directive
|
||||
if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok {
|
||||
callbacks := allCallbacks[dir]
|
||||
for _, callback := range callbacks {
|
||||
if err := callback(inst.context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
|
||||
errChan := make(chan error, len(serverList))
|
||||
|
||||
for _, s := range serverList {
|
||||
var (
|
||||
ln net.Listener
|
||||
pc net.PacketConn
|
||||
err error
|
||||
)
|
||||
|
||||
// If this is a reload and s is a GracefulServer,
|
||||
// reuse the listener for a graceful restart.
|
||||
if gs, ok := s.(GracefulServer); ok && restartFds != nil {
|
||||
addr := gs.Address()
|
||||
if old, ok := restartFds[addr]; ok {
|
||||
// listener
|
||||
if old.listener != nil {
|
||||
file, err := old.listener.File()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
// packetconn
|
||||
if old.packet != nil {
|
||||
file, err := old.packet.File()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pc, err = net.FilePacketConn(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ln == nil {
|
||||
ln, err = s.Listen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if pc == nil {
|
||||
pc, err = s.ListenPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
inst.wg.Add(2)
|
||||
go func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
|
||||
defer inst.wg.Done()
|
||||
|
||||
go func() {
|
||||
errChan <- s.Serve(ln)
|
||||
defer inst.wg.Done()
|
||||
}()
|
||||
errChan <- s.ServePacket(pc)
|
||||
}(s, ln, pc, inst)
|
||||
|
||||
inst.servers = append(inst.servers, serverListener{server: s, listener: ln, packet: pc})
|
||||
}
|
||||
|
||||
// Log errors that may be returned from Serve() calls,
|
||||
// these errors should only be occurring in the server loop.
|
||||
go func() {
|
||||
for err := range errChan {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener
|
||||
continue
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServerType(serverType string) (ServerType, error) {
|
||||
stype, ok := serverTypes[serverType]
|
||||
if ok {
|
||||
return stype, nil
|
||||
}
|
||||
if len(serverTypes) == 0 {
|
||||
return ServerType{}, fmt.Errorf("no server types plugged in")
|
||||
}
|
||||
if serverType == "" {
|
||||
if len(serverTypes) == 1 {
|
||||
for _, stype := range serverTypes {
|
||||
return stype, nil
|
||||
}
|
||||
}
|
||||
return ServerType{}, fmt.Errorf("multiple server types available; must choose one")
|
||||
}
|
||||
return ServerType{}, fmt.Errorf("unknown server type '%s'", serverType)
|
||||
}
|
||||
|
||||
func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) {
|
||||
validDirectives := ValidDirectives(serverType)
|
||||
serverBlocks, err := caddyfile.Parse(filename, input, validDirectives)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(serverBlocks) == 0 && serverTypes[serverType].DefaultInput != nil {
|
||||
newInput := serverTypes[serverType].DefaultInput()
|
||||
serverBlocks, err = caddyfile.Parse(newInput.Path(),
|
||||
bytes.NewReader(newInput.Body()), validDirectives)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// Stop stops ALL servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks, and it deletes all
|
||||
// instances after stopping is completed. Do not re-use any
|
||||
// references to old instances after calling Stop.
|
||||
func Stop() error {
|
||||
instancesMu.Lock()
|
||||
for _, inst := range instances {
|
||||
if err := inst.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
|
||||
}
|
||||
}
|
||||
instances = []*Instance{}
|
||||
instancesMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLoopback returns true if the hostname of addr looks
|
||||
// explicitly like a common local hostname. addr must only
|
||||
// be a host or a host:port combination.
|
||||
func IsLoopback(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname
|
||||
}
|
||||
return host == "localhost" ||
|
||||
strings.Trim(host, "[]") == "::1" ||
|
||||
strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// checkFdlimit issues a warning if the OS limit for
|
||||
// max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 8192
|
||||
|
||||
// Warn if ulimit is too low for production sites
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
|
||||
if err == nil {
|
||||
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
|
||||
if err == nil && lim < min {
|
||||
fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+
|
||||
"At least %d is recommended. Fix with \"ulimit -n %d\".\n", lim, min, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade re-launches the process, preserving the listeners
|
||||
// for a graceful restart. It does NOT load new configuration;
|
||||
// it only starts the process anew with a fresh binary.
|
||||
//
|
||||
// TODO: This is not yet implemented
|
||||
func Upgrade() error {
|
||||
return fmt.Errorf("not implemented")
|
||||
// TODO: have child process set isUpgrade = true
|
||||
}
|
||||
|
||||
// IsUpgrade returns true if this process is part of an upgrade
|
||||
// where a parent caddy process spawned this one to ugprade
|
||||
// the binary.
|
||||
func IsUpgrade() bool {
|
||||
return isUpgrade
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
ServerTypeName string
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// ServerType returns c.ServerType.
|
||||
func (c CaddyfileInput) ServerType() string { return c.ServerTypeName }
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// The type of server this input is intended for
|
||||
ServerType() string
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
// It uses the default host and port (depends on
|
||||
// host, e.g. localhost is 2015, otherwise 443) and
|
||||
// root.
|
||||
func DefaultInput(serverType string) Input {
|
||||
if _, ok := serverTypes[serverType]; !ok {
|
||||
return nil
|
||||
}
|
||||
if serverTypes[serverType].DefaultInput == nil {
|
||||
return nil
|
||||
}
|
||||
return serverTypes[serverType].DefaultInput()
|
||||
}
|
||||
|
||||
// writePidFile writes the process ID to the file at PidFile.
|
||||
// It does nothing if PidFile is not set.
|
||||
func writePidFile() error {
|
||||
if PidFile == "" {
|
||||
return nil
|
||||
}
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
return ioutil.WriteFile(PidFile, pid, 0644)
|
||||
}
|
||||
|
||||
type restartTriple struct {
|
||||
server GracefulServer
|
||||
listener Listener
|
||||
packet PacketConn
|
||||
}
|
||||
|
||||
var (
|
||||
// instances is the list of running Instances.
|
||||
instances []*Instance
|
||||
|
||||
// instancesMu protects instances.
|
||||
instancesMu sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
||||
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,288 @@
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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.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.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")
|
||||
|
||||
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
|
||||
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
|
||||
}
|
||||
|
||||
// Run is Caddy's main() function.
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
moveStorage() // TODO: This is temporary for the 0.9 release, or until most users upgrade to 0.9+
|
||||
|
||||
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 {
|
||||
log.Fatal(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 {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Get Caddyfile input
|
||||
caddyfile, err := caddy.LoadCaddyfile(serverType)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(caddyfile)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Twiddle your thumbs
|
||||
instance.Wait()
|
||||
}
|
||||
|
||||
// mustLogFatal wraps log.Fatal() 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 mustLogFatal(args ...interface{}) {
|
||||
if !caddy.IsUpgrade() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
log.Fatal(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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// moveStorage moves the old certificate storage location by
|
||||
// renaming the "letsencrypt" folder to the hostname of the
|
||||
// CA URL. This is TEMPORARY until most users have upgraded to 0.9+.
|
||||
func moveStorage() {
|
||||
oldPath := filepath.Join(caddy.AssetsPath(), "letsencrypt")
|
||||
_, err := os.Stat(oldPath)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
// Just use a default config to get default (file) storage
|
||||
fileStorage, err := new(caddytls.Config).StorageFor(caddytls.DefaultCAUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to get new path for certificate storage: %v", err)
|
||||
}
|
||||
newPath := string(fileStorage.(caddytls.FileStorage))
|
||||
err = os.MkdirAll(string(newPath), 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to make new certificate storage path: %v\n\nPlease follow instructions at:\nhttps://github.com/mholt/caddy/issues/902#issuecomment-228876011", err)
|
||||
}
|
||||
err = os.Rename(oldPath, string(newPath))
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to migrate certificate storage: %v\n\nPlease follow instructions at:\nhttps://github.com/mholt/caddy/issues/902#issuecomment-228876011", err)
|
||||
}
|
||||
// convert mixed case folder and file names to lowercase
|
||||
var done bool // walking is recursive and preloads the file names, so we must restart walk after a change until no changes
|
||||
for !done {
|
||||
done = true
|
||||
filepath.Walk(string(newPath), func(path string, info os.FileInfo, err error) error {
|
||||
// must be careful to only lowercase the base of the path, not the whole thing!!
|
||||
base := filepath.Base(path)
|
||||
if lowerBase := strings.ToLower(base); base != lowerBase {
|
||||
lowerPath := filepath.Join(filepath.Dir(path), lowerBase)
|
||||
err = os.Rename(path, lowerPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to lower-case: %v\n\nPlease follow instructions at:\nhttps://github.com/mholt/caddy/issues/902#issuecomment-228876011", err)
|
||||
}
|
||||
// terminate traversal and restart since Walk needs the updated file list with new file names
|
||||
done = false
|
||||
return errors.New("start over")
|
||||
}
|
||||
return 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
|
||||
)
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package caddy
|
||||
|
||||
import "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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// some really convenient methods.
|
||||
type Dispenser struct {
|
||||
filename string
|
||||
tokens []token
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func NewDispenser(filename string, input io.Reader) Dispenser {
|
||||
}
|
||||
|
||||
// NewDispenserTokens returns a Dispenser filled with the given tokens.
|
||||
func NewDispenserTokens(filename string, tokens []token) Dispenser {
|
||||
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
@@ -59,8 +59,8 @@ func (d *Dispenser) NextArg() bool {
|
||||
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.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
|
||||
}
|
||||
@@ -80,8 +80,8 @@ func (d *Dispenser) NextLine() bool {
|
||||
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.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
|
||||
}
|
||||
@@ -119,6 +119,7 @@ func (d *Dispenser) NextBlock() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IncrNest adds a level of nesting to the dispenser.
|
||||
func (d *Dispenser) IncrNest() {
|
||||
d.nesting++
|
||||
return
|
||||
@@ -130,7 +131,7 @@ func (d *Dispenser) Val() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].text
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token. If there is no token
|
||||
@@ -139,7 +140,7 @@ func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].line
|
||||
return d.tokens[d.cursor].Line
|
||||
}
|
||||
|
||||
// File gets the filename of the current token. If there is no token loaded,
|
||||
@@ -148,7 +149,7 @@ 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 != "" {
|
||||
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
|
||||
return tokenFilename
|
||||
}
|
||||
return d.filename
|
||||
@@ -208,9 +209,9 @@ func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EofErr returns an EOF error, meaning that end of input
|
||||
// was found when another token was expected.
|
||||
func (d *Dispenser) EofErr() error {
|
||||
// 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")
|
||||
}
|
||||
|
||||
@@ -232,7 +233,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].text, "\n")
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
@@ -245,6 +246,6 @@ func (d *Dispenser) isNewLine() bool {
|
||||
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
|
||||
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,4 +1,4 @@
|
||||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -13,15 +13,15 @@ type (
|
||||
// in quotes if it contains whitespace.
|
||||
lexer struct {
|
||||
reader *bufio.Reader
|
||||
token token
|
||||
token Token
|
||||
line int
|
||||
}
|
||||
|
||||
// token represents a single parsable unit.
|
||||
token struct {
|
||||
file string
|
||||
line int
|
||||
text string
|
||||
// Token represents a single parsable unit.
|
||||
Token struct {
|
||||
File string
|
||||
Line int
|
||||
Text string
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ func (l *lexer) next() bool {
|
||||
var comment, quoted, escaped bool
|
||||
|
||||
makeToken := func() bool {
|
||||
l.token.text = string(val)
|
||||
l.token.Text = string(val)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (l *lexer) next() bool {
|
||||
}
|
||||
|
||||
if len(val) == 0 {
|
||||
l.token = token{line: l.line}
|
||||
l.token = Token{Line: l.line}
|
||||
if ch == '"' {
|
||||
quoted = true
|
||||
continue
|
||||
@@ -0,0 +1,165 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexerTestCase struct {
|
||||
input string
|
||||
expected []Token
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testCases := []lexerTestCase{
|
||||
{
|
||||
input: `host:123`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123
|
||||
|
||||
directive`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 3, Text: "directive"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
directive
|
||||
}`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 2, Text: "directive"},
|
||||
{Line: 3, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 { directive }`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 1, Text: "directive"},
|
||||
{Line: 1, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
#comment
|
||||
directive
|
||||
# comment
|
||||
foobar # another comment
|
||||
}`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 3, Text: "directive"},
|
||||
{Line: 5, Text: "foobar"},
|
||||
{Line: 6, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `a "quoted value" b
|
||||
foobar`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "a"},
|
||||
{Line: 1, Text: "quoted value"},
|
||||
{Line: 1, Text: "b"},
|
||||
{Line: 2, Text: "foobar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted \"value\" inside" B`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "A"},
|
||||
{Line: 1, Text: `quoted "value" inside`},
|
||||
{Line: 1, Text: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `don't\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\\escape"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
break inside" {
|
||||
foobar
|
||||
}`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "A"},
|
||||
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||
{Line: 2, Text: "{"},
|
||||
{Line: 3, Text: "foobar"},
|
||||
{Line: 4, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"C:\php\php-cgi.exe"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `C:\php\php-cgi.exe`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `empty "" string`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `empty`},
|
||||
{Line: 1, Text: ``},
|
||||
{Line: 1, Text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "skip those\r\nCR characters",
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "skip"},
|
||||
{Line: 1, Text: "those"},
|
||||
{Line: 2, Text: "CR"},
|
||||
{Line: 2, Text: "characters"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := tokenize(testCase.input)
|
||||
lexerCompare(t, i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(input string) (tokens []Token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||
}
|
||||
|
||||
for i := 0; i < len(actual) && i < len(expected); i++ {
|
||||
if actual[i].Line != expected[i].Line {
|
||||
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
||||
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
|
||||
break
|
||||
}
|
||||
if actual[i].Text != expected[i].Text {
|
||||
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
|
||||
n, i, expected[i].Text, actual[i].Text)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
// order, by server block. No further parsing is performed.
|
||||
// Server blocks are returned in the order in which they appear.
|
||||
// Directives that do not appear in validDirectives will cause
|
||||
// an error. If you do not want to check for valid directives,
|
||||
// pass in nil instead.
|
||||
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []Token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
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
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
var blocks []ServerBlock
|
||||
|
||||
for p.Next() {
|
||||
err := p.parseOne()
|
||||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Keys) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseOne() error {
|
||||
p.block = ServerBlock{Tokens: make(map[string][]Token)}
|
||||
|
||||
err := p.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) begin() error {
|
||||
if len(p.tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := p.addresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.eof {
|
||||
// this happens if the Caddyfile consists of only
|
||||
// a line of addresses and nothing else
|
||||
return nil
|
||||
}
|
||||
|
||||
err = p.blockContents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) addresses() error {
|
||||
var expectingAnother bool
|
||||
|
||||
for {
|
||||
tkn := replaceEnvVars(p.Val())
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if tkn == "import" && p.isNewLine() {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Open brace definitely indicates end of addresses
|
||||
if tkn == "{" {
|
||||
if expectingAnother {
|
||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed ""
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
if tkn[len(tkn)-1] == ',' {
|
||||
tkn = tkn[:len(tkn)-1]
|
||||
expectingAnother = true
|
||||
} else {
|
||||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
p.block.Keys = append(p.block.Keys, tkn)
|
||||
}
|
||||
|
||||
// Advance token and possibly break out of loop or return error
|
||||
hasNext := p.Next()
|
||||
if expectingAnother && !hasNext {
|
||||
return p.EOFErr()
|
||||
}
|
||||
if !hasNext {
|
||||
p.eof = true
|
||||
break // EOF
|
||||
}
|
||||
if !expectingAnother && p.isNewLine() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) blockContents() error {
|
||||
errOpenCurlyBrace := p.openCurlyBrace()
|
||||
if errOpenCurlyBrace != nil {
|
||||
// single-server configs don't need curly braces
|
||||
p.cursor--
|
||||
}
|
||||
|
||||
err := p.directives()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only look for close curly brace if there was an opening
|
||||
if errOpenCurlyBrace == nil {
|
||||
err = p.closeCurlyBrace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// directives parses through all the lines for directives
|
||||
// and it expects the next token to be the first
|
||||
// directive. It goes until EOF or closing curly brace
|
||||
// which ends the server block.
|
||||
func (p *parser) directives() error {
|
||||
for p.Next() {
|
||||
// end of server block
|
||||
if p.Val() == "}" {
|
||||
break
|
||||
}
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if p.Val() == "import" {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
|
||||
// normal case: parse a directive on this line
|
||||
if err := p.directive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// doImport swaps out the import directive and its argument
|
||||
// (a total of 2 tokens) with the tokens in the specified file
|
||||
// or globbing pattern. When the function returns, the cursor
|
||||
// is on the token before where the import directive was. In
|
||||
// other words, call Next() to access the first token that was
|
||||
// imported.
|
||||
func (p *parser) doImport() error {
|
||||
// syntax check
|
||||
if !p.NextArg() {
|
||||
return p.ArgErr()
|
||||
}
|
||||
importPattern := p.Val()
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return p.Errf("No files matching import pattern %s", importPattern)
|
||||
}
|
||||
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
|
||||
// collect all the imported tokens
|
||||
var importedTokens []Token
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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(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,
|
||||
}
|
||||
}
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
// and rewind cursor so Next() will land on first imported token
|
||||
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
|
||||
p.cursor--
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doSingleImport lexes the individual file at importFile and returns
|
||||
// its tokens or an error, if any.
|
||||
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
file, err := os.Open(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
||||
}
|
||||
defer file.Close()
|
||||
importedTokens := allTokens(file)
|
||||
|
||||
// Tack the filename onto these tokens so errors show the imported file's name
|
||||
filename := filepath.Base(importFile)
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].File = filename
|
||||
}
|
||||
|
||||
return importedTokens, nil
|
||||
}
|
||||
|
||||
// directive collects tokens until the directive's scope
|
||||
// closes (either end of line or end of curly brace block).
|
||||
// It expects the currently-loaded token to be a directive
|
||||
// (or } that ends a server block). The collected tokens
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := p.Val()
|
||||
nesting := 0
|
||||
|
||||
// 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
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
nesting++
|
||||
} else if p.isNewLine() && nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
} else if p.Val() == "}" && nesting > 0 {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
}
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
}
|
||||
|
||||
if nesting > 0 {
|
||||
return p.EOFErr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openCurlyBrace expects the current token to be an
|
||||
// opening curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a opening curly brace. It does NOT advance the token.
|
||||
func (p *parser) openCurlyBrace() error {
|
||||
if p.Val() != "{" {
|
||||
return p.SyntaxErr("{")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeCurlyBrace expects the current token to be
|
||||
// a closing curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a closing curly brace. It does NOT advance the token.
|
||||
func (p *parser) closeCurlyBrace() error {
|
||||
if p.Val() != "}" {
|
||||
return p.SyntaxErr("}")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validDirective returns true if dir is in p.validDirectives.
|
||||
func (p *parser) validDirective(dir string) bool {
|
||||
if p.validDirectives == nil {
|
||||
return true
|
||||
}
|
||||
for _, d := range p.validDirectives {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces environment variables that appear in the token
|
||||
// and understands both the $UNIX and %WINDOWS% syntaxes.
|
||||
func replaceEnvVars(s string) string {
|
||||
s = replaceEnvReferences(s, "{%", "%}")
|
||||
s = replaceEnvReferences(s, "{$", "}")
|
||||
return s
|
||||
}
|
||||
|
||||
// replaceEnvReferences performs the actual replacement of env variables
|
||||
// in s, given the placeholder start and placeholder end strings.
|
||||
func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
index := strings.Index(s, refStart)
|
||||
for index != -1 {
|
||||
endIndex := strings.Index(s, refEnd)
|
||||
if endIndex != -1 {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
index = strings.Index(s, refStart)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ServerBlock associates any number of keys (usually addresses
|
||||
// of some sort) with tokens (grouped by directive name).
|
||||
type ServerBlock struct {
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].Text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(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
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys []string
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []string{
|
||||
"http://localhost",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []string{
|
||||
"host1:80",
|
||||
"http://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []string{
|
||||
"http://host1.com",
|
||||
"http://host2.com",
|
||||
"https://host3.com",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []string{
|
||||
"http://host1.com:1234",
|
||||
"https://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []string{
|
||||
"127.0.0.1",
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import testdata/import_test1.txt`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test2.txt`, false, []string{
|
||||
"host1",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
|
||||
|
||||
{`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []string{}, map[string]int{}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Keys) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d keys, got %d",
|
||||
i, len(test.keys), len(result.Keys))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Keys {
|
||||
if addr != test.keys[j] {
|
||||
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
|
||||
i, j, test.keys[j], addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys [][]string // keys per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]string{
|
||||
{"localhost"},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]string{
|
||||
{"localhost:1234"},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]string{
|
||||
{"localhost:1234"},
|
||||
{"localhost:2015"},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]string{
|
||||
{"localhost:1234", "http://host2"},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]string{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]string{
|
||||
{"http://host1.com", "http://host2.com"},
|
||||
{"https://host3.com", "https://host4.com"},
|
||||
}},
|
||||
|
||||
{`import testdata/import_glob*.txt`, false, [][]string{
|
||||
{"glob0.host0"},
|
||||
{"glob0.host1"},
|
||||
{"glob1.host0"},
|
||||
{"glob2.host0"},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.keys), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Keys) != len(test.keys[j]) {
|
||||
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
|
||||
i, len(test.keys[j]), j, len(block.Keys))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Keys {
|
||||
if addr != test.keys[j][k] {
|
||||
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
|
||||
i, j, k, test.keys[j][k], addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
|
||||
return p
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
glob0.host0 {
|
||||
dir2 arg1
|
||||
}
|
||||
|
||||
glob0.host1 {
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
glob1.host0 {
|
||||
dir1
|
||||
dir2 arg1
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
glob2.host0 {
|
||||
dir2 arg1
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
// Package basicauth implements HTTP Basic Authentication.
|
||||
// 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"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -13,7 +18,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// BasicAuth is middleware to protect resources with a username and password.
|
||||
@@ -22,20 +27,19 @@ import (
|
||||
// security of HTTP Basic Auth is disputed. Use discretion when deciding
|
||||
// what to protect with BasicAuth.
|
||||
type BasicAuth struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
SiteRoot string
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
var hasAuth bool
|
||||
var isAuthenticated bool
|
||||
|
||||
for _, rule := range a.Rules {
|
||||
for _, res := range rule.Resources {
|
||||
if !middleware.Path(r.URL.Path).Matches(res) {
|
||||
if !httpserver.Path(r.URL.Path).Matches(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -47,7 +51,6 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
if !ok ||
|
||||
username != rule.Username ||
|
||||
!rule.Password(password) {
|
||||
//subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -78,6 +81,7 @@ type Rule struct {
|
||||
Resources []string
|
||||
}
|
||||
|
||||
// PasswordMatcher determines whether a password matches a rule.
|
||||
type PasswordMatcher func(pw string) bool
|
||||
|
||||
var (
|
||||
@@ -85,6 +89,7 @@ var (
|
||||
htpasswordsMu sync.Mutex
|
||||
)
|
||||
|
||||
// GetHtpasswdMatcher matches password rules.
|
||||
func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) {
|
||||
filename = filepath.Join(siteRoot, filename)
|
||||
htpasswordsMu.Lock()
|
||||
@@ -137,8 +142,18 @@ func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
|
||||
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 {
|
||||
return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1
|
||||
pwHash := sha1.New()
|
||||
pwHash.Write([]byte(pw))
|
||||
pwSum := pwHash.Sum(nil)
|
||||
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
|
||||
rw := BasicAuth{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}},
|
||||
},
|
||||
@@ -66,7 +66,7 @@ func TestBasicAuth(t *testing.T) {
|
||||
|
||||
func TestMultipleOverlappingRules(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
|
||||
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
|
||||
@@ -124,18 +124,21 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
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()
|
||||
defer os.Remove(htfh.Name())
|
||||
|
||||
for i, username := range []string{"sha1", "md5"} {
|
||||
rule := Rule{Username: username, Resources: []string{"/testing"}}
|
||||
if rule.Password, err = GetHtpasswdMatcher(htfh.Name(), rule.Username, "/"); err != nil {
|
||||
|
||||
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 password=%v", i, rule.Username, rule.Password)
|
||||
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)
|
||||
}
|
||||
@@ -1,43 +1,54 @@
|
||||
package setup
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// BasicAuth configures a new BasicAuth middleware instance.
|
||||
func BasicAuth(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
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 nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
basic := basicauth.BasicAuth{Rules: rules}
|
||||
basic := BasicAuth{Rules: rules}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
basic.Next = next
|
||||
basic.SiteRoot = root
|
||||
return basic
|
||||
}, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
var rules []basicauth.Rule
|
||||
func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
var err error
|
||||
for c.Next() {
|
||||
var rule basicauth.Rule
|
||||
var rule Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule.Username = args[0]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -50,7 +61,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
case 3:
|
||||
rule.Resources = append(rule.Resources, args[0])
|
||||
rule.Username = args[1]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
|
||||
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:
|
||||
@@ -63,10 +74,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
|
||||
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
return basicauth.PlainMatcher(passw), nil
|
||||
return PlainMatcher(passw), nil
|
||||
}
|
||||
|
||||
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package setup
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,27 +7,28 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
c := NewTestController(`basicauth user pwd`)
|
||||
|
||||
mid, err := BasicAuth(c)
|
||||
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)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(basicauth.BasicAuth)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(BasicAuth)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
@@ -38,7 +39,7 @@ func TestBasicAuthParse(t *testing.T) {
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
var skipHtpassword bool
|
||||
htfh, err := ioutil.TempFile("", "basicauth-")
|
||||
htfh, err := ioutil.TempFile(".", "basicauth-")
|
||||
if err != nil {
|
||||
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
||||
skipHtpassword = true
|
||||
@@ -54,41 +55,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
input string
|
||||
shouldErr bool
|
||||
password string
|
||||
expected []basicauth.Rule
|
||||
expected []Rule
|
||||
}{
|
||||
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{`basicauth user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{`basicauth /resource user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
{`basicauth /res1 user1 pwd1
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []Rule{
|
||||
{Username: "user1", Resources: []string{"/res1"}},
|
||||
{Username: "user2", Resources: []string{"/res2"}},
|
||||
}},
|
||||
{`basicauth user`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth user`, true, "", []Rule{}},
|
||||
{`basicauth`, true, "", []Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []Rule{}},
|
||||
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
|
||||
{Username: "sha1"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := basicAuthParse(c)
|
||||
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)
|
||||
@@ -118,7 +118,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
}
|
||||
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)
|
||||
i, j, test.password, actualRule.Password(""))
|
||||
}
|
||||
|
||||
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
|
||||
@@ -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,432 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
Root http.FileSystem
|
||||
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
|
||||
}
|
||||
|
||||
// BreadcrumbMap returns l.Path where every element is a map
|
||||
// of URLs and path segment names.
|
||||
func (l Listing) BreadcrumbMap() map[string]string {
|
||||
result := map[string]string{}
|
||||
|
||||
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, part := range parts {
|
||||
if i == 0 && part == "" {
|
||||
// Leading slash (root)
|
||||
result["/"] = "/"
|
||||
continue
|
||||
}
|
||||
result[strings.Join(parts[:i+1], "/")] = part
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FileInfo is the info about a particular file or directory
|
||||
type FileInfo struct {
|
||||
IsDir bool
|
||||
Name string
|
||||
Size int64
|
||||
URL string
|
||||
ModTime time.Time
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
// 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 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 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
|
||||
if l.Items[i].IsDir {
|
||||
iSize = directoryOffset + iSize
|
||||
}
|
||||
if l.Items[j].IsDir {
|
||||
jSize = directoryOffset + jSize
|
||||
}
|
||||
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 "name":
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case "size":
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case "time":
|
||||
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 "name":
|
||||
sort.Sort(byName(l))
|
||||
case "size":
|
||||
sort.Sort(bySize(l))
|
||||
case "time":
|
||||
sort.Sort(byTime(l))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (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++
|
||||
}
|
||||
|
||||
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) {
|
||||
var bc *Config
|
||||
// See if there's a browse configuration to match the path
|
||||
for i := range b.Configs {
|
||||
if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
|
||||
bc = &b.Configs[i]
|
||||
goto inScope
|
||||
}
|
||||
}
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
inScope:
|
||||
|
||||
// Browse works on existing directories; delegate everything else
|
||||
requestedFilepath, err := bc.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, "/") {
|
||||
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return b.ServeListing(w, r, requestedFilepath, bc)
|
||||
}
|
||||
|
||||
func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string) (*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)
|
||||
|
||||
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 = "name"
|
||||
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
||||
sort = sortCookie.Value
|
||||
}
|
||||
case "name", "size", "type":
|
||||
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)
|
||||
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.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,355 @@
|
||||
package browse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseHTTPMethods(t *testing.T) {
|
||||
tmpl, err := template.ParseFiles("testdata/photos.tpl")
|
||||
if err != nil {
|
||||
t.Fatalf("An error occured 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",
|
||||
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 occured 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",
|
||||
Root: http.Dir("./testdata"),
|
||||
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="./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/",
|
||||
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(),
|
||||
})
|
||||
}
|
||||
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
|
||||
{"/", "", "", -1, false, listing.Items},
|
||||
//test case 2: limit is set to 1, orderBy and sortBy is default
|
||||
{"/?limit=1", "", "", 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", "", "", 100000000, false, listing.Items},
|
||||
//test case 4 : testing for negative limit
|
||||
{"/?limit=-1", "", "", -1, false, listing.Items},
|
||||
//test case 5 : testing with limit set to -1 and order set to descending
|
||||
{"/?limit=-1&order=desc", "", "desc", -1, false, listing.Items},
|
||||
//test case 6 : testing with limit set to 2 and order set to descending
|
||||
{"/?limit=2&order=desc", "", "desc", 2, false, listing.Items},
|
||||
//test case 7 : testing with limit set to 3 and order set to descending
|
||||
{"/?limit=3&order=desc", "", "desc", 3, false, listing.Items},
|
||||
//test case 8 : testing with limit set to 3 and order set to ascending
|
||||
{"/?limit=3&order=asc", "", "asc", 3, false, listing.Items},
|
||||
//test case 9 : testing with limit set to 1111111 and order set to ascending
|
||||
{"/?limit=1111111&order=asc", "", "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", "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", "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", "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", "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", "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 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)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
code, err := b.ServeHTTP(rec, req)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package browse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
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.Root = http.Dir(cfg.Root)
|
||||
theRoot, err := bc.Root.Open("/") // catch a missing path early
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
defer theRoot.Close()
|
||||
_, err = theRoot.Readdir(-1)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// Second argument would be the template file to use
|
||||
var tplText string
|
||||
if c.NextArg() {
|
||||
tplBytes, err := ioutil.ReadFile(c.Val())
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
} else {
|
||||
tplText = defaultTemplate
|
||||
}
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
bc.Template = tpl
|
||||
|
||||
// Save configuration
|
||||
err = appendCfg(bc)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// The default template to use when serving up directory listings
|
||||
const defaultTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Name}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { padding: 0; margin: 0; }
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #006ed3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
h1 a:hover {
|
||||
color: #319cff;
|
||||
}
|
||||
|
||||
header,
|
||||
#summary {
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 5%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 5%;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-top: 25px;
|
||||
padding-bottom: 15px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
font-family: Verdana, sans-serif;
|
||||
border-bottom: 1px solid #9C9C9C;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px dashed #dadada;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #ffffec;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
th {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
th svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:first-child svg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
td .name,
|
||||
td .goup {
|
||||
margin-left: 1.75em;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 40px 20px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hideable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
padding-right: 5%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
|
||||
<defs>
|
||||
<!-- Folder -->
|
||||
<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84">
|
||||
<stop stop-color="#b3ddfd" offset="0"/>
|
||||
<stop stop-color="#69c" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26">
|
||||
<stop stop-color="#ace" offset="0"/>
|
||||
<stop stop-color="#369" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="folder" transform="translate(-266.06 -193.36)">
|
||||
<g transform="matrix(.066019 0 0 .066019 264.2 170.93)">
|
||||
<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)">
|
||||
<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/>
|
||||
<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
|
||||
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- File -->
|
||||
<linearGradient id="a">
|
||||
<stop stop-color="#cbcbcb" offset="0"/>
|
||||
<stop stop-color="#f0f0f0" offset=".34923"/>
|
||||
<stop stop-color="#e2e2e2" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/>
|
||||
<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/>
|
||||
<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96">
|
||||
<stop stop-color="#fff" offset="0"/>
|
||||
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<g id="file" transform="translate(-278.15 -216.59)">
|
||||
<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)">
|
||||
<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/>
|
||||
<g>
|
||||
<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/>
|
||||
<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/>
|
||||
<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Up arrow -->
|
||||
<g id="up-arrow" transform="translate(-279.22 -208.12)">
|
||||
<path transform="matrix(.22413 0 0 .12089 335.67 164.35)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
|
||||
</g>
|
||||
|
||||
<!-- Down arrow -->
|
||||
<g id="down-arrow" transform="translate(-279.22 -208.12)">
|
||||
<path transform="matrix(.22413 0 0 -.12089 335.67 257.93)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
|
||||
</g>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
{{range $url, $name := .BreadcrumbMap}}<a href="{{$url}}">{{$name}}</a>{{if ne $url "/"}}/{{end}}{{end}}
|
||||
</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="meta">
|
||||
<div id="summary">
|
||||
<span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span>
|
||||
<span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span>
|
||||
{{- if ne 0 .ItemsLimitedTo}}
|
||||
<span class="meta-item">(of which only <b>{{.ItemsLimitedTo}}</b> are displayed)</span>
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="listing">
|
||||
<table aria-describedby="summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{- if and (eq .Sort "name") (ne .Order "desc")}}
|
||||
<a href="?sort=name&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
{{- else if and (eq .Sort "name") (ne .Order "asc")}}
|
||||
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
|
||||
{{- else}}
|
||||
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name</a>
|
||||
{{- end}}
|
||||
</th>
|
||||
<th>
|
||||
{{- if and (eq .Sort "size") (ne .Order "desc")}}
|
||||
<a href="?sort=size&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
{{- else if and (eq .Sort "size") (ne .Order "asc")}}
|
||||
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
|
||||
{{- else}}
|
||||
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size</a>
|
||||
{{- end}}
|
||||
</th>
|
||||
<th class="hideable">
|
||||
{{- if and (eq .Sort "time") (ne .Order "desc")}}
|
||||
<a href="?sort=time&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
{{- else if and (eq .Sort "time") (ne .Order "asc")}}
|
||||
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
|
||||
{{- else}}
|
||||
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a>
|
||||
{{- end}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- if .CanGoUp}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="..">
|
||||
<span class="goup">Go up</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td class="hideable">—</td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
{{- range .Items}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{.URL}}">
|
||||
{{- if .IsDir}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
|
||||
{{- else}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
|
||||
{{- end}}
|
||||
<span class="name">{{.Name}}</span>
|
||||
</a>
|
||||
</td>
|
||||
{{- if .IsDir}}
|
||||
<td data-order="-1">—</td>
|
||||
{{- else}}
|
||||
<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>
|
||||
</tr>
|
||||
{{- end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Served with <a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a>.
|
||||
</footer>
|
||||
<script type="text/javascript">
|
||||
function localizeDatetime(e, index, ar) {
|
||||
if (e.textContent === undefined) {
|
||||
return;
|
||||
}
|
||||
var d = new Date(e.getAttribute('datetime'));
|
||||
if (isNaN(d)) {
|
||||
d = new Date(e.textContent);
|
||||
if (isNaN(d)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.textContent = d.toLocaleString();
|
||||
}
|
||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||
timeList.forEach(localizeDatetime);
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
|
||||
|
||||
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
|
||||
}
|
||||
defer os.Remove(tempTemplate.Name())
|
||||
|
||||
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expectedPathScope []string
|
||||
shouldErr bool
|
||||
}{
|
||||
// test case #0 tests handling of multiple pathscopes
|
||||
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
|
||||
|
||||
// test case #1 tests instantiation of Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detectaction of custom template
|
||||
{"browse . " + tempTemplatePath, []string{"."}, false},
|
||||
|
||||
// test case #3 tests detection of non-existent template
|
||||
{"browse . " + nonExistantDirPath, nil, true},
|
||||
|
||||
// test case #4 tests detection of duplicate pathscopes
|
||||
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
|
||||
} {
|
||||
|
||||
c := caddy.NewTestController("http", test.input)
|
||||
err := setup(c)
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d recieved an error of %v", i, err)
|
||||
}
|
||||
if test.expectedPathScope == nil {
|
||||
continue
|
||||
}
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
mid := mids[len(mids)-1]
|
||||
recievedConfigs := mid(nil).(Browse).Configs
|
||||
for j, config := range recievedConfigs {
|
||||
if config.PathScope != test.expectedPathScope[j] {
|
||||
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
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/internalsrv"
|
||||
_ "github.com/mholt/caddy/caddyhttp/log"
|
||||
_ "github.com/mholt/caddy/caddyhttp/markdown"
|
||||
_ "github.com/mholt/caddy/caddyhttp/mime"
|
||||
_ "github.com/mholt/caddy/caddyhttp/pprof"
|
||||
_ "github.com/mholt/caddy/caddyhttp/proxy"
|
||||
_ "github.com/mholt/caddy/caddyhttp/redirect"
|
||||
_ "github.com/mholt/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/mholt/caddy/caddyhttp/root"
|
||||
_ "github.com/mholt/caddy/caddyhttp/templates"
|
||||
_ "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 := 25 // 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)
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,26 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"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 middleware.Handler
|
||||
Next httpserver.Handler
|
||||
ErrorPages map[int]string // map of status code to filename
|
||||
LogFile string
|
||||
Log *log.Logger
|
||||
LogRoller *middleware.LogRoller
|
||||
Debug bool // if true, errors are written out to client rather than to a log
|
||||
LogRoller *httpserver.LogRoller
|
||||
Debug bool // if true, errors are written out to client rather than to a log
|
||||
file *os.File // a log file to close when done
|
||||
}
|
||||
|
||||
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
@@ -31,15 +40,14 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
||||
|
||||
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 < 400 signals that a response has been written
|
||||
} else {
|
||||
h.Log.Println(errMsg)
|
||||
return 0, err // returning 0 signals that a response has been written
|
||||
}
|
||||
h.Log.Println(errMsg)
|
||||
}
|
||||
|
||||
if status >= 400 {
|
||||
@@ -54,18 +62,15 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
||||
// 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) {
|
||||
defaultBody := fmt.Sprintf("%d %s", code, http.StatusText(code))
|
||||
|
||||
// See if an error page for this status code was specified
|
||||
if pagePath, ok := h.ErrorPages[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)
|
||||
http.Error(w, defaultBody, code)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
return
|
||||
}
|
||||
defer errorPage.Close()
|
||||
@@ -79,14 +84,14 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
|
||||
// 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)
|
||||
http.Error(w, defaultBody, code)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Default error response
|
||||
http.Error(w, defaultBody, code)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -125,8 +130,7 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
// Write error and stack trace to the response rather than to a log
|
||||
var stackBuf [4096]byte
|
||||
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "%s\n\n%s", panicMsg, stack)
|
||||
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)
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
@@ -43,7 +44,7 @@ func TestErrors(t *testing.T) {
|
||||
|
||||
testErr := errors.New("test error")
|
||||
tests := []struct {
|
||||
next middleware.Handler
|
||||
next httpserver.Handler
|
||||
expectedCode int
|
||||
expectedBody string
|
||||
expectedLog string
|
||||
@@ -123,7 +124,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
eh := ErrorHandler{
|
||||
ErrorPages: make(map[int]string),
|
||||
Debug: true,
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
panic(panicMsg)
|
||||
}),
|
||||
}
|
||||
@@ -145,7 +146,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, "[PANIC /] middleware/errors/errors_test.go") {
|
||||
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) {
|
||||
@@ -156,9 +157,12 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func genErrorHandler(status int, err error, body string) middleware.Handler {
|
||||
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprint(w, body)
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
package setup
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Errors configures a new gzip middleware instance.
|
||||
func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
// setup configures a new errors middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
handler, err := errorsParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the log file for writing when the server starts
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
c.OnStartup(func() error {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
@@ -50,11 +49,10 @@ func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
}
|
||||
if handler.LogRoller != nil {
|
||||
file.Close()
|
||||
|
||||
handler.LogRoller.Filename = handler.LogFile
|
||||
|
||||
writer = handler.LogRoller.GetLogWriter()
|
||||
} else {
|
||||
handler.file = file
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
@@ -63,17 +61,29 @@ func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
// When server stops, close any open log file
|
||||
c.OnShutdown(func() error {
|
||||
if handler.file != nil {
|
||||
handler.file.Close()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
}, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
// Very important that we make a pointer because the Startup
|
||||
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 := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
handler := &ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
@@ -95,7 +105,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
c.IncrNest()
|
||||
logRoller, err := parseRoller(c)
|
||||
logRoller, err := httpserver.ParseRoller(c)
|
||||
if err != nil {
|
||||
return hadBlock, err
|
||||
}
|
||||
@@ -105,10 +115,10 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
}
|
||||
} else {
|
||||
// Error page; ensure it exists
|
||||
where = path.Join(c.Root, where)
|
||||
where = filepath.Join(cfg.Root, where)
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error())
|
||||
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
package setup
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
c := NewTestController(`errors`)
|
||||
mid, err := Errors(c)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `errors`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middlewares, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(*errors.ErrorHandler)
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(*ErrorHandler)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
@@ -32,53 +30,53 @@ func TestErrors(t *testing.T) {
|
||||
if myHandler.LogRoller != nil {
|
||||
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
// Test Startup function
|
||||
if len(c.Startup) == 0 {
|
||||
t.Fatal("Expected 1 startup function, had 0")
|
||||
}
|
||||
err = c.Startup[0]()
|
||||
if myHandler.Log == nil {
|
||||
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
}
|
||||
// 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) {
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler errors.ErrorHandler
|
||||
expectedErrorHandler ErrorHandler
|
||||
}{
|
||||
{`errors`, false, errors.ErrorHandler{
|
||||
{`errors`, false, ErrorHandler{
|
||||
LogFile: "",
|
||||
}},
|
||||
{`errors errors.txt`, false, errors.ErrorHandler{
|
||||
{`errors errors.txt`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
}},
|
||||
{`errors visible`, false, errors.ErrorHandler{
|
||||
{`errors visible`, false, ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log visible }`, false, errors.ErrorHandler{
|
||||
{`errors { log visible }`, false, ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
}`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
500: "500.html",
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
LogRoller: &middleware.LogRoller{
|
||||
LogRoller: &httpserver.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
@@ -92,13 +90,13 @@ func TestErrorsParse(t *testing.T) {
|
||||
}
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
}`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
LogRoller: &middleware.LogRoller{
|
||||
LogRoller: &httpserver.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
@@ -107,8 +105,7 @@ func TestErrorsParse(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputErrorsRules)
|
||||
actualErrorsRule, err := errorsParse(c)
|
||||
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)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"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 middleware.Handler
|
||||
Next httpserver.Handler
|
||||
|
||||
// Path to ther root of the site
|
||||
Root string
|
||||
@@ -28,10 +28,10 @@ type Ext struct {
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
// 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) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
|
||||
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
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
package setup
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
c := NewTestController(`ext .html .htm .php`)
|
||||
|
||||
mid, err := Ext(c)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `ext .html .htm .php`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
t.Fatalf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, had 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(extensions.Ext)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Ext)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
|
||||
@@ -35,7 +35,7 @@ func TestExt(t *testing.T) {
|
||||
if myHandler.Extensions[2] != ".php" {
|
||||
t.Errorf("Expected .php in the list of Extensions")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ func TestExtParse(t *testing.T) {
|
||||
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputExts)
|
||||
actualExts, err := extParse(c)
|
||||
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)
|
||||
Executable → Regular
+132
-60
@@ -4,19 +4,21 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Handler is a middleware type that can handle requests as a FastCGI client.
|
||||
type Handler struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Rules []Rule
|
||||
Root string
|
||||
AbsRoot string // same as root, but absolute path
|
||||
@@ -29,12 +31,12 @@ type Handler struct {
|
||||
ServerPort string
|
||||
}
|
||||
|
||||
// ServeHTTP satisfies the middleware.Handler interface.
|
||||
// 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
|
||||
if !middleware.Path(r.URL.Path).Matches(rule.Path) {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -44,12 +46,24 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
// but we also want to be flexible for the script we proxy to.
|
||||
|
||||
fpath := r.URL.Path
|
||||
if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
|
||||
|
||||
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 fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) || !h.exists(fpath) {
|
||||
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)
|
||||
@@ -58,17 +72,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// Connect to FastCGI gateway
|
||||
var fcgi *FCGIClient
|
||||
|
||||
// check if unix socket or tcp
|
||||
if strings.HasPrefix(rule.Address, "/") || strings.HasPrefix(rule.Address, "unix:") {
|
||||
if strings.HasPrefix(rule.Address, "unix:") {
|
||||
rule.Address = rule.Address[len("unix:"):]
|
||||
}
|
||||
fcgi, err = Dial("unix", rule.Address)
|
||||
} else {
|
||||
fcgi, err = Dial("tcp", rule.Address)
|
||||
}
|
||||
network, address := rule.parseAddress()
|
||||
fcgiBackend, err := Dial(network, address)
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
@@ -77,21 +82,13 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
contentLength, _ := strconv.Atoi(r.Header.Get("Content-Length"))
|
||||
switch r.Method {
|
||||
case "HEAD":
|
||||
resp, err = fcgi.Head(env)
|
||||
resp, err = fcgiBackend.Head(env)
|
||||
case "GET":
|
||||
resp, err = fcgi.Get(env)
|
||||
resp, err = fcgiBackend.Get(env)
|
||||
case "OPTIONS":
|
||||
resp, err = fcgi.Options(env)
|
||||
case "POST":
|
||||
resp, err = fcgi.Post(env, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
case "PUT":
|
||||
resp, err = fcgi.Put(env, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
case "PATCH":
|
||||
resp, err = fcgi.Patch(env, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
case "DELETE":
|
||||
resp, err = fcgi.Delete(env, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
resp, err = fcgiBackend.Options(env)
|
||||
default:
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
}
|
||||
|
||||
if resp.Body != nil {
|
||||
@@ -102,30 +99,67 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
|
||||
// Write the response header
|
||||
for key, vals := range resp.Header {
|
||||
for _, val := range vals {
|
||||
w.Header().Add(key, val)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
// Write response header
|
||||
writeHeader(w, resp)
|
||||
|
||||
// Write the response body
|
||||
// TODO: If this has an error, the response will already be
|
||||
// partly written. We should copy out of resp.Body into a buffer
|
||||
// first, then write it to the response...
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
// Log any stderr output from upstream
|
||||
if fcgiBackend.stderr.Len() != 0 {
|
||||
// Remove trailing newline, error logger already does this.
|
||||
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
|
||||
}
|
||||
|
||||
// Normally we would return the status code if it is an error status (>= 400),
|
||||
// however, upstream FastCGI apps don't know about our contract and have
|
||||
// probably already written an error page. So we just return 0, indicating
|
||||
// that the response body is already written. However, we do return any
|
||||
// error value so it can be logged.
|
||||
// Note that the proxy middleware works the same way, returning status=0.
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return h.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// parseAddress returns the network and address of r.
|
||||
// The first string is the network, "tcp" or "unix", implied from the scheme and address.
|
||||
// The second string is r.Address, with scheme prefixes removed.
|
||||
// The two returned strings can be used as parameters to the Dial() function.
|
||||
func (r Rule) parseAddress() (string, string) {
|
||||
// check if address has tcp scheme explicitly set
|
||||
if strings.HasPrefix(r.Address, "tcp://") {
|
||||
return "tcp", r.Address[len("tcp://"):]
|
||||
}
|
||||
// check if address has fastcgi scheme explicitly set
|
||||
if strings.HasPrefix(r.Address, "fastcgi://") {
|
||||
return "tcp", r.Address[len("fastcgi://"):]
|
||||
}
|
||||
// check if unix socket
|
||||
if trim := strings.HasPrefix(r.Address, "unix"); strings.HasPrefix(r.Address, "/") || trim {
|
||||
if trim {
|
||||
return "unix", r.Address[len("unix:"):]
|
||||
}
|
||||
return "unix", r.Address
|
||||
}
|
||||
// default case, a plain tcp address with no scheme
|
||||
return "tcp", r.Address
|
||||
}
|
||||
|
||||
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
|
||||
@@ -142,29 +176,26 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
|
||||
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
var ip, port string
|
||||
if idx := strings.Index(r.RemoteAddr, ":"); idx > -1 {
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
|
||||
ip = r.RemoteAddr[:idx]
|
||||
port = r.RemoteAddr[idx+1:]
|
||||
} else {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
|
||||
// Split path in preparation for env variables
|
||||
splitPos := strings.Index(fpath, rule.SplitPath)
|
||||
var docURI, scriptName, scriptFilename, pathInfo string
|
||||
if splitPos == -1 {
|
||||
// Request doesn't have the extension, so assume index file in root
|
||||
docURI = "/" + rule.IndexFiles[0]
|
||||
scriptName = "/" + rule.IndexFiles[0]
|
||||
scriptFilename = filepath.Join(h.AbsRoot, rule.IndexFiles[0])
|
||||
pathInfo = fpath
|
||||
} else {
|
||||
// Request has the extension; path was split successfully
|
||||
docURI = fpath[:splitPos+len(rule.SplitPath)]
|
||||
pathInfo = fpath[splitPos+len(rule.SplitPath):]
|
||||
scriptName = fpath
|
||||
scriptFilename = absPath
|
||||
}
|
||||
// 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)
|
||||
@@ -223,9 +254,11 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
env["HTTPS"] = "on"
|
||||
}
|
||||
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
// Add env variables from config
|
||||
for _, envVar := range rule.EnvVars {
|
||||
env[envVar[0]] = envVar[1]
|
||||
// replace request placeholders in environment variables
|
||||
env[envVar[0]] = replacer.Replace(envVar[1])
|
||||
}
|
||||
|
||||
// Add all HTTP headers to env variables
|
||||
@@ -261,6 +294,45 @@ type Rule struct {
|
||||
|
||||
// Environment Variables
|
||||
EnvVars [][2]string
|
||||
|
||||
// Ignored paths
|
||||
IgnoredSubPaths []string
|
||||
}
|
||||
|
||||
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
|
||||
// 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,199 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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))
|
||||
}))
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{{Path: "/", Address: listener.Addr().String()}},
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create request: %v", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
status, err := handler.ServeHTTP(w, r)
|
||||
|
||||
if got, want := status, 0; got != want {
|
||||
t.Errorf("Expected returned status code to be %d, got %d", want, got)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got: %v", err)
|
||||
}
|
||||
if got, want := w.Header().Get("Content-Length"), bodyLenStr; got != want {
|
||||
t.Errorf("Expected Content-Length to be '%s', got: '%s'", want, got)
|
||||
}
|
||||
if got, want := w.Body.String(), body; got != want {
|
||||
t.Errorf("Expected response body to be '%s', got: '%s'", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleParseAddress(t *testing.T) {
|
||||
getClientTestTable := []struct {
|
||||
rule *Rule
|
||||
expectednetwork string
|
||||
expectedaddress string
|
||||
}{
|
||||
{&Rule{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, _ := entry.rule.parseAddress(); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", entry.rule.Address, actualnetwork, entry.expectednetwork)
|
||||
}
|
||||
if _, actualaddress := entry.rule.parseAddress(); 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",
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -30,45 +30,76 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
const FCGI_LISTENSOCK_FILENO uint8 = 0
|
||||
const FCGI_HEADER_LEN uint8 = 8
|
||||
const VERSION_1 uint8 = 1
|
||||
const FCGI_NULL_REQUEST_ID uint8 = 0
|
||||
const FCGI_KEEP_CONN uint8 = 1
|
||||
// FCGIListenSockFileno describes listen socket file number.
|
||||
const FCGIListenSockFileno uint8 = 0
|
||||
|
||||
// FCGIHeaderLen describes header length.
|
||||
const FCGIHeaderLen uint8 = 8
|
||||
|
||||
// Version1 describes the version.
|
||||
const Version1 uint8 = 1
|
||||
|
||||
// FCGINullRequestID describes the null request ID.
|
||||
const FCGINullRequestID uint8 = 0
|
||||
|
||||
// FCGIKeepConn describes keep connection mode.
|
||||
const FCGIKeepConn uint8 = 1
|
||||
const doubleCRLF = "\r\n\r\n"
|
||||
|
||||
const (
|
||||
FCGI_BEGIN_REQUEST uint8 = iota + 1
|
||||
FCGI_ABORT_REQUEST
|
||||
FCGI_END_REQUEST
|
||||
FCGI_PARAMS
|
||||
FCGI_STDIN
|
||||
FCGI_STDOUT
|
||||
FCGI_STDERR
|
||||
FCGI_DATA
|
||||
FCGI_GET_VALUES
|
||||
FCGI_GET_VALUES_RESULT
|
||||
FCGI_UNKNOWN_TYPE
|
||||
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
|
||||
// BeginRequest is the begin request flag.
|
||||
BeginRequest uint8 = iota + 1
|
||||
// AbortRequest is the abort request flag.
|
||||
AbortRequest
|
||||
// EndRequest is the end request flag.
|
||||
EndRequest
|
||||
// Params is the parameters flag.
|
||||
Params
|
||||
// Stdin is the standard input flag.
|
||||
Stdin
|
||||
// Stdout is the standard output flag.
|
||||
Stdout
|
||||
// Stderr is the standard error flag.
|
||||
Stderr
|
||||
// Data is the data flag.
|
||||
Data
|
||||
// GetValues is the get values flag.
|
||||
GetValues
|
||||
// GetValuesResult is the get values result flag.
|
||||
GetValuesResult
|
||||
// UnknownType is the unknown type flag.
|
||||
UnknownType
|
||||
// MaxType is the maximum type flag.
|
||||
MaxType = UnknownType
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_RESPONDER uint8 = iota + 1
|
||||
FCGI_AUTHORIZER
|
||||
FCGI_FILTER
|
||||
// Responder is the responder flag.
|
||||
Responder uint8 = iota + 1
|
||||
// Authorizer is the authorizer flag.
|
||||
Authorizer
|
||||
// Filter is the filter flag.
|
||||
Filter
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_REQUEST_COMPLETE uint8 = iota
|
||||
FCGI_CANT_MPX_CONN
|
||||
FCGI_OVERLOADED
|
||||
FCGI_UNKNOWN_ROLE
|
||||
// RequestComplete is the completed request flag.
|
||||
RequestComplete uint8 = iota
|
||||
// CantMultiplexConns is the multiplexed connections flag.
|
||||
CantMultiplexConns
|
||||
// Overloaded is the overloaded flag.
|
||||
Overloaded
|
||||
// UnknownRole is the unknown role flag.
|
||||
UnknownRole
|
||||
)
|
||||
|
||||
const (
|
||||
FCGI_MAX_CONNS string = "MAX_CONNS"
|
||||
FCGI_MAX_REQS string = "MAX_REQS"
|
||||
FCGI_MPXS_CONNS string = "MPXS_CONNS"
|
||||
// MaxConns is the maximum connections flag.
|
||||
MaxConns string = "MAX_CONNS"
|
||||
// MaxRequests is the maximum requests flag.
|
||||
MaxRequests string = "MAX_REQS"
|
||||
// MultiplexConns is the multiplex connections flag.
|
||||
MultiplexConns string = "MPXS_CONNS"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -79,7 +110,7 @@ const (
|
||||
type header struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
Id uint16
|
||||
ID uint16
|
||||
ContentLength uint16
|
||||
PaddingLength uint8
|
||||
Reserved uint8
|
||||
@@ -92,7 +123,7 @@ var pad [maxPad]byte
|
||||
func (h *header) init(recType uint8, reqID uint16, contentLength int) {
|
||||
h.Version = 1
|
||||
h.Type = recType
|
||||
h.Id = reqID
|
||||
h.ID = reqID
|
||||
h.ContentLength = uint16(contentLength)
|
||||
h.PaddingLength = uint8(-contentLength & 7)
|
||||
}
|
||||
@@ -110,7 +141,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
err = errors.New("fcgi: invalid header version")
|
||||
return
|
||||
}
|
||||
if rec.h.Type == FCGI_END_REQUEST {
|
||||
if rec.h.Type == EndRequest {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
@@ -118,7 +149,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
if len(rec.rbuf) < n {
|
||||
rec.rbuf = make([]byte, n)
|
||||
}
|
||||
if n, err = io.ReadFull(r, rec.rbuf[:n]); err != nil {
|
||||
if _, err = io.ReadFull(r, rec.rbuf[:n]); err != nil {
|
||||
return
|
||||
}
|
||||
buf = rec.rbuf[:int(rec.h.ContentLength)]
|
||||
@@ -126,21 +157,23 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
reqID uint16
|
||||
}
|
||||
|
||||
// Dial connects to the fcgi responder at the specified network address.
|
||||
// DialWithDialer connects to the fcgi responder at the specified network address, using custom net.Dialer.
|
||||
// See func net.Dial for a description of the network and address parameters.
|
||||
func Dial(network, address string) (fcgi *FCGIClient, err error) {
|
||||
func DialWithDialer(network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
|
||||
var conn net.Conn
|
||||
|
||||
conn, err = net.Dial(network, address)
|
||||
conn, err = dialer.Dial(network, address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -148,12 +181,18 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) {
|
||||
fcgi = &FCGIClient{
|
||||
rwc: conn,
|
||||
keepAlive: false,
|
||||
reqId: 1,
|
||||
reqID: 1,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 DialWithDialer(network, address, net.Dialer{})
|
||||
}
|
||||
|
||||
// Close closes fcgi connnection
|
||||
func (c *FCGIClient) Close() {
|
||||
c.rwc.Close()
|
||||
@@ -163,7 +202,7 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.buf.Reset()
|
||||
c.h.init(recType, c.reqId, len(content))
|
||||
c.h.init(recType, c.reqID, len(content))
|
||||
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -179,14 +218,14 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
|
||||
|
||||
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
|
||||
b := [8]byte{byte(role >> 8), byte(role), flags}
|
||||
return c.writeRecord(FCGI_BEGIN_REQUEST, b[:])
|
||||
return c.writeRecord(BeginRequest, b[:])
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
||||
b[4] = protocolStatus
|
||||
return c.writeRecord(FCGI_END_REQUEST, b)
|
||||
return c.writeRecord(EndRequest, b)
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error {
|
||||
@@ -313,10 +352,22 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
|
||||
|
||||
if len(p) > 0 {
|
||||
if len(w.buf) == 0 {
|
||||
rec := &record{}
|
||||
w.buf, err = rec.read(w.c.rwc)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
// filter outputs for error log
|
||||
for {
|
||||
rec := &record{}
|
||||
var buf []byte
|
||||
buf, err = rec.read(w.c.rwc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// standard error output
|
||||
if rec.h.Type == Stderr {
|
||||
w.c.stderr.Write(buf)
|
||||
continue
|
||||
}
|
||||
w.buf = buf
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,17 +385,17 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
|
||||
// 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(FCGI_RESPONDER), 0)
|
||||
err = c.writeBeginRequest(uint16(Responder), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.writePairs(FCGI_PARAMS, p)
|
||||
err = c.writePairs(Params, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
body := newWriter(c, FCGI_STDIN)
|
||||
body := newWriter(c, Stdin)
|
||||
if req != nil {
|
||||
io.Copy(body, req)
|
||||
}
|
||||
@@ -354,6 +405,15 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
|
||||
return
|
||||
}
|
||||
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
// that closes FCGIClient connection.
|
||||
type clientCloser struct {
|
||||
*FCGIClient
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (f clientCloser) Close() error { return f.rwc.Close() }
|
||||
|
||||
// Request returns a HTTP Response with Header and Body
|
||||
// from fcgi responder
|
||||
func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
|
||||
@@ -393,9 +453,9 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
|
||||
resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
||||
|
||||
if chunked(resp.TransferEncoding) {
|
||||
resp.Body = ioutil.NopCloser(httputil.NewChunkedReader(rb))
|
||||
resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)}
|
||||
} else {
|
||||
resp.Body = ioutil.NopCloser(rb)
|
||||
resp.Body = clientCloser{c, ioutil.NopCloser(rb)}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -429,11 +489,17 @@ func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err erro
|
||||
|
||||
// Post issues a POST request to the fcgi responder. with request body
|
||||
// in the format that bodyType specified
|
||||
func (c *FCGIClient) Post(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
|
||||
func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
|
||||
if p == nil {
|
||||
p = make(map[string]string)
|
||||
}
|
||||
|
||||
p["REQUEST_METHOD"] = strings.ToUpper(method)
|
||||
|
||||
if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" {
|
||||
p["REQUEST_METHOD"] = "POST"
|
||||
}
|
||||
|
||||
p["CONTENT_LENGTH"] = strconv.Itoa(l)
|
||||
if len(bodyType) > 0 {
|
||||
p["CONTENT_TYPE"] = bodyType
|
||||
@@ -444,35 +510,11 @@ func (c *FCGIClient) Post(p map[string]string, bodyType string, body io.Reader,
|
||||
return c.Request(p, body)
|
||||
}
|
||||
|
||||
// Put issues a PUT request to the fcgi responder.
|
||||
func (c *FCGIClient) Put(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "PUT"
|
||||
|
||||
return c.Post(p, bodyType, body, l)
|
||||
}
|
||||
|
||||
// Patch issues a PATCH request to the fcgi responder.
|
||||
func (c *FCGIClient) Patch(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "PATCH"
|
||||
|
||||
return c.Post(p, bodyType, body, l)
|
||||
}
|
||||
|
||||
// Delete issues a DELETE request to the fcgi responder.
|
||||
func (c *FCGIClient) Delete(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "DELETE"
|
||||
|
||||
return c.Post(p, bodyType, body, l)
|
||||
}
|
||||
|
||||
// PostForm issues a POST to the fcgi responder, with form
|
||||
// as a string key to a list values (url.Values)
|
||||
func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
|
||||
body := bytes.NewReader([]byte(data.Encode()))
|
||||
return c.Post(p, "application/x-www-form-urlencoded", body, body.Len())
|
||||
return c.Post(p, "POST", "application/x-www-form-urlencoded", body, body.Len())
|
||||
}
|
||||
|
||||
// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
|
||||
@@ -511,7 +553,7 @@ func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[str
|
||||
return
|
||||
}
|
||||
|
||||
return c.Post(p, bodyType, buf, buf.Len())
|
||||
return c.Post(p, "POST", bodyType, buf, buf.Len())
|
||||
}
|
||||
|
||||
// Checks whether chunked is part of the encodings stack
|
||||
@@ -34,14 +34,12 @@ import (
|
||||
// test failed if the remote fcgi(script) failed md5 verification
|
||||
// and output "FAILED" in response
|
||||
const (
|
||||
script_file = "/tank/www/fcgic_test.php"
|
||||
//ip_port = "remote-php-serv:59000"
|
||||
ip_port = "127.0.0.1:59000"
|
||||
scriptFile = "/tank/www/fcgic_test.php"
|
||||
//ipPort = "remote-php-serv:59000"
|
||||
ipPort = "127.0.0.1:59000"
|
||||
)
|
||||
|
||||
var (
|
||||
t_ *testing.T = nil
|
||||
)
|
||||
var globalt *testing.T
|
||||
|
||||
type FastCGIServer struct{}
|
||||
|
||||
@@ -51,7 +49,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
stat := "PASSED"
|
||||
fmt.Fprintln(resp, "-")
|
||||
file_num := 0
|
||||
fileNum := 0
|
||||
{
|
||||
length := 0
|
||||
for k0, v0 := range req.Form {
|
||||
@@ -69,7 +67,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
if req.MultipartForm != nil {
|
||||
file_num = len(req.MultipartForm.File)
|
||||
fileNum = len(req.MultipartForm.File)
|
||||
for kn, fns := range req.MultipartForm.File {
|
||||
//fmt.Fprintln(resp, "server:filekey ", kn )
|
||||
length += len(kn)
|
||||
@@ -101,11 +99,11 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
fmt.Fprintln(resp, "server:got data length", length)
|
||||
}
|
||||
fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", file_num, ")--")
|
||||
fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--")
|
||||
}
|
||||
|
||||
func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
|
||||
fcgi, err := Dial("tcp", ip_port)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return
|
||||
@@ -119,16 +117,16 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
|
||||
if len(data) > 0 {
|
||||
length = len(data)
|
||||
rd := bytes.NewReader(data)
|
||||
resp, err = fcgi.Post(fcgi_params, "", rd, rd.Len())
|
||||
resp, err = fcgi.Post(fcgiParams, "", "", rd, rd.Len())
|
||||
} else if len(posts) > 0 {
|
||||
values := url.Values{}
|
||||
for k, v := range posts {
|
||||
values.Set(k, v)
|
||||
length += len(k) + 2 + len(v)
|
||||
}
|
||||
resp, err = fcgi.PostForm(fcgi_params, values)
|
||||
resp, err = fcgi.PostForm(fcgiParams, values)
|
||||
} else {
|
||||
resp, err = fcgi.Get(fcgi_params)
|
||||
resp, err = fcgi.Get(fcgiParams)
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -142,7 +140,7 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
|
||||
fi, _ := os.Lstat(v)
|
||||
length += len(k) + int(fi.Size())
|
||||
}
|
||||
resp, err = fcgi.PostFile(fcgi_params, values, files)
|
||||
resp, err = fcgi.PostFile(fcgiParams, values, files)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -151,14 +149,14 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
content, err = ioutil.ReadAll(resp.Body)
|
||||
content, _ = ioutil.ReadAll(resp.Body)
|
||||
|
||||
log.Println("c: send data length ≈", length, string(content))
|
||||
fcgi.Close()
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if bytes.Index(content, []byte("FAILED")) >= 0 {
|
||||
t_.Error("Server return failed message")
|
||||
globalt.Error("Server return failed message")
|
||||
}
|
||||
|
||||
return
|
||||
@@ -191,18 +189,18 @@ func generateRandFile(size int) (p string, m string) {
|
||||
return
|
||||
}
|
||||
|
||||
func Disabled_Test(t *testing.T) {
|
||||
func DisabledTest(t *testing.T) {
|
||||
// TODO: test chunked reader
|
||||
globalt = t
|
||||
|
||||
t_ = t
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
// server
|
||||
go func() {
|
||||
listener, err := net.Listen("tcp", ip_port)
|
||||
listener, err := net.Listen("tcp", ipPort)
|
||||
if err != nil {
|
||||
// handle error
|
||||
log.Println("listener creatation failed: ", err)
|
||||
log.Println("listener creation failed: ", err)
|
||||
}
|
||||
|
||||
srv := new(FastCGIServer)
|
||||
@@ -212,41 +210,36 @@ func Disabled_Test(t *testing.T) {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// init
|
||||
fcgi_params := make(map[string]string)
|
||||
fcgi_params["REQUEST_METHOD"] = "GET"
|
||||
fcgi_params["SERVER_PROTOCOL"] = "HTTP/1.1"
|
||||
fcgiParams := make(map[string]string)
|
||||
fcgiParams["REQUEST_METHOD"] = "GET"
|
||||
fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
|
||||
//fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
|
||||
fcgi_params["SCRIPT_FILENAME"] = script_file
|
||||
fcgiParams["SCRIPT_FILENAME"] = scriptFile
|
||||
|
||||
// simple GET
|
||||
log.Println("test:", "get")
|
||||
sendFcgi(0, fcgi_params, nil, nil, nil)
|
||||
sendFcgi(0, fcgiParams, nil, nil, nil)
|
||||
|
||||
// simple post data
|
||||
log.Println("test:", "post")
|
||||
sendFcgi(0, fcgi_params, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil)
|
||||
sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil)
|
||||
|
||||
log.Println("test:", "post data (more than 60KB)")
|
||||
data := ""
|
||||
length := 0
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 256)
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
length += len(k0)
|
||||
length += len(v0)
|
||||
|
||||
data += k0 + "=" + url.QueryEscape(v0) + "&"
|
||||
}
|
||||
sendFcgi(0, fcgi_params, []byte(data), nil, nil)
|
||||
sendFcgi(0, fcgiParams, []byte(data), nil, nil)
|
||||
|
||||
log.Println("test:", "post form (use url.Values)")
|
||||
p0 := make(map[string]string, 1)
|
||||
p0["c4ca4238a0b923820dcc509a6f75849b"] = "1"
|
||||
p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n"
|
||||
sendFcgi(1, fcgi_params, nil, p0, nil)
|
||||
sendFcgi(1, fcgiParams, nil, p0, nil)
|
||||
|
||||
log.Println("test:", "post forms (256 keys, more than 1MB)")
|
||||
p1 := make(map[string]string, 1)
|
||||
@@ -257,25 +250,25 @@ func Disabled_Test(t *testing.T) {
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
p1[k0] = v0
|
||||
}
|
||||
sendFcgi(1, fcgi_params, nil, p1, nil)
|
||||
sendFcgi(1, fcgiParams, nil, p1, nil)
|
||||
|
||||
log.Println("test:", "post file (1 file, 500KB)) ")
|
||||
f0 := make(map[string]string, 1)
|
||||
path0, m0 := generateRandFile(500000)
|
||||
f0[m0] = path0
|
||||
sendFcgi(1, fcgi_params, nil, p1, f0)
|
||||
sendFcgi(1, fcgiParams, nil, p1, f0)
|
||||
|
||||
log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data")
|
||||
path1, m1 := generateRandFile(5000000)
|
||||
f0[m1] = path1
|
||||
sendFcgi(1, fcgi_params, nil, p1, f0)
|
||||
sendFcgi(1, fcgiParams, nil, p1, f0)
|
||||
|
||||
log.Println("test:", "post only files (2 files, 5M each)")
|
||||
sendFcgi(1, fcgi_params, nil, nil, f0)
|
||||
sendFcgi(1, fcgiParams, nil, nil, f0)
|
||||
|
||||
log.Println("test:", "post only 1 file")
|
||||
delete(f0, "m0")
|
||||
sendFcgi(1, fcgi_params, nil, nil, f0)
|
||||
sendFcgi(1, fcgiParams, nil, nil, f0)
|
||||
|
||||
os.Remove(path0)
|
||||
os.Remove(path1)
|
||||
@@ -1,46 +1,56 @@
|
||||
package setup
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// FastCGI configures a new FastCGI middleware instance.
|
||||
func FastCGI(c *Controller) (middleware.Middleware, error) {
|
||||
absRoot, err := filepath.Abs(c.Root)
|
||||
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)
|
||||
absRoot, err := filepath.Abs(cfg.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
rules, err := fastcgiParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return fastcgi.Handler{
|
||||
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Handler{
|
||||
Next: next,
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
Root: cfg.Root,
|
||||
AbsRoot: absRoot,
|
||||
FileSys: http.Dir(c.Root),
|
||||
SoftwareName: c.AppName,
|
||||
SoftwareVersion: c.AppVersion,
|
||||
ServerName: c.Host,
|
||||
ServerPort: c.Port, // BUG: This is not known until the server blocks are split up...
|
||||
FileSys: http.Dir(cfg.Root),
|
||||
SoftwareName: caddy.AppName,
|
||||
SoftwareVersion: caddy.AppVersion,
|
||||
ServerName: cfg.Addr.Host,
|
||||
ServerPort: cfg.Addr.Port,
|
||||
}
|
||||
}, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
var rules []fastcgi.Rule
|
||||
func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule fastcgi.Rule
|
||||
var rule Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
@@ -86,6 +96,12 @@ func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +113,7 @@ func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
|
||||
func fastcgiPreset(name string, rule *Rule) error {
|
||||
switch name {
|
||||
case "php":
|
||||
rule.Ext = ".php"
|
||||
@@ -1,27 +1,26 @@
|
||||
package setup
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestFastCGI(t *testing.T) {
|
||||
|
||||
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
|
||||
|
||||
mid, err := FastCGI(c)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(fastcgi.Handler)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Handler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type , got: %#v", handler)
|
||||
@@ -40,11 +39,11 @@ func TestFastcgiParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []fastcgi.Rule
|
||||
expectedFastcgiConfig []Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []fastcgi.Rule{{
|
||||
false, []Rule{{
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
@@ -54,17 +53,28 @@ func TestFastcgiParse(t *testing.T) {
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
except /admin /user
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
IgnoredSubPaths: []string{"/admin", "/user"},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputFastcgiConfig)
|
||||
actualFastcgiConfigs, err := fastcgiParse(c)
|
||||
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)
|
||||
@@ -101,6 +111,11 @@ func TestFastcgiParse(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Package gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
// Delete this header so gzipping is not repeated later in the chain
|
||||
r.Header.Del("Accept-Encoding")
|
||||
|
||||
// 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, fmt.Errorf("not a Hijacker")
|
||||
}
|
||||
|
||||
// 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("not a Flusher") // 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("not a CloseNotifier")
|
||||
}
|
||||
@@ -2,15 +2,16 @@ package gzip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
|
||||
pathFilter := PathFilter{make(Set)}
|
||||
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
|
||||
for _, p := range badPaths {
|
||||
@@ -21,7 +22,7 @@ func TestGzipHandler(t *testing.T) {
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
gz := Gzip{Configs: []Config{
|
||||
Config{Filters: []Filter{pathFilter, extFilter}},
|
||||
{RequestFilters: []RequestFilter{pathFilter, extFilter}},
|
||||
}}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -78,8 +79,17 @@ func TestGzipHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func nextFunc(shouldGzip bool) middleware.Handler {
|
||||
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
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 r.Header.Get("Accept-Encoding") != "" {
|
||||
return 0, fmt.Errorf("Accept-Encoding header not expected")
|
||||
@@ -87,9 +97,15 @@ func nextFunc(shouldGzip bool) middleware.Handler {
|
||||
if w.Header().Get("Content-Encoding") != "gzip" {
|
||||
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", r.Header.Get("Content-Encoding"))
|
||||
}
|
||||
if _, ok := w.(gzipResponseWriter); !ok {
|
||||
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") == "" {
|
||||
@@ -98,7 +114,7 @@ func nextFunc(shouldGzip bool) middleware.Handler {
|
||||
if w.Header().Get("Content-Encoding") == "gzip" {
|
||||
return 0, fmt.Errorf("Content-Encoding must not be gzip, found gzip")
|
||||
}
|
||||
if _, ok := w.(gzipResponseWriter); ok {
|
||||
if _, ok := w.(*gzipResponseWriter); ok {
|
||||
return 0, fmt.Errorf("ResponseWriter should not be gzipResponseWriter")
|
||||
}
|
||||
return 0, nil
|
||||
@@ -4,18 +4,19 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Filter determines if a request should be gzipped.
|
||||
type Filter interface {
|
||||
// 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", ".xml"}
|
||||
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 {
|
||||
@@ -26,13 +27,13 @@ func DefaultExtFilter() ExtFilter {
|
||||
return m
|
||||
}
|
||||
|
||||
// ExtFilter is Filter for file name extensions.
|
||||
// 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.
|
||||
// ExtWildCard is the wildcard for extensions.
|
||||
const ExtWildCard = "*"
|
||||
|
||||
// ShouldCompress checks if the request file extension matches any
|
||||
@@ -43,7 +44,7 @@ func (e ExtFilter) ShouldCompress(r *http.Request) bool {
|
||||
return e.Exts.Contains(ExtWildCard) || e.Exts.Contains(ext)
|
||||
}
|
||||
|
||||
// PathFilter is Filter for request path.
|
||||
// PathFilter is RequestFilter for request path.
|
||||
type PathFilter struct {
|
||||
// IgnoredPaths is the paths to ignore
|
||||
IgnoredPaths Set
|
||||
@@ -54,7 +55,7 @@ type PathFilter struct {
|
||||
// is found and true otherwise.
|
||||
func (p PathFilter) ShouldCompress(r *http.Request) bool {
|
||||
return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
|
||||
return middleware.Path(r.URL.Path).Matches(value)
|
||||
return httpserver.Path(r.URL.Path).Matches(value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ func (s Set) Contains(value string) bool {
|
||||
// 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 {
|
||||
for k := range s {
|
||||
if f(k) {
|
||||
return true
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func TestSet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExtFilter(t *testing.T) {
|
||||
var filter Filter = ExtFilter{make(Set)}
|
||||
var filter RequestFilter = ExtFilter{make(Set)}
|
||||
for _, e := range []string{".txt", ".html", ".css", ".md"} {
|
||||
filter.(ExtFilter).Exts.Add(e)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func TestPathFilter(t *testing.T) {
|
||||
paths := []string{
|
||||
"/a", "/b", "/c", "/de",
|
||||
}
|
||||
var filter Filter = PathFilter{make(Set)}
|
||||
var filter RequestFilter = PathFilter{make(Set)}
|
||||
for _, p := range paths {
|
||||
filter.(PathFilter).IgnoredPaths.Add(p)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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,89 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,40 @@
|
||||
package setup
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Gzip configures a new gzip middleware instance.
|
||||
func Gzip(c *Controller) (middleware.Middleware, error) {
|
||||
// setup configures a new gzip middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := gzipParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return gzip.Gzip{Next: next, Configs: configs}
|
||||
}, nil
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Gzip{Next: next, Configs: configs}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
var configs []gzip.Config
|
||||
func gzipParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
for c.Next() {
|
||||
config := gzip.Config{}
|
||||
config := Config{}
|
||||
|
||||
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
|
||||
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
|
||||
// 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 {
|
||||
@@ -43,7 +49,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, e := range exts {
|
||||
if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard {
|
||||
if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" {
|
||||
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
|
||||
}
|
||||
extFilter.Exts.Add(e)
|
||||
@@ -68,24 +74,42 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
config.Filters = []gzip.Filter{}
|
||||
// Request Filters
|
||||
config.RequestFilters = []RequestFilter{}
|
||||
|
||||
// If ignored paths are specified, put in front to filter with path first
|
||||
if len(pathFilter.IgnoredPaths) > 0 {
|
||||
config.Filters = []gzip.Filter{pathFilter}
|
||||
config.RequestFilters = []RequestFilter{pathFilter}
|
||||
}
|
||||
|
||||
// Then, if extensions are specified, use those to filter.
|
||||
// Otherwise, use default extensions filter.
|
||||
if len(extFilter.Exts) > 0 {
|
||||
config.Filters = append(config.Filters, extFilter)
|
||||
config.RequestFilters = append(config.RequestFilters, extFilter)
|
||||
} else {
|
||||
config.Filters = append(config.Filters, gzip.DefaultExtFilter())
|
||||
config.RequestFilters = append(config.RequestFilters, DefaultExtFilter())
|
||||
}
|
||||
|
||||
// Response Filters
|
||||
// If min_length is specified, use it.
|
||||
if int64(lengthFilter) != 0 {
|
||||
config.ResponseFilters = append(config.ResponseFilters, lengthFilter)
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
@@ -1,29 +1,30 @@
|
||||
package setup
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
c := NewTestController(`gzip`)
|
||||
|
||||
mid, err := Gzip(c)
|
||||
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)
|
||||
}
|
||||
if mid == nil {
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if mids == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(gzip.Gzip)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Gzip)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
@@ -50,6 +51,9 @@ func TestGzip(t *testing.T) {
|
||||
level 1
|
||||
}
|
||||
gzip`, false},
|
||||
{`gzip {
|
||||
ext ""
|
||||
}`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
@@ -73,10 +77,21 @@ func TestGzip(t *testing.T) {
|
||||
level 1
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
min_length ab
|
||||
}
|
||||
`, true},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
min_length 1000
|
||||
}
|
||||
`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err := gzipParse(c)
|
||||
_, 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 {
|
||||
Vendored
+199
@@ -0,0 +1,199 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mattis, dolor et feugiat suscipit, nisi urna consectetur enim, et porta libero dui eget dolor. Sed ac enim aliquet, ornare ipsum a, aliquet augue. Fusce faucibus id mi at porta. Donec scelerisque consectetur rutrum. In aliquam est vel quam molestie porttitor. Sed eget nunc blandit, commodo dui ac, tempus mi. Nulla ac est ac leo hendrerit rhoncus. Etiam id finibus neque. In ultrices diam orci, eu tempor neque bibendum sed. Fusce facilisis mauris in finibus elementum. Pellentesque tristique bibendum diam, nec molestie velit facilisis quis. Nulla facilisi. Morbi nec velit eget massa blandit consequat sed hendrerit velit. Ut euismod elit sit amet dui venenatis, a luctus tortor vestibulum.
|
||||
|
||||
Aliquam consequat rutrum sagittis. Donec eros felis, ultricies quis elementum nec, consequat et ex. Nullam feugiat eu sapien nec mollis. Quisque porttitor tortor ipsum, quis aliquam diam tincidunt a. Praesent tortor lorem, finibus sit amet tempor ac, iaculis et nunc. Duis enim justo, gravida in pharetra id, rhoncus id nisi. Maecenas dui risus, accumsan ut nisi vitae, placerat dignissim risus. Quisque orci lectus, sodales a lacus sit amet, bibendum gravida odio. Duis consectetur, ante et vulputate convallis, augue augue mollis ligula, sed tincidunt nunc ligula vel enim. Donec faucibus pulvinar consectetur. Nam laoreet, dolor ac elementum vestibulum, eros justo fringilla leo, pretium fringilla sem nunc non sem. Proin at nisl eget leo hendrerit lacinia.
|
||||
|
||||
Aliquam id lacus mauris. Morbi et justo urna. Vestibulum venenatis pharetra lectus in pharetra. Fusce feugiat nisl nec enim rutrum finibus. Etiam a nunc metus. Phasellus non dignissim eros, non feugiat quam. Nunc velit ante, commodo at ex eu, fringilla semper tellus.
|
||||
|
||||
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Etiam rhoncus ultricies nisl, id vestibulum dolor eleifend id. Quisque condimentum condimentum nisi, at dapibus sapien pharetra ut. Suspendisse a sodales odio, sit amet facilisis risus. Vestibulum varius pretium pellentesque. Nunc sit amet sollicitudin sapien, eu finibus mi. Aenean justo arcu, ornare id massa et, auctor dictum dui. Morbi et aliquet velit.
|
||||
|
||||
Curabitur leo ligula, congue non tincidunt vel, consectetur at erat. In scelerisque orci in cursus molestie. Duis erat erat, vehicula sed sollicitudin eget, suscipit in erat. Quisque sed quam in ante dictum hendrerit. Aenean elementum mattis ipsum, sollicitudin luctus felis. Duis fringilla varius mattis. Integer dapibus efficitur blandit.
|
||||
|
||||
Vivamus placerat enim vel est feugiat, cursus pretium ante aliquam. Curabitur hendrerit iaculis odio, in tristique nisl rutrum non. Aenean varius luctus ipsum quis interdum. Nam suscipit purus et lacus accumsan, a rutrum turpis mattis. Sed non bibendum mi. Pellentesque pulvinar ligula eget massa feugiat pharetra. Sed malesuada sem metus, sit amet feugiat purus pretium vel. Fusce magna est, fermentum vitae mauris vel, elementum pharetra est. In blandit faucibus tortor in elementum. Maecenas vitae ornare dui, quis commodo orci. Pellentesque sed scelerisque dolor. Aliquam eget libero in risus tempor efficitur vitae sit amet libero. Sed sed eros dapibus, consectetur mauris eget, auctor mauris.
|
||||
|
||||
Quisque sed efficitur dui. Suspendisse tempor maximus varius. Vivamus tristique volutpat orci, sed placerat ligula condimentum in. Aliquam sodales mi interdum diam semper pulvinar. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum faucibus facilisis leo, eget suscipit urna luctus nec. Integer posuere gravida nibh in imperdiet. Nunc ultrices in lorem vel volutpat. Morbi laoreet augue id dapibus porta. Pellentesque ac convallis lectus. Proin placerat lorem sed lacus blandit rhoncus.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mattis in nunc id scelerisque. Donec et elit ac odio blandit euismod. Mauris enim risus, fermentum ac accumsan in, rhoncus sed erat. Donec sed laoreet eros. In ut purus sed nunc consequat pretium sit amet in nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean at semper nisl. Quisque vel lectus ut felis pellentesque imperdiet.
|
||||
|
||||
Maecenas eget lacinia mi. Vivamus eget sollicitudin tortor. Pellentesque molestie, massa in bibendum ultrices, nunc mauris finibus ex, non porttitor enim leo ac dolor. Vivamus dapibus nisl dui, sit amet convallis ipsum dictum ac. Nulla facilisi. Curabitur laoreet auctor sapien, at porttitor urna scelerisque sit amet. Ut placerat neque congue vulputate ullamcorper. Nunc ut sem tristique, volutpat sem at, commodo augue. Ut facilisis pulvinar vestibulum.
|
||||
|
||||
Duis volutpat eros laoreet, fringilla nulla eu, congue lorem. Fusce vestibulum bibendum ornare. Donec venenatis nisi at augue suscipit, vel cursus massa bibendum. Fusce facilisis nisl ut volutpat tempus. Fusce porttitor ante mauris, et bibendum sapien tincidunt non. In rhoncus tincidunt fermentum. Morbi id venenatis nunc. Maecenas blandit suscipit porta. Proin porttitor molestie nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;
|
||||
|
||||
Phasellus sed nulla nisi. Proin mollis hendrerit felis, ultricies sollicitudin mi semper eget. Fusce at neque at justo vehicula rhoncus a gravida lectus. Cras tristique eget arcu nec fringilla. Nam sit amet dolor mollis, fermentum lorem at, faucibus erat. Morbi eu urna laoreet, faucibus purus et, accumsan neque. Maecenas eget velit lorem. Sed eu est elementum, sagittis elit id, scelerisque elit. Nunc elementum fermentum sem, id viverra mauris viverra at. Sed molestie mi sed velit vestibulum, sit amet porttitor tellus dictum. Fusce rhoncus felis et molestie iaculis. Cras ornare eget urna non pulvinar. Pellentesque ultricies leo quis dignissim sollicitudin. Nunc elit mi, bibendum eu tellus quis, pellentesque dapibus erat.
|
||||
|
||||
Morbi blandit sit amet sem sed fringilla. Ut quis augue egestas, pharetra sapien at, tincidunt neque. Aliquam sodales, justo et tincidunt posuere, nunc lacus tincidunt lectus, non iaculis sem arcu nec diam. Nullam lobortis lorem at quam euismod, non elementum eros vulputate. Vivamus gravida non libero ac tristique. Morbi sit amet lectus elementum, malesuada diam a, pharetra odio. Phasellus non erat ipsum. Phasellus ullamcorper feugiat nunc, ut porta mauris feugiat eu. Ut egestas ligula dolor, ac mollis massa rhoncus at. Donec maximus pulvinar est non eleifend. Quisque vulputate turpis sed ligula accumsan tincidunt. Cras tortor augue, aliquam sed pretium vel, semper sit amet nisi. Pellentesque gravida dapibus libero porta viverra. Donec lacinia, ante et porta mollis, lorem est feugiat odio, vitae sollicitudin augue ipsum ac mi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
|
||||
|
||||
Phasellus non mauris consequat, congue metus at, sodales quam. Phasellus imperdiet et elit quis rhoncus. Phasellus aliquet euismod ligula quis elementum. Etiam sed nibh arcu. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed enim sem, pulvinar quis elit non, dignissim rhoncus mi. Quisque vitae ullamcorper enim, vitae varius lacus. Nunc hendrerit augue quis volutpat bibendum. Nulla ultricies justo sit amet lorem dignissim interdum. Duis congue, mauris vel posuere ornare, lacus justo accumsan ex, a malesuada arcu orci sit amet risus. Sed at nisl non massa sollicitudin convallis at in erat. Ut imperdiet, eros at hendrerit maximus, lacus orci molestie dolor, vitae aliquam ligula lacus eget mi. Sed massa purus, sagittis sed sollicitudin nec, condimentum eget metus. Sed commodo nunc in nibh auctor, a pellentesque nisl pretium. Vivamus ac ex sit amet libero posuere malesuada. Interdum et malesuada fames ac ante ipsum primis in faucibus.
|
||||
|
||||
Praesent molestie est et pulvinar placerat. Sed tincidunt sapien eu justo vulputate rutrum. Donec vel ultrices erat. Morbi quis rhoncus metus. Cras pellentesque augue quis auctor eleifend. Suspendisse quis commodo quam. Etiam dapibus metus nec tortor pharetra lobortis. Quisque pharetra lectus ac velit commodo, nec blandit nisl viverra. Pellentesque aliquet tincidunt molestie. Vestibulum ullamcorper, eros eu posuere varius, metus enim rutrum elit, quis fringilla orci tellus at odio. Ut malesuada, justo vitae lobortis porta, nisi magna dictum metus, sed porta arcu turpis vitae tortor. Morbi in elit sapien.
|
||||
|
||||
Maecenas quis feugiat augue. Integer sollicitudin dolor sit amet mi dictum, non consequat turpis scelerisque. Aliquam erat volutpat. Aliquam venenatis mollis orci, nec sollicitudin elit convallis quis. Vivamus varius, libero a sagittis lobortis, massa massa suscipit neque, lacinia lacinia ante justo non massa. Quisque ut erat ac purus euismod consequat. Phasellus malesuada elementum ipsum sed rutrum. Vivamus scelerisque erat vel nisl tempus euismod id eget arcu.
|
||||
|
||||
Ut fermentum dapibus finibus. Praesent aliquam magna tellus, et pulvinar ipsum dignissim volutpat. Ut mollis diam eros, lacinia euismod lectus venenatis in. Sed lobortis eros massa, at vulputate ipsum hendrerit ac. Aenean id commodo sapien, id suscipit metus. Ut fringilla quam laoreet posuere pretium. Donec vel dapibus nibh. Donec non neque felis. Sed consequat efficitur semper. In ac porta mi. Fusce non elit urna. Aenean pharetra urna laoreet enim malesuada, vitae feugiat nibh molestie. Nam varius porta arcu non convallis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam tempus ex et quam accumsan sodales. Aliquam eget metus ac est accumsan ultrices.
|
||||
|
||||
Nullam eleifend mauris ac maximus scelerisque. Integer nec sapien massa. Ut laoreet, nibh non egestas congue, enim metus mollis mi, sit amet sodales dolor diam sed augue. Vestibulum dignissim metus at felis faucibus faucibus. Pellentesque molestie nisi viverra quam tincidunt, ut molestie est feugiat. Sed nulla tortor, gravida quis varius sed, commodo sit amet arcu. Curabitur viverra, lacus at placerat vestibulum, ligula orci semper ipsum, et mollis mi erat a ligula. Pellentesque ornare urna eu lorem blandit vestibulum. Mauris quis rutrum justo. Vivamus non sollicitudin odio. Curabitur ornare ante vitae justo mollis efficitur.
|
||||
|
||||
Suspendisse interdum bibendum lobortis. Etiam commodo gravida sollicitudin. Curabitur interdum vel nisi a auctor. Nunc varius dui in aliquam porttitor. Nam consequat mi enim, sed tempor eros scelerisque eu. Proin auctor, enim nec imperdiet mollis, dui magna blandit tellus, non elementum orci lorem vitae odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec laoreet cursus risus. Curabitur porta massa vitae lorem posuere, non tristique mauris lacinia. Ut ligula nisl, pulvinar eget ligula eu, suscipit hendrerit ligula.
|
||||
|
||||
Maecenas elementum justo quis odio vestibulum ullamcorper. Quisque commodo non odio quis vehicula. Ut vel ornare erat. Duis quis tristique tellus. Suspendisse dignissim aliquet purus sed consectetur. Cras in sollicitudin lectus. Donec posuere venenatis efficitur. Nullam ac pellentesque mi.
|
||||
|
||||
Etiam malesuada ut tellus nec sodales. Cras malesuada eu arcu quis tincidunt. Sed consequat malesuada eleifend. Aenean accumsan vel ligula efficitur elementum. Sed elementum sapien nec magna iaculis pharetra. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed in augue sit amet sem malesuada facilisis at ac risus. Aliquam aliquam dignissim mauris vel pharetra. In hac habitasse platea dictumst. Phasellus varius risus id urna pretium, fermentum tristique lorem condimentum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum tincidunt eu dolor at vehicula. Proin pulvinar ex non gravida venenatis. Donec lacinia, ipsum in congue hendrerit, risus turpis elementum elit, eget tempor purus lorem ut sapien. Maecenas dapibus eros eget viverra imperdiet. Donec ullamcorper nulla hendrerit molestie rutrum.
|
||||
|
||||
Duis maximus molestie pellentesque. Duis mattis hendrerit dolor, et vulputate turpis pretium ut. Aliquam porta elit in pretium faucibus. Vivamus tortor nunc, elementum nec cursus facilisis, consequat ac elit. Suspendisse porttitor lobortis sollicitudin. Curabitur auctor velit ac gravida feugiat. Praesent convallis tempor nulla, id sodales eros ullamcorper eget. Praesent blandit massa quis placerat molestie. Integer elementum lacinia fermentum. Phasellus ullamcorper mattis finibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur diam quam, egestas nec arcu vitae, efficitur pellentesque nisl. Curabitur a dui vitae metus vehicula condimentum vel sit amet dolor. Vestibulum vehicula dictum semper. Sed vitae aliquet odio.
|
||||
|
||||
Etiam quis purus molestie, finibus nisi aliquam, venenatis nunc. Phasellus tempor id erat sed mattis. Praesent at facilisis diam. In tortor turpis, eleifend vitae facilisis sit amet, ultrices ut nibh. Cras placerat mollis risus, nec accumsan massa aliquam eu. Aliquam erat volutpat. Aliquam tincidunt, leo in laoreet viverra, metus sapien convallis justo, ut pellentesque massa est sit amet ligula. Suspendisse et arcu justo. Quisque at luctus urna. Vestibulum elit eros, dapibus at faucibus sit amet, dapibus a sem. Donec nec mi pulvinar, luctus nisi quis, sollicitudin mauris. Phasellus id velit a enim vehicula eleifend.
|
||||
|
||||
Sed volutpat vel eros sit amet efficitur. Nullam ut neque lobortis, rutrum dui at, blandit massa. Vivamus quis commodo mi. Praesent ultrices lacinia consectetur. Phasellus nec imperdiet augue, eget eleifend arcu. Morbi viverra a sapien vel eleifend. Pellentesque in volutpat sapien. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque facilisis metus a nulla fermentum sollicitudin. Nam hendrerit risus eget purus bibendum ultricies eu commodo leo. Nulla placerat feugiat massa nec efficitur. Nam quis mattis erat, vel porta ex.
|
||||
|
||||
Fusce ullamcorper nibh velit. Praesent porttitor dignissim eros, nec fermentum tellus pellentesque sit amet. Suspendisse potenti. Suspendisse eget quam in lorem sollicitudin porttitor sed a arcu. Cras eget ligula venenatis, posuere sem ac, semper leo. Nam dictum ipsum at leo convallis finibus. Nam a lorem consectetur, ullamcorper nulla ut, posuere sapien. Quisque quis vulputate enim, sit amet consectetur tellus. Integer in massa sed diam mattis imperdiet non a velit. Maecenas consectetur posuere erat, sed cursus dui pretium sed. Etiam at pellentesque mi. Aliquam pharetra rutrum rhoncus.
|
||||
|
||||
Proin finibus luctus leo, at iaculis arcu. Pellentesque ullamcorper ligula sapien, quis tempor velit bibendum ac. Fusce vestibulum posuere purus, quis efficitur diam venenatis quis. Sed posuere arcu ut diam varius laoreet. Integer non eros id odio finibus eleifend. Mauris vulputate arcu non sem placerat, vel sodales dolor accumsan. Phasellus felis elit, viverra nec congue ac, pretium in ante. Fusce ut ex eu massa rutrum dictum id sit amet lectus. Duis venenatis ligula erat, eu congue enim gravida suscipit. Pellentesque luctus eget sem eu luctus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean dignissim suscipit hendrerit.
|
||||
|
||||
Vivamus vel dui ac leo maximus efficitur eget iaculis nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent et elit ultrices, aliquet diam sed, placerat lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed lectus ex, elementum pretium sem nec, tincidunt tincidunt felis. Ut viverra neque mi, vitae convallis dolor tempor vel. Nam eleifend non erat id cursus.
|
||||
|
||||
In euismod scelerisque auctor. Fusce ligula nunc, dictum non euismod nec, ultricies eget sapien. Fusce ultrices in ipsum non vehicula. Nunc a elit a mauris dapibus ullamcorper quis ac neque. Integer vel commodo eros, nec tincidunt mi. Maecenas mi augue, congue a condimentum a, gravida non tellus. Pellentesque tortor eros, fermentum in nunc non, placerat cursus tortor. Donec malesuada eleifend sem, ac consectetur neque ultricies dictum.
|
||||
|
||||
Pellentesque mattis nulla sed bibendum viverra. Phasellus maximus dolor justo, et aliquam felis ultricies vel. Curabitur aliquet pulvinar elit vitae pulvinar. Praesent sed gravida turpis. Praesent ipsum ipsum, lacinia nec bibendum in, sagittis id felis. Donec nibh neque, aliquet non porttitor vitae, maximus id urna. Pellentesque est libero, placerat in libero et, cursus consectetur felis.
|
||||
|
||||
Donec eget ipsum eu nisl aliquam pharetra. Suspendisse consequat enim mi, lacinia ullamcorper nisi vulputate id. Aliquam finibus ligula tortor, eget varius erat convallis a. Donec sollicitudin rutrum rutrum. Aenean mollis lacinia dictum. Morbi ac tempus arcu, sit amet auctor nunc. Nulla sed est dignissim, dictum sem vitae, tincidunt dolor. Pellentesque nunc ipsum, semper quis eros ut, luctus hendrerit velit. Maecenas risus ipsum, posuere et molestie id, tempor a elit. Etiam aliquam turpis sit amet accumsan vestibulum. Curabitur consectetur erat vel magna sodales, sit amet aliquet metus scelerisque. Proin pretium lacus in commodo efficitur. Pellentesque congue aliquam neque at imperdiet. Suspendisse faucibus, ex quis varius consectetur, sem magna congue ligula, sed fringilla urna ligula a libero. Nullam ut sapien at erat ullamcorper venenatis sit amet eu mauris.
|
||||
|
||||
Praesent ante massa, laoreet eu tortor ut, molestie tristique risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Curabitur fermentum vulputate mauris eget sodales. Nunc eleifend leo ac dui efficitur, sit amet volutpat enim semper. Morbi lorem diam, dictum nec felis sed, lobortis tempor nunc. Curabitur dapibus volutpat arcu, eget lacinia ipsum ultricies sed. Fusce hendrerit erat nunc, nec tempus risus pulvinar in. Suspendisse fringilla sagittis ante vel molestie. Suspendisse vulputate sagittis congue. Quisque gravida metus sit amet risus rutrum bibendum. Nullam id ex risus. Aliquam erat volutpat. Nunc vitae tellus id purus scelerisque efficitur. Quisque nulla dui, tempor sed sollicitudin vitae, hendrerit dapibus enim. Nullam tincidunt non est ut lobortis.
|
||||
|
||||
Nunc molestie nisi non vestibulum maximus. Phasellus tincidunt maximus fringilla. Donec justo sem, viverra vel ligula in, ullamcorper mattis odio. Duis commodo urna nec pharetra mollis. Sed vehicula metus et nibh laoreet congue. In tincidunt, metus sit amet pulvinar placerat, arcu quam vestibulum lorem, vel efficitur elit mi at dui. Etiam id congue urna, sit amet tincidunt tellus. Vestibulum euismod magna eget orci semper, in placerat quam congue. Suspendisse eget turpis quis sapien imperdiet sollicitudin. Nullam sodales gravida justo at iaculis. Nunc iaculis leo at orci viverra, in facilisis neque ultricies. Integer gravida neque ut ex sodales posuere. Nullam odio mauris, consectetur ut ultrices eget, porttitor eu tortor. Proin ut risus quis erat tristique ornare non ut est. Nulla dignissim sed ipsum ut sagittis. Curabitur facilisis mattis nunc eget molestie.
|
||||
|
||||
Nam ultrices vitae velit sollicitudin volutpat. Aenean sagittis diam aliquet pretium imperdiet. Donec ultricies nisl a nisi porttitor iaculis. Donec quis tempus purus, in suscipit nibh. Nulla dignissim tristique vulputate. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent egestas sit amet enim at porttitor. Vivamus nec tortor lobortis mauris tempus varius non sed ante. Praesent lorem nisl, blandit ac rutrum at, faucibus eu sem. Nunc arcu nulla, mollis nec eros eget, hendrerit cursus ex. Praesent ut pretium dolor. Vestibulum varius sapien et eleifend commodo. Fusce auctor ut orci a consectetur. Praesent condimentum sit amet elit sit amet sollicitudin.
|
||||
|
||||
In luctus, diam eu ultrices maximus, urna nibh ullamcorper orci, id tempor nibh arcu eu mauris. Phasellus lacus nunc, fringilla eu turpis sed, pharetra ultricies diam. Etiam nisi enim, sagittis et erat non, finibus rhoncus ex. Cras maximus ullamcorper magna, vel tempor lacus venenatis quis. Vivamus fringilla tincidunt purus et maximus. Quisque commodo ullamcorper tellus, eget accumsan massa maximus nec. Curabitur in imperdiet mi. Proin lorem est, pellentesque quis hendrerit at, condimentum non mi. Nulla id ante placerat, dapibus metus nec, hendrerit justo. Praesent pretium, est quis eleifend aliquam, nunc odio convallis mi, vitae dapibus enim sem quis quam. Nulla facilisi.
|
||||
|
||||
Quisque ornare, erat non egestas vulputate, odio massa consequat ex, nec convallis ligula dui at urna. Pellentesque lacinia, est a ornare facilisis, mi leo malesuada nunc, sed tristique ex augue sed magna. Mauris imperdiet pharetra blandit. Etiam eget nisl id massa elementum imperdiet. Nullam tortor leo, volutpat ac nulla id, feugiat pulvinar eros. Pellentesque tempus bibendum felis et luctus. Duis non imperdiet nibh. Sed blandit ex a odio feugiat maximus. Maecenas elementum odio sit amet purus fermentum, ut mattis enim maximus. Proin efficitur lectus enim, nec fringilla velit pharetra quis. Aliquam ligula massa, tincidunt venenatis dolor sit amet, condimentum suscipit nisi. Cras a velit mi. Curabitur id ligula vestibulum, semper erat ut, tincidunt lacus. Nunc egestas urna et velit hendrerit, eu posuere libero sollicitudin. Suspendisse vitae fringilla nunc. Aenean eros ipsum, scelerisque at laoreet sed, mollis vel eros.
|
||||
|
||||
Proin ac placerat nulla. Proin sollicitudin eget dolor eu luctus. Maecenas sit amet ante volutpat, molestie metus sit amet, tempus justo. Cras tellus odio, accumsan vitae urna eget, tempor eleifend turpis. Nulla venenatis posuere lacus congue convallis. Fusce suscipit egestas aliquet. Vivamus condimentum massa eget lacinia finibus. Morbi vitae leo ut eros consequat dapibus.
|
||||
|
||||
Sed vel arcu a lorem posuere placerat in quis purus. Aliquam dapibus libero dui. Etiam in ex diam. Morbi nec nibh ut purus posuere euismod. Sed lobortis neque mauris, ac efficitur arcu venenatis ac. Donec orci purus, accumsan cursus ligula eu, mollis fermentum metus. Donec placerat diam et lorem dignissim, vel varius odio sagittis. Suspendisse potenti. Nam et erat facilisis ex maximus ultricies. Sed non magna nec turpis malesuada vulputate ac in massa.
|
||||
|
||||
Cras ac dignissim purus, quis vehicula tortor. Maecenas vulputate, nisl vel egestas dignissim, nibh elit commodo elit, et maximus elit est vel massa. Vestibulum fermentum, dolor sed pellentesque scelerisque, ligula mauris lacinia lorem, ac commodo elit turpis rhoncus velit. Donec a enim in metus eleifend pharetra et ac elit. Morbi sed pellentesque leo. Nullam fringilla ultricies ante, nec pellentesque massa convallis nec. Maecenas elementum, mi a congue ullamcorper, arcu enim hendrerit enim, vitae fermentum sapien tellus ac sem. Nunc et tempus est. Aliquam lacinia, quam non tempor consequat, lorem lectus mollis nisl, id tempor nisl turpis eu purus. Sed eget maximus diam, convallis hendrerit neque.
|
||||
|
||||
Donec venenatis odio ipsum, in consectetur dui tristique sit amet. Duis placerat neque nec lorem laoreet, et fermentum sapien rutrum. Quisque et diam odio. Phasellus ultrices nibh nec dolor laoreet fringilla. Nulla facilisi. Curabitur pretium erat quam, id facilisis nunc euismod ac. Duis condimentum risus ut vestibulum dapibus. Suspendisse lorem nunc, aliquet quis commodo eu, sagittis a neque. In sodales augue mattis odio sagittis scelerisque. Sed sem purus, commodo nec ornare non, dapibus id enim.
|
||||
|
||||
Mauris urna nisl, consectetur vitae blandit a, posuere a dui. Praesent ut nisl accumsan, vehicula erat a, mattis ligula. Nam metus neque, blandit vel diam a, finibus sagittis orci. Sed metus lorem, congue in tincidunt quis, varius ut lectus. In ultrices augue nec sem bibendum ultricies. Cras a tellus at est gravida tempor vel id risus. Nullam erat nulla, fringilla nec nunc sit amet, placerat ultricies turpis. Sed interdum mi vitae ullamcorper posuere. Proin eu faucibus urna. Mauris maximus, quam a accumsan commodo, justo diam mollis nunc, ac laoreet lacus magna quis nisl. Quisque tincidunt lacus ipsum, ut sagittis elit mattis in. Donec elementum vitae purus ut tristique. Fusce lobortis orci ante, ut bibendum risus imperdiet at.
|
||||
|
||||
Donec dignissim tristique tincidunt. Sed vitae felis justo. Curabitur tincidunt, enim non finibus luctus, elit nunc consectetur libero, sit amet tincidunt odio purus eget quam. Nam tincidunt id metus malesuada feugiat. Vivamus metus quam, posuere quis quam sollicitudin, tincidunt placerat nisi. Aenean nec enim elementum, varius augue id, iaculis urna. Mauris nec risus eu arcu elementum tristique. Nullam ultrices diam eget lorem porta iaculis. Donec fringilla nibh mi, vel auctor enim maximus sed. Vestibulum metus erat, sollicitudin nec pharetra at, sodales vel tellus.
|
||||
|
||||
Phasellus sed erat ac velit faucibus semper ut ut justo. Proin commodo porttitor magna dapibus rutrum. Maecenas placerat neque ac aliquet maximus. Praesent rutrum, felis quis venenatis placerat, tortor ligula porta ipsum, eu dapibus mi urna eget nisl. Proin ultrices vitae ipsum non tristique. Donec convallis massa metus, id mollis felis sollicitudin eu. Nulla gravida nunc id dui bibendum blandit. Suspendisse ut venenatis quam, vitae tempus massa. Quisque tincidunt mi eget erat congue, ac volutpat nunc posuere. Aenean aliquet eros id sapien rhoncus, ac hendrerit urna tincidunt. Morbi vitae euismod nisl, ut accumsan diam. Sed ultricies, tellus nec mollis rhoncus, lacus tellus feugiat est, eu ullamcorper arcu eros nec nulla.
|
||||
|
||||
Donec pretium arcu eget justo efficitur mollis. Vivamus id arcu sit amet mauris congue vulputate sit amet in tellus. Vivamus commodo est libero, et vulputate lorem convallis in. Donec cursus arcu et nibh congue porta. Suspendisse laoreet neque sit amet magna ultrices, consequat scelerisque velit fermentum. Curabitur faucibus lorem faucibus, condimentum sem id, volutpat velit. Donec sed odio dolor. Fusce sodales ligula sit amet pretium hendrerit.
|
||||
|
||||
Mauris ultrices vehicula metus, sed finibus lectus tristique et. Morbi id egestas lacus. Nulla volutpat, lectus et molestie elementum, urna nulla semper massa, at feugiat enim magna quis lacus. Phasellus vitae erat vitae turpis mattis tempus et sed dui. Ut vitae cursus orci. Etiam in massa lectus. Donec purus augue, bibendum sit amet feugiat et, lacinia vitae velit.
|
||||
|
||||
Nulla ac enim tempus, accumsan nunc quis, malesuada quam. Etiam cursus nisi ut aliquam dapibus. Ut vulputate pulvinar ante, id condimentum dui euismod a. Maecenas arcu velit, ornare in nibh eu, feugiat volutpat augue. Duis a diam vitae orci tristique auctor id auctor nisi. Curabitur tincidunt, leo viverra elementum auctor, mi ex efficitur libero, vel aliquet eros mauris sed odio. Nulla consectetur, metus non ullamcorper rutrum, quam eros egestas lacus, vitae luctus odio neque hendrerit nulla. Suspendisse ullamcorper orci massa, non pretium libero dignissim id. Aenean scelerisque justo sapien, quis convallis augue dapibus ut. Aenean ut tincidunt justo. Aenean a lacus lectus.
|
||||
|
||||
Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed sit amet magna eleifend, dictum risus non, dictum turpis. Nam ut quam malesuada, accumsan magna rhoncus, malesuada arcu. In id egestas purus. Nunc nec arcu vel elit vulputate dignissim accumsan sed quam. Suspendisse lacinia ex mattis mi ornare, et maximus ex rhoncus. Morbi ac posuere ligula, eu imperdiet mi. Fusce a dapibus turpis. Duis vel odio elementum, laoreet ipsum sit amet, blandit tellus. Vestibulum eleifend condimentum enim, fermentum vehicula nisi tincidunt nec. Aenean at maximus ante. Maecenas iaculis enim ac tortor feugiat, ac ultricies metus mollis. Nam posuere et turpis vitae egestas.
|
||||
|
||||
Proin vitae eros vel quam tristique ullamcorper. In nec feugiat diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aenean dui elit, dictum sit amet mi non, dignissim laoreet nisi. Nulla eget congue ipsum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis vitae libero sed nulla sagittis placerat nec sed lorem. Phasellus accumsan suscipit nunc a ultricies. Sed dapibus et elit a hendrerit. Ut fermentum quis velit vel dignissim. Etiam vel odio justo. In aliquet eros ut ultrices imperdiet. Aliquam erat volutpat. Phasellus quis dolor aliquam, lacinia mauris ut, blandit elit. Proin risus ipsum, laoreet nec gravida ut, condimentum at nisl.
|
||||
|
||||
Nam eu ipsum vel sem semper consectetur porttitor ut sem. Pellentesque ac leo ultricies, ornare risus pulvinar, elementum ipsum. Maecenas a turpis et lorem auctor sagittis. Pellentesque porttitor eu erat non maximus. Duis tristique in orci sed facilisis. Nulla orci justo, facilisis eu libero et, consequat varius lectus. Fusce quis quam et mauris tristique condimentum at vitae urna. Proin tristique ex ut mauris eleifend, sed dapibus felis semper. Aenean placerat sollicitudin sollicitudin. Ut elementum tincidunt neque in feugiat. Aliquam rhoncus ligula id hendrerit dapibus. Nulla ac ex dolor. Phasellus ex nisi, viverra sit amet faucibus nec, malesuada nec nisl. Aliquam erat volutpat. Ut consequat egestas odio, pretium ullamcorper massa viverra at.
|
||||
|
||||
Pellentesque blandit sit amet est a auctor. Nullam et arcu et risus tempus eleifend. Nunc tristique lectus est, quis rutrum magna sagittis eget. Etiam iaculis tellus eget metus laoreet faucibus vel vel nisi. Fusce et posuere enim. Morbi nec interdum nisl. Donec diam est, semper vel hendrerit at, rutrum sed eros. Nunc eu dapibus sem, eget ultricies elit. Nam condimentum ex pharetra turpis ornare, at pretium orci vehicula. Proin in tristique est, eu mollis ante. Sed tortor nisl, tincidunt et maximus quis, porta vitae elit. Quisque bibendum condimentum sodales. Vivamus fermentum posuere molestie. Morbi pharetra elit eget turpis vulputate, eget laoreet metus rutrum.
|
||||
|
||||
Suspendisse at diam ac nisl vestibulum facilisis. Quisque non sollicitudin urna, sed lacinia tortor. Duis ultrices ut mauris at aliquet. Vestibulum eu mauris dapibus, dictum nibh rhoncus, bibendum magna. Suspendisse rutrum ligula eu posuere efficitur. Nullam in egestas arcu. Proin a urna ac felis tempor sagittis egestas nec tortor. Donec ac feugiat leo. Ut ac tempor sapien, ac lacinia dolor. In placerat sagittis libero, sit amet varius massa auctor sit amet. Fusce pellentesque finibus odio vel scelerisque. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus euismod nisi nisi, ac luctus sapien venenatis nec.
|
||||
|
||||
Quisque sit amet metus et turpis laoreet pulvinar. Sed at accumsan ex. Aliquam eu tempus odio. Fusce consectetur dolor ut magna fringilla venenatis. Pellentesque rhoncus libero elementum, placerat eros et, laoreet eros. Nulla sit amet ligula viverra, aliquam felis accumsan, blandit eros. Vestibulum porta volutpat magna vel malesuada.
|
||||
|
||||
Duis semper aliquam nisi, vel finibus tellus pretium mollis. Vivamus nec libero vitae neque blandit finibus. Aliquam maximus, libero ut tristique cursus, magna risus placerat dui, sit amet semper odio metus vel mauris. Aenean porta ut nisi in aliquet. Vivamus in felis eu odio porttitor finibus. Phasellus placerat urna non leo congue, at placerat magna volutpat. Etiam ut fermentum ligula. Sed nunc lectus, posuere et tellus sed, rutrum pulvinar ex. Nulla ullamcorper at mauris at efficitur. Vivamus ultrices, odio sit amet congue porttitor, purus ante mattis justo, quis dapibus sem tellus sit amet dui. Morbi nec neque efficitur, scelerisque erat in, posuere diam. Nam faucibus nunc eget condimentum pulvinar. Praesent rutrum nisi et mauris fermentum sagittis. Cras imperdiet elit in eros mollis, ut pulvinar lacus tincidunt.
|
||||
|
||||
Vestibulum vel convallis ante. Duis et varius nibh. Praesent sodales purus at efficitur lacinia. In eget lacus odio. Vivamus eros nisi, lobortis non quam et, sollicitudin congue nulla. Pellentesque vestibulum turpis non purus pharetra egestas. Quisque accumsan vestibulum fermentum. Nam eu hendrerit neque. Proin ut aliquet lorem, id suscipit massa. Sed volutpat viverra magna, vel faucibus felis convallis in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras a hendrerit odio, ac facilisis purus.
|
||||
|
||||
Nam a nulla ultricies tellus porttitor feugiat. Morbi et urna suscipit, accumsan massa id, semper nisl. Nam eros nibh, eleifend semper erat vitae, vehicula cursus mauris. Nam semper pulvinar augue eu auctor. Morbi eget laoreet magna. Proin porta, nulla egestas commodo ultrices, risus felis efficitur eros, et mollis augue nisl in neque. Nullam dignissim dui eget tellus gravida, non congue nisl elementum. Nulla ac eros hendrerit, sollicitudin mauris vitae, tristique turpis. Maecenas id laoreet erat.
|
||||
|
||||
Nullam vestibulum in felis euismod aliquam. Sed sit amet magna in mauris feugiat scelerisque et fermentum lorem. Aenean scelerisque eget mauris vel molestie. Sed vel ipsum non magna eleifend tristique. Phasellus viverra nulla justo, sed maximus neque sollicitudin hendrerit. Mauris facilisis sem vel elit mattis iaculis. Vivamus ac dapibus sem. Sed quis egestas sapien, eu volutpat augue. Vestibulum molestie suscipit est, ut fermentum diam rhoncus eu. Maecenas accumsan massa sed sapien viverra commodo. Duis eget lacus a urna accumsan mattis at sed orci. Aenean at scelerisque felis. Sed magna lectus, lacinia eu pharetra ac, pharetra at ex. Vestibulum vel tortor a sem luctus placerat. Maecenas efficitur, est sed auctor vehicula, ante lorem finibus dolor, tristique efficitur lacus risus eget ex. Pellentesque maximus velit eu ipsum lobortis blandit.
|
||||
|
||||
Maecenas aliquam sollicitudin tellus, consequat ultricies lacus. Proin pretium nibh fermentum, malesuada quam sit amet, luctus orci. Curabitur sodales varius tortor, eu rutrum nisi blandit eu. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Quisque nec lectus quis nisi mattis dapibus. Donec dapibus leo ac nibh elementum pharetra. Aliquam lobortis ante a risus facilisis condimentum. Sed mauris enim, tristique sed viverra eu, varius vel lorem. Vivamus non ex mi. Etiam sed pellentesque ante. Ut leo ipsum, mattis a eleifend sed, tincidunt pharetra nibh. Nam vel tellus nunc.
|
||||
|
||||
Nullam libero urna, euismod at consequat quis, fringilla eu turpis. Aliquam tellus leo, posuere elementum risus vel, pulvinar tincidunt quam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut non nibh id massa bibendum porta a sed ante. Donec commodo accumsan orci, quis volutpat sem tincidunt id. Nulla mollis, lacus id pretium cursus, nisi nulla rhoncus quam, non maximus leo ligula consequat metus. In fringilla et est quis tristique. Nullam vestibulum urna leo, non molestie ipsum dignissim at. Donec tristique erat vitae interdum gravida. Vestibulum tincidunt nec nunc eget blandit. Vestibulum at laoreet arcu, sed ultrices orci. Nam vitae sapien bibendum, aliquam massa quis, convallis mauris. Morbi eu mi ultrices, aliquam tortor sit amet, elementum diam. Sed pulvinar lorem eget urna consequat consectetur. Integer hendrerit tortor quis ipsum suscipit, eget vulputate neque semper. Duis pulvinar hendrerit nisi sed pretium.
|
||||
|
||||
Ut imperdiet tempor convallis. Nullam vehicula sem id ligula hendrerit consectetur. Nunc a ornare justo. Donec elementum eros id mi sagittis, sit amet consectetur nisi semper. Maecenas varius gravida turpis, quis dapibus ex consectetur sed. Cras tortor neque, lacinia et neque scelerisque, hendrerit semper est. In ultricies blandit nisl ut finibus.
|
||||
|
||||
In sapien ante, euismod non molestie vel, venenatis eu tortor. Pellentesque nec tortor sed ante posuere faucibus. Morbi sed euismod urna, vel dictum neque. Aenean ultrices in eros a lobortis. Suspendisse lacinia est eu lacus porttitor ornare. Donec vitae sapien diam. Integer vitae pellentesque ex. Maecenas iaculis purus id felis hendrerit suscipit vitae non tortor. Pellentesque rhoncus facilisis orci, at ultrices nibh egestas nec.
|
||||
|
||||
Quisque eu quam eros. Nam venenatis pharetra augue. Donec ut mauris maximus, rutrum nulla id, dignissim purus. Curabitur nec sem id risus mollis bibendum nec sit amet ipsum. Nam dignissim hendrerit ullamcorper. Suspendisse eu risus scelerisque, dapibus nibh vitae, pellentesque eros. Proin finibus sapien id cursus consequat. Quisque augue mi, imperdiet vitae dignissim eget, lacinia id lectus. Etiam at augue non lectus iaculis fermentum vel in arcu. Suspendisse et nunc sodales, auctor arcu in, faucibus est. Ut quis neque vitae leo placerat luctus sed quis velit. Suspendisse vehicula, erat vel dictum posuere, ligula sapien tristique velit, vel tincidunt orci leo at enim. Quisque non ullamcorper lectus.
|
||||
|
||||
Aliquam dapibus mattis purus eu euismod. Maecenas sagittis mi quis tellus sodales hendrerit. Phasellus at sem egestas, condimentum purus in, cursus quam. Praesent nec suscipit diam, at laoreet ex. Suspendisse cursus lectus eu erat convallis accumsan et nec orci. Pellentesque finibus hendrerit tellus, at rutrum enim rhoncus eget. Etiam id hendrerit nulla. Phasellus tempus urna a lorem ullamcorper accumsan. Proin sit amet orci ante.
|
||||
|
||||
Aenean pulvinar sem quis ligula dignissim tincidunt. Maecenas eu odio ut tortor venenatis elementum. Aenean a purus nunc. Suspendisse ut hendrerit elit. Phasellus ac lectus in tellus ullamcorper tincidunt. Aliquam ac pellentesque leo. Nam dignissim semper quam a efficitur. Suspendisse finibus pretium risus non lacinia. Phasellus consectetur porta pellentesque. Integer ac facilisis libero. Quisque pellentesque imperdiet tortor. Fusce porta mi nec consequat sagittis. Aliquam turpis sem, finibus ut tellus at, efficitur tincidunt velit. Nullam vitae diam sit amet quam malesuada lacinia.
|
||||
|
||||
Proin in hendrerit nisi. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean elementum tortor libero, ac volutpat eros euismod quis. Ut malesuada convallis tortor a consectetur. Curabitur urna sem, consectetur in elit at, mollis dictum elit. Nulla rhoncus libero id purus fermentum faucibus. Curabitur finibus auctor suscipit. Ut sed molestie elit. In ut sem non sapien varius commodo. Duis nec facilisis erat. Donec sollicitudin mollis ex, in dapibus erat commodo eget. Vestibulum lorem sapien, consequat sed commodo ac, maximus vitae nunc. Pellentesque interdum interdum nulla, id facilisis magna viverra at. Donec porttitor ante id congue dignissim. Aliquam dignissim eget diam pellentesque accumsan.
|
||||
|
||||
Donec a efficitur neque. Integer iaculis sit amet augue sed pharetra. Nulla dignissim dolor a orci maximus ullamcorper. Morbi varius enim ut posuere molestie. Donec volutpat magna ut suscipit interdum. Cras magna dolor, rutrum eget nisi quis, ornare fringilla libero. Nullam tempor tempor erat eget condimentum. Vivamus egestas ex ac tellus finibus maximus. Donec eget vehicula purus, a venenatis neque. Duis mollis quam sit amet lectus ullamcorper tempus. Etiam eget sapien in orci porta pharetra. Phasellus et viverra dolor, vitae placerat urna. Nullam vel porttitor tellus. Aenean dictum imperdiet lobortis.
|
||||
|
||||
Suspendisse auctor metus ut aliquam ullamcorper. Morbi ac aliquet massa, a porta lorem. Etiam luctus dignissim erat. Etiam eu mi eu nisi convallis bibendum vel a nisi. Aenean condimentum enim et magna placerat, dictum aliquet tellus bibendum. Nam interdum mauris at facilisis porta. Morbi sodales sollicitudin tempus.
|
||||
|
||||
Nam quis aliquam tellus. Vestibulum placerat nisi eget tempus egestas. Etiam congue diam ut leo blandit fermentum. Curabitur eu dui quis mauris sodales imperdiet ac ac urna. Integer at nisl cursus, suscipit odio vel, consequat neque. Suspendisse in lectus turpis. Nullam dui odio, tempus ut feugiat at, suscipit a justo. Phasellus mauris augue, aliquet efficitur cursus eu, finibus sit amet ipsum. Vestibulum urna mauris, mollis sit amet ex at, cursus imperdiet nulla. Donec velit justo, viverra et erat in, hendrerit hendrerit turpis. Proin pharetra nunc aliquam neque congue eleifend.
|
||||
|
||||
Praesent vel egestas metus. Sed dui mi, laoreet a euismod ac, ullamcorper in orci. Aenean eu risus pulvinar, aliquet ex ac, ornare ex. Mauris lectus purus, placerat ut lacus ac, pellentesque sollicitudin sem. Proin sed volutpat sapien. Ut posuere ex ac odio eleifend, ac ultrices ligula ultricies. Vestibulum nec nunc a leo volutpat convallis eget vel turpis. Suspendisse ut dapibus enim, at scelerisque ligula. Mauris vestibulum nec tellus a porta. Quisque ac placerat sapien, nec maximus nunc. Vivamus mollis tincidunt risus in auctor. In laoreet elit et eleifend facilisis. Integer facilisis tortor quam, sed tristique purus mattis id. Etiam faucibus leo augue.
|
||||
|
||||
Sed tristique efficitur nulla, et rutrum mauris euismod non. Praesent ac leo luctus dui rutrum dictum a vel ipsum. Vestibulum faucibus risus vitae imperdiet rhoncus. Nunc volutpat viverra nisl. Nunc sit amet urna sed purus iaculis egestas. Nam pretium dui eget ante ullamcorper tempus. Pellentesque gravida, dui non cursus tempus, nisl lorem viverra diam, in condimentum metus neque eu justo. In malesuada tortor sagittis posuere finibus. Nunc ac nisi lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin nec nibh nec orci sagittis venenatis sed et justo. Vestibulum et nisl vitae ex mattis efficitur. Interdum et malesuada fames ac ante ipsum primis in faucibus.
|
||||
|
||||
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse hendrerit nibh nec ex consequat tempor. Suspendisse vestibulum urna in blandit iaculis. Aenean lacinia odio ex, eget tempor nulla condimentum sed. Proin suscipit, est eget sollicitudin feugiat, nisl felis ultrices massa, non finibus justo risus quis lectus. Mauris ipsum tellus, suscipit nec gravida sed, aliquet non sem. Ut suscipit sodales ante, vitae feugiat quam scelerisque in. Curabitur id luctus dui, eget interdum dui. Maecenas sagittis quis nisi et laoreet. Cras maximus in leo et feugiat. Proin aliquam egestas consequat. Suspendisse sit amet arcu auctor eros imperdiet rhoncus sed in lorem. Sed porttitor at elit laoreet mattis. Donec dui eros, dictum sit amet massa at, facilisis consequat nisi. Phasellus arcu est, viverra vel lacus id, efficitur sodales ligula.
|
||||
|
||||
Nam ultrices dignissim mi vel vestibulum. Mauris a euismod lorem, in scelerisque enim. Pellentesque molestie fermentum purus, nec semper nunc placerat sit amet. Aenean ullamcorper sollicitudin faucibus. Curabitur a hendrerit leo. Donec congue massa nulla, vitae sodales ex posuere non. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras pharetra risus eu consequat commodo. Donec iaculis lorem vitae diam mollis feugiat.
|
||||
|
||||
Nulla molestie dui quis blandit pretium. Curabitur et tempus nisi. Aliquam luctus interdum libero a varius. Morbi nec volutpat odio. Quisque elementum velit vitae mi hendrerit, nec aliquet arcu tempus. Aenean ullamcorper, tortor sit amet rutrum dapibus, metus dui sagittis nisl, id volutpat velit lorem sodales erat. Quisque vel justo sed massa rutrum accumsan eget non elit. Fusce luctus felis mi, id aliquet massa commodo sit amet. Morbi blandit et est ac faucibus. Quisque eget malesuada nibh, sit amet venenatis purus. Proin risus eros, ullamcorper nec posuere a, hendrerit eu dolor. Nunc a mattis dolor. Fusce ultricies arcu eu lectus consequat elementum. Phasellus euismod nunc sapien, eget lobortis urna efficitur vitae. Phasellus sed ligula rhoncus, imperdiet neque eget, luctus est. Praesent aliquam aliquet turpis, quis ornare lacus sagittis ut.
|
||||
|
||||
Quisque rhoncus velit in suscipit commodo. Donec massa turpis, posuere ut vulputate vitae, ultrices gravida velit. Aenean lectus nulla, hendrerit vitae laoreet imperdiet, ultrices a ipsum. Morbi a tincidunt odio, et lacinia nisi. In in pulvinar nibh. Vestibulum vehicula tellus quis est mollis efficitur. Maecenas non pellentesque dui, et sagittis erat. Sed sed diam blandit diam convallis euismod sit amet sit amet arcu.
|
||||
|
||||
Proin ullamcorper maximus suscipit. Aenean non volutpat quam. Vivamus ex justo, aliquam vitae vulputate eu, porttitor ut leo. Sed lorem mauris, finibus in feugiat sit amet, tincidunt ac risus. Duis accumsan lectus at purus dapibus ultricies. Ut ac sagittis velit. In vel enim eu neque imperdiet interdum eget eget massa. Aliquam varius sagittis pulvinar. Etiam fermentum sit amet mi finibus tempor. Aliquam varius risus erat, ac commodo sapien euismod et. Proin pretium mattis turpis et aliquet. Suspendisse facilisis ut mi ut maximus. Donec dictum dapibus feugiat.
|
||||
|
||||
Proin tempor diam vestibulum odio elementum aliquam. Integer nunc urna, dictum id tortor eget, efficitur accumsan elit. Quisque lobortis ante ac mollis dignissim. Integer vel nunc rutrum, malesuada metus a, vestibulum neque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec ac erat ut mi fermentum consequat. Praesent tempor, erat id porttitor tempor, erat mauris semper massa, ac vulputate lacus justo ac ligula. Sed ligula neque, tincidunt ut tortor sed, volutpat sollicitudin diam. Pellentesque ligula purus, volutpat nec velit non, vulputate euismod arcu. Aenean quis purus volutpat, placerat sem vitae, sollicitudin nunc. Cras laoreet semper ipsum quis finibus. Fusce pellentesque purus at nibh mattis, et consequat dui viverra. Proin eu tellus sit amet sapien volutpat dictum. Curabitur luctus feugiat massa ac vehicula. Nullam eu lacus tempus, hendrerit lectus ac, posuere odio. Donec eu dui metus.
|
||||
|
||||
Quisque molestie turpis eget dolor iaculis cursus. Suspendisse facilisis tristique enim at efficitur. Quisque sit amet nunc nisl. Nam placerat interdum fringilla. Aliquam vulputate, risus et euismod vulputate, ante dolor consequat odio, at finibus diam ante quis sem. Nulla accumsan quam id neque aliquam maximus. Proin sed convallis eros. Phasellus imperdiet in quam a ultrices. Fusce et imperdiet elit. Sed et hendrerit ipsum. Fusce arcu odio, rutrum vitae posuere vitae, facilisis lacinia nibh.
|
||||
|
||||
Fusce nec tincidunt sem. Morbi tempus, nisi quis malesuada ornare, turpis neque condimentum justo, bibendum bibendum justo ex eu nulla. Pellentesque laoreet tincidunt dolor nec venenatis. Morbi eros risus, venenatis ac luctus ut, luctus quis sapien. Etiam et tellus massa. Aenean sit amet libero massa. Fusce venenatis elit vitae tellus commodo hendrerit. Curabitur ipsum tellus, dignissim nec lacus non, suscipit efficitur magna.
|
||||
|
||||
Integer et erat tortor. Ut elementum pharetra scelerisque. Pellentesque mollis, urna quis faucibus cursus, arcu orci mattis lorem, varius maximus magna justo quis massa. Donec id scelerisque metus. Fusce sit amet ex a lectus ornare mollis. Donec ut metus nisl. Sed at turpis turpis. Suspendisse ut nibh nunc. Duis eget viverra nisi.
|
||||
|
||||
In massa velit, ultricies sit amet elementum vel, sollicitudin ac tellus. Ut euismod nisl non leo porta varius. Phasellus sapien velit, auctor quis pulvinar nec, venenatis in urna. Aliquam elementum, eros vitae dignissim condimentum, enim lacus suscipit nibh, eget imperdiet nibh metus ac arcu. Quisque condimentum, ligula in luctus pharetra, elit tortor lacinia neque, et aliquam ex velit sed ipsum. Maecenas convallis ante odio, finibus egestas dolor porta non. Nunc fringilla erat diam, nec lobortis est scelerisque eget. Cras dictum lectus rhoncus, rhoncus dui at, bibendum risus. Aenean eget tristique nunc. Etiam eget elementum urna. Nulla facilisi.
|
||||
|
||||
Aliquam porta magna id est fermentum, sit amet iaculis mi viverra. Nullam pharetra, ex non ultricies fringilla, dui ligula cursus ipsum, vitae iaculis eros quam vel sem. Sed lacinia vestibulum tellus at sodales. Quisque vulputate, quam eu porta convallis, lectus lacus viverra metus, quis tincidunt augue libero nec tellus. Curabitur et fermentum turpis. Praesent urna neque, fermentum ac magna vitae, convallis fermentum enim. Nam ut nulla at dui viverra faucibus vel at justo. Praesent feugiat commodo ante et dictum. Nullam quis gravida diam. Curabitur nec nisl ac elit luctus bibendum a sed augue. Mauris nec dolor eget eros ullamcorper condimentum.
|
||||
|
||||
In leo mauris, sagittis id rhoncus vel, aliquet non augue. Pellentesque ornare vitae nibh luctus pulvinar. Proin eu lacinia neque, ut faucibus lorem. Curabitur quis justo porttitor, tincidunt mauris id, porta turpis. Sed id leo elit. Morbi auctor nisi eget augue feugiat consequat. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed ut purus felis.
|
||||
|
||||
Ut pretium, purus eget ultricies condimentum, diam quam finibus lacus, non scelerisque leo nisi at arcu. Vestibulum consectetur quam ac dapibus dignissim. Cras et lacus sit amet nisl sodales dignissim non eu tortor. Nam ultrices enim quam, vitae lobortis est varius in. Sed tincidunt tincidunt eros eget semper. Etiam accumsan efficitur suscipit. Donec at facilisis purus, et egestas ex. Vivamus dapibus iaculis sapien. Sed fermentum dui sit amet est blandit scelerisque non vitae neque. Vivamus a placerat ex. Mauris feugiat ultrices turpis, id luctus nulla rhoncus mattis. Curabitur accumsan eleifend ligula id viverra.
|
||||
|
||||
Aliquam molestie maximus nisl, vitae fermentum odio hendrerit quis. Fusce varius quis velit at sagittis. Morbi eu magna aliquet, scelerisque risus sit amet, sollicitudin ante. Mauris scelerisque pellentesque lacinia. Duis quis ligula nec mauris pretium lacinia. Nulla imperdiet lorem est, nec laoreet ante commodo et. Suspendisse et sollicitudin turpis, ut luctus nibh.
|
||||
|
||||
Ut id libero nec purus condimentum pharetra eget ac mauris. Vivamus finibus dignissim lacus, at porta leo sodales sit amet. Nunc augue leo, eleifend eu ultricies eget, posuere et tellus. Maecenas porta, nibh in rutrum semper, ex eros fringilla nisl, eu ultricies sem nunc sit amet magna. Aenean nec pharetra odio. Pellentesque quis diam quam. Fusce tincidunt justo enim, in condimentum erat euismod id. Etiam varius dui ut turpis placerat efficitur non id nulla. Nunc in velit magna. Sed posuere enim enim, sed semper urna auctor at. Nam scelerisque, est sed accumsan convallis, ipsum ligula imperdiet purus, eget malesuada urna nisi at nisl. Donec massa purus, venenatis at ex in, lobortis porttitor erat. In hac habitasse platea dictumst.
|
||||
|
||||
Mauris varius mattis fringilla. Ut pretium leo turpis. Etiam nec scelerisque sem. Donec velit elit, suscipit in dolor scelerisque, sagittis dignissim justo. Morbi egestas sit amet turpis vitae elementum. Integer maximus justo ac elit tempus faucibus. Donec consequat elementum dui consequat lobortis. Etiam eget est nec nibh pulvinar maximus. Integer egestas pulvinar leo vitae luctus. Nullam enim justo, vestibulum ac bibendum et, pharetra eleifend mauris. In ut ligula nec risus eleifend blandit.
|
||||
|
||||
Mauris sagittis arcu quis magna pulvinar, quis interdum leo semper. Aenean viverra turpis eget erat molestie, sit amet malesuada ipsum maximus. Duis eros metus, consequat vel metus quis, ullamcorper ullamcorper tortor. Duis condimentum, metus non molestie mollis, ante augue eleifend elit, nec porttitor nunc ex id purus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam nisi velit, placerat et dictum sed, vulputate et mauris. Sed bibendum porttitor tellus vel vehicula.
|
||||
|
||||
Nam non gravida sapien, et euismod nulla. Pellentesque dapibus tristique lectus, sed finibus turpis accumsan ut. Pellentesque ultricies diam eu felis iaculis, vitae egestas nulla tempus. Ut pharetra, sem ac feugiat lobortis, ex nisl congue libero, vel pellentesque leo nibh eu elit. Etiam vehicula non metus at ornare. Aliquam orci leo, eleifend a fermentum condimentum, eleifend at lectus. Ut sit amet iaculis elit. Nullam molestie mattis leo a aliquet. Vestibulum convallis commodo rhoncus. Vestibulum at eros leo. Nam tempus urna sit amet nunc sagittis blandit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nam condimentum, felis sit amet ultricies consectetur, velit ex ultrices ipsum, sit amet condimentum enim dui pulvinar risus. Vivamus euismod a quam et sagittis. Fusce posuere at sem non egestas. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
|
||||
|
||||
Maecenas urna felis, condimentum vitae pretium vel, consequat id ligula. Proin eget ante eget tellus luctus maximus vitae sit amet libero. Donec at risus eget tellus feugiat vulputate. Aliquam laoreet sapien at pellentesque vehicula. In egestas laoreet lobortis. Duis ultrices molestie urna sit amet dapibus. Fusce ultricies mauris lectus, in luctus est facilisis sit amet. Nunc sed faucibus orci. Fusce fringilla est a dictum commodo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aliquam aliquam lacus a eros tincidunt maximus.
|
||||
|
||||
Aenean vestibulum, metus vel interdum lacinia, libero metus consectetur tellus, non sollicitudin ante ipsum ut velit. Donec accumsan laoreet justo hendrerit venenatis. Mauris hendrerit tincidunt diam eu accumsan. Suspendisse egestas non orci at vulputate. Praesent sollicitudin congue magna egestas aliquet. Mauris at molestie mi. Mauris vel magna viverra, mattis velit at, fringilla elit. Cras porta lorem leo, ut tincidunt velit pharetra non.
|
||||
|
||||
Praesent commodo turpis et ligula vulputate, non ullamcorper ante faucibus. Phasellus cursus magna quis est mollis, id suscipit libero mattis. Nam felis lectus, accumsan vitae sollicitudin eu, facilisis nec velit. Nam sollicitudin odio ut sapien vulputate, quis commodo diam convallis. Aliquam fringilla est quis lacus elementum lobortis. Mauris pulvinar enim justo, ut sollicitudin eros pulvinar sit amet. Sed lobortis vestibulum eleifend. Morbi at mattis elit. In sit amet porttitor lacus, eget pharetra sapien. Proin elit metus, varius in dictum sed, ornare vel odio.
|
||||
|
||||
Suspendisse luctus viverra mollis. Vivamus est ante, egestas nec dignissim egestas, varius a libero. Phasellus id lacinia tellus. Sed pellentesque porttitor rutrum. Duis a mi sollicitudin, mollis purus et, fermentum ante. Nullam non metus et lacus vehicula ultricies eget non risus. Aenean ipsum dui, venenatis ut leo sed, ultricies volutpat tellus. Praesent consectetur id felis maximus suscipit. Nulla vel ipsum vel massa convallis ornare a vel felis. Aliquam convallis vestibulum ante.
|
||||
|
||||
Quisque eu pretium libero. Morbi volutpat velit ut est finibus, eu condimentum dui tristique. Cras dignissim tempus risus id gravida. Donec vitae lorem dolor. Aliquam et cursus lorem. Sed molestie dui nibh, et bibendum turpis elementum quis. Nunc consequat diam et tortor consectetur, eget sollicitudin urna mollis.
|
||||
|
||||
Cras vel est posuere ipsum viverra mollis. In lacus elit, interdum eget pretium non, dictum vitae dolor. Nulla arcu neque, sollicitudin varius iaculis in, gravida id justo. Phasellus ut condimentum justo. Aenean consectetur vitae sem quis rhoncus. Nam id nulla sit amet mauris ultricies bibendum et vitae quam. Mauris blandit metus in egestas egestas. Aenean erat ligula, consectetur nec elementum in, elementum quis quam. Quisque est arcu, auctor sed est eget, bibendum tristique mauris. Aliquam a varius purus, sit amet faucibus augue. Vestibulum a mattis lacus. Mauris eleifend, tortor sit amet fermentum lobortis, libero risus feugiat erat, sodales imperdiet magna nibh ut elit. In porta sapien urna, eget viverra magna egestas ut. Aliquam ac eleifend ligula. Morbi dignissim magna id varius pulvinar. Ut pulvinar commodo placerat.
|
||||
|
||||
In iaculis tortor id molestie scelerisque. Quisque congue tristique nisi bibendum efficitur. Proin rutrum ultrices tincidunt. Praesent faucibus, mauris in dictum tincidunt, quam ligula suscipit nisi, nec efficitur ligula mi sit amet purus. Donec porta, velit convallis elementum posuere, sem nibh ornare dui, eget semper purus erat dictum diam. Mauris congue eros tellus, eget bibendum purus blandit sit amet. Sed faucibus ipsum vel velit maximus, vitae lobortis lectus luctus. Morbi tempor lacus sit amet hendrerit auctor. Aliquam auctor maximus laoreet. Nulla viverra massa eu tortor porta molestie. Fusce quis nisi arcu. Duis sit amet vestibulum quam, in posuere nunc. Nulla non placerat tortor. Curabitur malesuada eget metus ac auctor. Interdum et malesuada fames ac ante ipsum primis in faucibus.
|
||||
|
||||
Donec bibendum libero nisl, eu varius est imperdiet id. Morbi sed arcu rhoncus, maximus libero eleifend, facilisis lacus. Sed sollicitudin, odio suscipit congue malesuada, justo magna finibus mauris, sed faucibus ipsum ligula ac augue. Sed sodales nulla mauris, efficitur bibendum sapien sagittis sit amet. Fusce consectetur justo diam, eu consequat nunc lacinia at. Curabitur ex ante, tempus at elementum non, maximus id lacus. Pellentesque in urna turpis. Proin tristique consequat lacus tincidunt faucibus. Proin ac tortor metus. Maecenas mollis blandit arcu, sit amet aliquam mauris laoreet sit amet. Nullam vulputate erat ac magna convallis efficitur. Proin cursus sodales est non suscipit. Nunc commodo lorem aliquet, commodo felis sit amet, dapibus orci. Nulla a enim vel ex facilisis laoreet. Donec malesuada laoreet turpis vel interdum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||
|
||||
Donec venenatis lectus eu tortor lobortis cursus. Quisque sit amet scelerisque quam, quis imperdiet ex. Morbi lacus purus, interdum non fringilla ut, ornare eget nisl. Morbi eleifend justo magna. Ut rutrum, nisl quis ultrices euismod, ligula arcu auctor quam, vel mattis ipsum lectus sit amet felis. Pellentesque vitae sagittis lorem. Morbi blandit finibus purus, quis commodo velit ultrices venenatis. Pellentesque eu erat enim. Fusce sollicitudin nunc nisi, non viverra neque aliquet at.
|
||||
|
||||
Curabitur bibendum vel augue eget viverra. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus id hendrerit quam, id tempor ex. Fusce quis sem dapibus, ornare leo ut, fringilla metus. Praesent molestie posuere scelerisque. Praesent ut libero vitae nisi iaculis venenatis maximus in leo. Donec sed ipsum massa. Aenean id purus tellus.
|
||||
|
||||
Maecenas varius pharetra dignissim. Nulla iaculis in elit a facilisis. Nam ipsum enim, tristique nec feugiat ut, tristique vel felis. Donec tincidunt suscipit justo, at dictum ante sollicitudin sed. Mauris nisi ipsum, faucibus id sollicitudin at, consequat eu nibh. Duis lorem lectus, vestibulum vel eros eget, efficitur semper ligula. Quisque tristique tortor eu eros fringilla scelerisque. Aenean nisl nisi, mollis non dolor in, sagittis blandit metus. Cras tellus turpis, volutpat in varius non, semper eget magna. Pellentesque finibus augue id fringilla cursus.
|
||||
|
||||
Fusce id est elit. Aliquam sit amet nunc finibus mi consectetur hendrerit in sit amet leo. Nunc posuere lectus ut est pellentesque, eu mollis tortor efficitur. Aliquam tincidunt placerat mauris non aliquam. Sed finibus dapibus tortor, sit amet fringilla libero tincidunt ut. Morbi at lectus volutpat, congue turpis sit amet, dignissim quam. Sed suscipit dolor vel nibh facilisis porta. Donec ut purus a leo volutpat pellentesque.
|
||||
|
||||
Mauris vitae ipsum bibendum, interdum metus nec, pharetra purus. Nulla facilisi. Donec dolor ligula, iaculis vitae tempor quis, euismod non tortor. Phasellus condimentum accumsan augue, vel hendrerit nisi dictum id. Nullam eget ante sit amet turpis tempus finibus. Morbi a dolor sollicitudin, sollicitudin sem a, placerat massa. Aenean consectetur elit non aliquet lacinia. Maecenas lobortis ex turpis, vel sollicitudin dolor elementum in. Sed sit amet facilisis elit. Duis iaculis faucibus ante, ac bibendum ex placerat id. Nulla facilisi. Praesent sollicitudin ut velit at interdum. Nulla viverra volutpat lorem. Sed mattis gravida auctor. Nam pharetra erat id convallis luctus. Vestibulum congue est lorem, eget fermentum magna consequat eu.
|
||||
|
||||
Sed ac luctus lectus, egestas gravida lorem. Suspendisse a tortor et velit mattis efficitur. Integer ornare accumsan justo nec molestie. Donec leo nunc, ultrices a consequat ut, bibendum at nisl. Mauris varius sem ac malesuada pharetra. Quisque bibendum diam at interdum molestie. Mauris ac ante sapien. Integer ultricies vitae justo eu dictum. Nulla facilisi. Etiam aliquet tellus id dignissim pharetra.
|
||||
|
||||
Nam sed nibh ex. Phasellus faucibus ante sapien, sit amet tristique elit posuere mattis. Proin porta vitae enim sed auctor. Vestibulum quis ex euismod, imperdiet ante iaculis, vehicula mi. Vivamus convallis augue pellentesque commodo bibendum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer gravida vitae leo vel varius. Aenean porta vehicula lorem, vitae commodo turpis sagittis eu. Aenean mattis elit et mi sagittis vulputate. Morbi vitae nisi quis mauris malesuada scelerisque. Morbi id efficitur quam. Ut id elit quam. Fusce at nisi fringilla, pharetra diam ut, suscipit erat.
|
||||
@@ -1,32 +1,33 @@
|
||||
// Package headers provides middleware that appends headers to
|
||||
// Package header provides middleware that appends headers to
|
||||
// requests based on a set of configuration rules that define
|
||||
// which routes receive which headers.
|
||||
package headers
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"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 middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface and serves requests,
|
||||
// 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, "")
|
||||
for _, rule := range h.Rules {
|
||||
if middleware.Path(r.URL.Path).Matches(rule.Path) {
|
||||
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||
for _, header := range rule.Headers {
|
||||
if strings.HasPrefix(header.Name, "-") {
|
||||
w.Header().Del(strings.TrimLeft(header.Name, "-"))
|
||||
} else {
|
||||
w.Header().Set(header.Name, header.Value)
|
||||
w.Header().Set(header.Name, replacer.Replace(header.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
package headers
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
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
|
||||
@@ -17,16 +22,18 @@ func TestHeaders(t *testing.T) {
|
||||
{"/a", "Foo", "Bar"},
|
||||
{"/a", "Bar", ""},
|
||||
{"/a", "Baz", ""},
|
||||
{"/a", "ServerName", hostname},
|
||||
{"/b", "Foo", ""},
|
||||
{"/b", "Bar", "Removed in /a"},
|
||||
} {
|
||||
he := Headers{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
}),
|
||||
Rules: []Rule{
|
||||
{Path: "/a", Headers: []Header{
|
||||
{Name: "Foo", Value: "Bar"},
|
||||
{Name: "ServerName", Value: "{hostname}"},
|
||||
{Name: "-Bar"},
|
||||
}},
|
||||
},
|
||||
@@ -1,27 +1,36 @@
|
||||
package setup
|
||||
package header
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Headers configures a new Headers middleware instance.
|
||||
func Headers(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return headers.Headers{Next: next, Rules: rules}
|
||||
}, nil
|
||||
func init() {
|
||||
caddy.RegisterPlugin("header", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
var rules []headers.Rule
|
||||
// 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 headers.Rule
|
||||
var head Rule
|
||||
var isNewPattern bool
|
||||
|
||||
if !c.NextArg() {
|
||||
@@ -46,7 +55,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
for c.NextBlock() {
|
||||
// A block of headers was opened...
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
h := Header{Name: c.Val()}
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
@@ -57,7 +66,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
if c.NextArg() {
|
||||
// ... or single header was defined as an argument instead.
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
h := Header{Name: c.Val()}
|
||||
|
||||
h.Value = c.Val()
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
package setup
|
||||
package header
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
c := NewTestController(`header / Foo Bar`)
|
||||
|
||||
mid, err := Headers(c)
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("http", `header / Foo Bar`)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
|
||||
mids := httpserver.GetConfig(c).Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, had 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(headers.Headers)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Headers)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
@@ -33,26 +35,25 @@ func TestHeadersParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []headers.Rule
|
||||
expected []Rule
|
||||
}{
|
||||
{`header /foo Foo "Bar Baz"`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/foo", Headers: []headers.Header{
|
||||
{"Foo", "Bar Baz"},
|
||||
false, []Rule{
|
||||
{Path: "/foo", Headers: []Header{
|
||||
{Name: "Foo", Value: "Bar Baz"},
|
||||
}},
|
||||
}},
|
||||
{`header /bar { Foo "Bar Baz" Baz Qux }`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/bar", Headers: []headers.Header{
|
||||
{"Foo", "Bar Baz"},
|
||||
{"Baz", "Qux"},
|
||||
false, []Rule{
|
||||
{Path: "/bar", Headers: []Header{
|
||||
{Name: "Foo", Value: "Bar Baz"},
|
||||
{Name: "Baz", Value: "Qux"},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := headersParse(c)
|
||||
actual, err := headersParse(caddy.NewTestController("http", test.input))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
@@ -0,0 +1,199 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
// SetupIfMatcher parses `if` or `if_op` in the current dispenser block.
|
||||
// It returns a RequestMatcher and an error if any.
|
||||
func SetupIfMatcher(c caddyfile.Dispenser) (RequestMatcher, error) {
|
||||
var matcher IfMatcher
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "if":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) != 3 {
|
||||
return matcher, c.ArgErr()
|
||||
}
|
||||
ifc, err := newIfCond(args1[0], args1[1], args1[2])
|
||||
if err != nil {
|
||||
return matcher, err
|
||||
}
|
||||
matcher.ifs = append(matcher.ifs, ifc)
|
||||
case "if_op":
|
||||
if !c.NextArg() {
|
||||
return matcher, c.ArgErr()
|
||||
}
|
||||
switch c.Val() {
|
||||
case "and":
|
||||
matcher.isOr = false
|
||||
case "or":
|
||||
matcher.isOr = true
|
||||
default:
|
||||
return matcher, c.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
// operators
|
||||
const (
|
||||
isOp = "is"
|
||||
notOp = "not"
|
||||
hasOp = "has"
|
||||
notHasOp = "not_has"
|
||||
startsWithOp = "starts_with"
|
||||
endsWithOp = "ends_with"
|
||||
matchOp = "match"
|
||||
notMatchOp = "not_match"
|
||||
)
|
||||
|
||||
func operatorError(operator string) error {
|
||||
return fmt.Errorf("Invalid operator %v", operator)
|
||||
}
|
||||
|
||||
// ifCondition is a 'if' condition.
|
||||
type ifCondition func(string, string) bool
|
||||
|
||||
var ifConditions = map[string]ifCondition{
|
||||
isOp: isFunc,
|
||||
notOp: notFunc,
|
||||
hasOp: hasFunc,
|
||||
notHasOp: notHasFunc,
|
||||
startsWithOp: startsWithFunc,
|
||||
endsWithOp: endsWithFunc,
|
||||
matchOp: matchFunc,
|
||||
notMatchOp: notMatchFunc,
|
||||
}
|
||||
|
||||
// isFunc is condition for Is operator.
|
||||
// It checks for equality.
|
||||
func isFunc(a, b string) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
// notFunc is condition for Not operator.
|
||||
// It checks for inequality.
|
||||
func notFunc(a, b string) bool {
|
||||
return a != b
|
||||
}
|
||||
|
||||
// hasFunc is condition for Has operator.
|
||||
// It checks if b is a substring of a.
|
||||
func hasFunc(a, b string) bool {
|
||||
return strings.Contains(a, b)
|
||||
}
|
||||
|
||||
// notHasFunc is condition for NotHas operator.
|
||||
// It checks if b is not a substring of a.
|
||||
func notHasFunc(a, b string) bool {
|
||||
return !strings.Contains(a, b)
|
||||
}
|
||||
|
||||
// startsWithFunc is condition for StartsWith operator.
|
||||
// It checks if b is a prefix of a.
|
||||
func startsWithFunc(a, b string) bool {
|
||||
return strings.HasPrefix(a, b)
|
||||
}
|
||||
|
||||
// endsWithFunc is condition for EndsWith operator.
|
||||
// It checks if b is a suffix of a.
|
||||
func endsWithFunc(a, b string) bool {
|
||||
return strings.HasSuffix(a, b)
|
||||
}
|
||||
|
||||
// matchFunc is condition for Match operator.
|
||||
// It does regexp matching of a against pattern in b
|
||||
// and returns if they match.
|
||||
func matchFunc(a, b string) bool {
|
||||
matched, _ := regexp.MatchString(b, a)
|
||||
return matched
|
||||
}
|
||||
|
||||
// notMatchFunc is condition for NotMatch operator.
|
||||
// It does regexp matching of a against pattern in b
|
||||
// and returns if they do not match.
|
||||
func notMatchFunc(a, b string) bool {
|
||||
matched, _ := regexp.MatchString(b, a)
|
||||
return !matched
|
||||
}
|
||||
|
||||
// ifCond is statement for a IfMatcher condition.
|
||||
type ifCond struct {
|
||||
a string
|
||||
op string
|
||||
b string
|
||||
}
|
||||
|
||||
// newIfCond creates a new If condition.
|
||||
func newIfCond(a, operator, b string) (ifCond, error) {
|
||||
if _, ok := ifConditions[operator]; !ok {
|
||||
return ifCond{}, operatorError(operator)
|
||||
}
|
||||
return ifCond{
|
||||
a: a,
|
||||
op: operator,
|
||||
b: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// True returns true if the condition is true and false otherwise.
|
||||
// If r is not nil, it replaces placeholders before comparison.
|
||||
func (i ifCond) True(r *http.Request) bool {
|
||||
if c, ok := ifConditions[i.op]; ok {
|
||||
a, b := i.a, i.b
|
||||
if r != nil {
|
||||
replacer := NewReplacer(r, nil, "")
|
||||
a = replacer.Replace(i.a)
|
||||
b = replacer.Replace(i.b)
|
||||
}
|
||||
return c(a, b)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IfMatcher is a RequestMatcher for 'if' conditions.
|
||||
type IfMatcher struct {
|
||||
ifs []ifCond // list of If
|
||||
isOr bool // if true, conditions are 'or' instead of 'and'
|
||||
}
|
||||
|
||||
// Match satisfies RequestMatcher interface.
|
||||
// It returns true if the conditions in m are true.
|
||||
func (m IfMatcher) Match(r *http.Request) bool {
|
||||
if m.isOr {
|
||||
return m.Or(r)
|
||||
}
|
||||
return m.And(r)
|
||||
}
|
||||
|
||||
// And returns true if all conditions in m are true.
|
||||
func (m IfMatcher) And(r *http.Request) bool {
|
||||
for _, i := range m.ifs {
|
||||
if !i.True(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Or returns true if any of the conditions in m is true.
|
||||
func (m IfMatcher) Or(r *http.Request) bool {
|
||||
for _, i := range m.ifs {
|
||||
if i.True(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IfMatcherKeyword returns if k is a keyword for 'if' config block.
|
||||
func IfMatcherKeyword(k string) bool {
|
||||
return k == "if" || k == "if_op"
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestConditions(t *testing.T) {
|
||||
tests := []struct {
|
||||
condition string
|
||||
isTrue bool
|
||||
}{
|
||||
{"a is b", false},
|
||||
{"a is a", true},
|
||||
{"a not b", true},
|
||||
{"a not a", false},
|
||||
{"a has a", true},
|
||||
{"a has b", false},
|
||||
{"ba has b", true},
|
||||
{"bab has b", true},
|
||||
{"bab has bb", false},
|
||||
{"a not_has a", false},
|
||||
{"a not_has b", true},
|
||||
{"ba not_has b", false},
|
||||
{"bab not_has b", false},
|
||||
{"bab not_has bb", true},
|
||||
{"bab starts_with bb", false},
|
||||
{"bab starts_with ba", true},
|
||||
{"bab starts_with bab", true},
|
||||
{"bab ends_with bb", false},
|
||||
{"bab ends_with bab", true},
|
||||
{"bab ends_with ab", true},
|
||||
{"a match *", false},
|
||||
{"a match a", true},
|
||||
{"a match .*", true},
|
||||
{"a match a.*", true},
|
||||
{"a match b.*", false},
|
||||
{"ba match b.*", true},
|
||||
{"ba match b[a-z]", true},
|
||||
{"b0 match b[a-z]", false},
|
||||
{"b0a match b[a-z]", false},
|
||||
{"b0a match b[a-z]+", false},
|
||||
{"b0a match b[a-z0-9]+", true},
|
||||
{"a not_match *", true},
|
||||
{"a not_match a", false},
|
||||
{"a not_match .*", false},
|
||||
{"a not_match a.*", false},
|
||||
{"a not_match b.*", true},
|
||||
{"ba not_match b.*", false},
|
||||
{"ba not_match b[a-z]", false},
|
||||
{"b0 not_match b[a-z]", true},
|
||||
{"b0a not_match b[a-z]", true},
|
||||
{"b0a not_match b[a-z]+", true},
|
||||
{"b0a not_match b[a-z0-9]+", false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
str := strings.Fields(test.condition)
|
||||
ifCond, err := newIfCond(str[0], str[1], str[2])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
isTrue := ifCond.True(nil)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue)
|
||||
}
|
||||
}
|
||||
|
||||
invalidOperators := []string{"ss", "and", "if"}
|
||||
for _, op := range invalidOperators {
|
||||
_, err := newIfCond("a", op, "b")
|
||||
if err == nil {
|
||||
t.Errorf("Invalid operator %v used, expected error.", op)
|
||||
}
|
||||
}
|
||||
|
||||
replaceTests := []struct {
|
||||
url string
|
||||
condition string
|
||||
isTrue bool
|
||||
}{
|
||||
{"/home", "{uri} match /home", true},
|
||||
{"/hom", "{uri} match /home", false},
|
||||
{"/hom", "{uri} starts_with /home", false},
|
||||
{"/hom", "{uri} starts_with /h", true},
|
||||
{"/home/.hiddenfile", `{uri} match \/\.(.*)`, true},
|
||||
{"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true},
|
||||
}
|
||||
|
||||
for i, test := range replaceTests {
|
||||
r, err := http.NewRequest("GET", test.url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
str := strings.Fields(test.condition)
|
||||
ifCond, err := newIfCond(str[0], str[1], str[2])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
isTrue := ifCond.True(r)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
conditions []string
|
||||
isOr bool
|
||||
isTrue bool
|
||||
}{
|
||||
{
|
||||
[]string{
|
||||
"a is a",
|
||||
"b is b",
|
||||
"c is c",
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"a is b",
|
||||
"b is c",
|
||||
"c is c",
|
||||
},
|
||||
true,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"a is a",
|
||||
"b is a",
|
||||
"c is c",
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{
|
||||
"a is b",
|
||||
"b is c",
|
||||
"c is a",
|
||||
},
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{},
|
||||
true,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
matcher := IfMatcher{isOr: test.isOr}
|
||||
for _, condition := range test.conditions {
|
||||
str := strings.Fields(condition)
|
||||
ifCond, err := newIfCond(str[0], str[1], str[2])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
matcher.ifs = append(matcher.ifs, ifCond)
|
||||
}
|
||||
isTrue := matcher.Match(nil)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupIfMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected IfMatcher
|
||||
}{
|
||||
{`test {
|
||||
if a match b
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "a", op: "match", b: "b"},
|
||||
},
|
||||
}},
|
||||
{`test {
|
||||
if a match b
|
||||
if_op or
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "a", op: "match", b: "b"},
|
||||
},
|
||||
isOr: true,
|
||||
}},
|
||||
{`test {
|
||||
if a match
|
||||
}`, true, IfMatcher{},
|
||||
},
|
||||
{`test {
|
||||
if a isnt b
|
||||
}`, true, IfMatcher{},
|
||||
},
|
||||
{`test {
|
||||
if a match b c
|
||||
}`, true, IfMatcher{},
|
||||
},
|
||||
{`test {
|
||||
if goal has go
|
||||
if cook not_has go
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "goal", op: "has", b: "go"},
|
||||
{a: "cook", op: "not_has", b: "go"},
|
||||
},
|
||||
}},
|
||||
{`test {
|
||||
if goal has go
|
||||
if cook not_has go
|
||||
if_op and
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "goal", op: "has", b: "go"},
|
||||
{a: "cook", op: "not_has", b: "go"},
|
||||
},
|
||||
}},
|
||||
{`test {
|
||||
if goal has go
|
||||
if cook not_has go
|
||||
if_op not
|
||||
}`, true, IfMatcher{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("http", test.input)
|
||||
c.Next()
|
||||
matcher, err := SetupIfMatcher(c.Dispenser)
|
||||
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 _, ok := matcher.(IfMatcher); !ok {
|
||||
t.Error("RequestMatcher should be of type IfMatcher")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got: %v", err)
|
||||
}
|
||||
if fmt.Sprint(matcher) != fmt.Sprint(test.expected) {
|
||||
t.Errorf("Test %v: Expected %v, found %v", i,
|
||||
fmt.Sprint(test.expected), fmt.Sprint(matcher))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfMatcherKeyword(t *testing.T) {
|
||||
tests := []struct {
|
||||
keyword string
|
||||
expected bool
|
||||
}{
|
||||
{"if", true},
|
||||
{"ifs", false},
|
||||
{"tls", false},
|
||||
{"http", false},
|
||||
{"if_op", true},
|
||||
{"if_type", false},
|
||||
{"if_cond", false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
valid := IfMatcherKeyword(test.keyword)
|
||||
if valid != test.expected {
|
||||
t.Errorf("Test %d: expected %v found %v", i, test.expected, valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// This file contains the context and functions available for
|
||||
@@ -18,35 +22,12 @@ import (
|
||||
type Context struct {
|
||||
Root http.FileSystem
|
||||
Req *http.Request
|
||||
// This is used to access information about the URL.
|
||||
URL *url.URL
|
||||
URL *url.URL
|
||||
}
|
||||
|
||||
// Include returns the contents of filename relative to the site root
|
||||
// Include returns the contents of filename relative to the site root.
|
||||
func (c Context) Include(filename string) (string, error) {
|
||||
file, err := c.Root.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tpl, err := template.New(filename).Parse(string(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
return ContextInclude(filename, c, c.Root)
|
||||
}
|
||||
|
||||
// Now returns the current timestamp in the specified format.
|
||||
@@ -97,6 +78,10 @@ func (c Context) URI() string {
|
||||
func (c Context) Host() (string, error) {
|
||||
host, _, err := net.SplitHostPort(c.Req.Host)
|
||||
if err != nil {
|
||||
if !strings.Contains(c.Req.Host, ":") {
|
||||
// common with sites served on the default port 80
|
||||
return c.Req.Host, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return host, nil
|
||||
@@ -106,6 +91,10 @@ func (c Context) Host() (string, error) {
|
||||
func (c Context) Port() (string, error) {
|
||||
_, port, err := net.SplitHostPort(c.Req.Host)
|
||||
if err != nil {
|
||||
if !strings.Contains(c.Req.Host, ":") {
|
||||
// common with sites served on the default port 80
|
||||
return "80", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return port, nil
|
||||
@@ -122,10 +111,16 @@ func (c Context) PathMatches(pattern string) bool {
|
||||
return Path(c.Req.URL.Path).Matches(pattern)
|
||||
}
|
||||
|
||||
// Truncate truncates the input string to the given length. If
|
||||
// input is shorter than length, the entire string is returned.
|
||||
// Truncate truncates the input string to the given length.
|
||||
// If length is negative, it returns that many characters
|
||||
// starting from the end of the string. If the absolute value
|
||||
// of length is greater than len(input), the whole input is
|
||||
// returned.
|
||||
func (c Context) Truncate(input string, length int) string {
|
||||
if len(input) > length {
|
||||
if length < 0 && len(input)+length > 0 {
|
||||
return input[len(input)+length:]
|
||||
}
|
||||
if length >= 0 && len(input) > length {
|
||||
return input[:length]
|
||||
}
|
||||
return input
|
||||
@@ -160,11 +155,17 @@ func (c Context) StripHTML(s string) string {
|
||||
if inTag {
|
||||
// false start
|
||||
buf.WriteString(s[tagStart:])
|
||||
inTag = false
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Ext returns the suffix beginning at the final dot in the final
|
||||
// slash-separated element of the pathStr (or in other words, the
|
||||
// file extension).
|
||||
func (c Context) Ext(pathStr string) string {
|
||||
return path.Ext(pathStr)
|
||||
}
|
||||
|
||||
// StripExt returns the input string without the extension,
|
||||
// which is the suffix starting with the final '.' character
|
||||
// but not before the final path separator ('/') character.
|
||||
@@ -182,3 +183,89 @@ func (c Context) StripExt(path string) string {
|
||||
func (c Context) Replace(input, find, replacement string) string {
|
||||
return strings.Replace(input, find, replacement, -1)
|
||||
}
|
||||
|
||||
// Markdown returns the HTML contents of the markdown contained in filename
|
||||
// (relative to the site root).
|
||||
func (c Context) Markdown(filename string) (string, error) {
|
||||
body, err := c.Include(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
renderer := blackfriday.HtmlRenderer(0, "", "")
|
||||
extns := 0
|
||||
extns |= blackfriday.EXTENSION_TABLES
|
||||
extns |= blackfriday.EXTENSION_FENCED_CODE
|
||||
extns |= blackfriday.EXTENSION_STRIKETHROUGH
|
||||
extns |= blackfriday.EXTENSION_DEFINITION_LISTS
|
||||
markdown := blackfriday.Markdown([]byte(body), renderer, extns)
|
||||
|
||||
return string(markdown), nil
|
||||
}
|
||||
|
||||
// ContextInclude opens filename using fs and executes a template with the context ctx.
|
||||
// This does the same thing that Context.Include() does, but with the ability to provide
|
||||
// your own context so that the included files can have access to additional fields your
|
||||
// type may provide. You can embed Context in your type, then override its Include method
|
||||
// to call this function with ctx being the instance of your type, and fs being Context.Root.
|
||||
func ContextInclude(filename string, ctx interface{}, fs http.FileSystem) (string, error) {
|
||||
file, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tpl, err := template.New(filename).Parse(string(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// ToLower will convert the given string to lower case.
|
||||
func (c Context) ToLower(s string) string {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// ToUpper will convert the given string to upper case.
|
||||
func (c Context) ToUpper(s string) string {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
// Split is a passthrough to strings.Split. It will split the first argument at each instance of the separator and return a slice of strings.
|
||||
func (c Context) Split(s string, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
// Slice will convert the given arguments into a slice.
|
||||
func (c Context) Slice(elems ...interface{}) []interface{} {
|
||||
return elems
|
||||
}
|
||||
|
||||
// Map will convert the arguments into a map. It expects alternating string keys and values. This is useful for building more complicated data structures
|
||||
// if you are using subtemplates or things like that.
|
||||
func (c Context) Map(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, fmt.Errorf("Map expects an even number of arguments")
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Map keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func TestInclude(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
inputFilename := "test_file"
|
||||
absInFilePath := filepath.Join(fmt.Sprintf("%s", context.Root), inputFilename)
|
||||
defer func() {
|
||||
err := os.Remove(absInFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
t.Fatalf("Failed to clean test file!")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
fileContent string
|
||||
expectedContent string
|
||||
shouldErr bool
|
||||
expectedErrorContent string
|
||||
}{
|
||||
// Test 0 - all good
|
||||
{
|
||||
fileContent: `str1 {{ .Root }} str2`,
|
||||
expectedContent: fmt.Sprintf("str1 %s str2", context.Root),
|
||||
shouldErr: false,
|
||||
expectedErrorContent: "",
|
||||
},
|
||||
// Test 1 - failure on template.Parse
|
||||
{
|
||||
fileContent: `str1 {{ .Root } str2`,
|
||||
expectedContent: "",
|
||||
shouldErr: true,
|
||||
expectedErrorContent: `unexpected "}" in operand`,
|
||||
},
|
||||
// Test 3 - failure on template.Execute
|
||||
{
|
||||
fileContent: `str1 {{ .InvalidField }} str2`,
|
||||
expectedContent: "",
|
||||
shouldErr: true,
|
||||
expectedErrorContent: `InvalidField`,
|
||||
},
|
||||
{
|
||||
fileContent: `str1 {{ .InvalidField }} str2`,
|
||||
expectedContent: "",
|
||||
shouldErr: true,
|
||||
expectedErrorContent: `type httpserver.Context`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// WriteFile truncates the contentt
|
||||
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
|
||||
}
|
||||
|
||||
content, err := context.Include(inputFilename)
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf(testPrefix+"Expected no error, found [%s]", test.expectedErrorContent, err.Error())
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.expectedErrorContent) {
|
||||
t.Errorf(testPrefix+"Expected error content [%s], found [%s]", test.expectedErrorContent, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf(testPrefix+"Expected error [%s] but found nil. Input file was: %s", test.expectedErrorContent, inputFilename)
|
||||
}
|
||||
|
||||
if content != test.expectedContent {
|
||||
t.Errorf(testPrefix+"Expected content [%s] but found [%s]. Input file was: %s", test.expectedContent, content, inputFilename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeNotExisting(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
_, err := context.Include("not_existing")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but found nil!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
inputFilename := "test_file"
|
||||
absInFilePath := filepath.Join(fmt.Sprintf("%s", context.Root), inputFilename)
|
||||
defer func() {
|
||||
err := os.Remove(absInFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
t.Fatalf("Failed to clean test file!")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
fileContent string
|
||||
expectedContent string
|
||||
}{
|
||||
// Test 0 - test parsing of markdown
|
||||
{
|
||||
fileContent: "* str1\n* str2\n",
|
||||
expectedContent: "<ul>\n<li>str1</li>\n<li>str2</li>\n</ul>\n",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// WriteFile truncates the contentt
|
||||
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
|
||||
}
|
||||
|
||||
content, _ := context.Markdown(inputFilename)
|
||||
if content != test.expectedContent {
|
||||
t.Errorf(testPrefix+"Expected content [%s] but found [%s]. Input file was: %s", test.expectedContent, content, inputFilename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookie(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
cookie *http.Cookie
|
||||
cookieName string
|
||||
expectedValue string
|
||||
}{
|
||||
// Test 0 - happy path
|
||||
{
|
||||
cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
|
||||
cookieName: "cookieName",
|
||||
expectedValue: "cookieValue",
|
||||
},
|
||||
// Test 1 - try to get a non-existing cookie
|
||||
{
|
||||
cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
|
||||
cookieName: "notExisting",
|
||||
expectedValue: "",
|
||||
},
|
||||
// Test 2 - partial name match
|
||||
{
|
||||
cookie: &http.Cookie{Name: "cookie", Value: "cookieValue"},
|
||||
cookieName: "cook",
|
||||
expectedValue: "",
|
||||
},
|
||||
// Test 3 - cookie with optional fields
|
||||
{
|
||||
cookie: &http.Cookie{Name: "cookie", Value: "cookieValue", Path: "/path", Domain: "https://localhost", Expires: (time.Now().Add(10 * time.Minute)), MaxAge: 120},
|
||||
cookieName: "cookie",
|
||||
expectedValue: "cookieValue",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// reinitialize the context for each test
|
||||
context := getContextOrFail(t)
|
||||
|
||||
context.Req.AddCookie(test.cookie)
|
||||
|
||||
actualCookieVal := context.Cookie(test.cookieName)
|
||||
|
||||
if actualCookieVal != test.expectedValue {
|
||||
t.Errorf(testPrefix+"Expected cookie value [%s] but found [%s] for cookie with name %s", test.expectedValue, actualCookieVal, test.cookieName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieMultipleCookies(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
cookieNameBase, cookieValueBase := "cookieName", "cookieValue"
|
||||
|
||||
// make sure that there's no state and multiple requests for different cookies return the correct result
|
||||
for i := 0; i < 10; i++ {
|
||||
context.Req.AddCookie(&http.Cookie{Name: fmt.Sprintf("%s%d", cookieNameBase, i), Value: fmt.Sprintf("%s%d", cookieValueBase, i)})
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
expectedCookieVal := fmt.Sprintf("%s%d", cookieValueBase, i)
|
||||
actualCookieVal := context.Cookie(fmt.Sprintf("%s%d", cookieNameBase, i))
|
||||
if actualCookieVal != expectedCookieVal {
|
||||
t.Fatalf("Expected cookie value %s, found %s", expectedCookieVal, actualCookieVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
headerKey, headerVal := "Header1", "HeaderVal1"
|
||||
context.Req.Header.Add(headerKey, headerVal)
|
||||
|
||||
actualHeaderVal := context.Header(headerKey)
|
||||
if actualHeaderVal != headerVal {
|
||||
t.Errorf("Expected header %s, found %s", headerVal, actualHeaderVal)
|
||||
}
|
||||
|
||||
missingHeaderVal := context.Header("not-existing")
|
||||
if missingHeaderVal != "" {
|
||||
t.Errorf("Expected empty header value, found %s", missingHeaderVal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIP(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
tests := []struct {
|
||||
inputRemoteAddr string
|
||||
expectedIP string
|
||||
}{
|
||||
// Test 0 - ipv4 with port
|
||||
{"1.1.1.1:1111", "1.1.1.1"},
|
||||
// Test 1 - ipv4 without port
|
||||
{"1.1.1.1", "1.1.1.1"},
|
||||
// Test 2 - ipv6 with port
|
||||
{"[::1]:11", "::1"},
|
||||
// Test 3 - ipv6 without port and brackets
|
||||
{"[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]"},
|
||||
// Test 4 - ipv6 with zone and port
|
||||
{`[fe80:1::3%eth0]:44`, `fe80:1::3%eth0`},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
context.Req.RemoteAddr = test.inputRemoteAddr
|
||||
actualIP := context.IP()
|
||||
|
||||
if actualIP != test.expectedIP {
|
||||
t.Errorf(testPrefix+"Expected IP %s, found %s", test.expectedIP, actualIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
inputURL := "http://localhost"
|
||||
context.Req.RequestURI = inputURL
|
||||
|
||||
if inputURL != context.URI() {
|
||||
t.Errorf("Expected url %s, found %s", inputURL, context.URI())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHost(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedHost string
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
input: "localhost:123",
|
||||
expectedHost: "localhost",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
input: "localhost",
|
||||
expectedHost: "localhost",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
input: "[::]",
|
||||
expectedHost: "",
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testHostOrPort(t, true, test.input, test.expectedHost, test.shouldErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedPort string
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
input: "localhost:123",
|
||||
expectedPort: "123",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
input: "localhost",
|
||||
expectedPort: "80", // assuming 80 is the default port
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
input: ":8080",
|
||||
expectedPort: "8080",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
input: "[::]",
|
||||
expectedPort: "",
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
testHostOrPort(t, false, test.input, test.expectedPort, test.shouldErr)
|
||||
}
|
||||
}
|
||||
|
||||
func testHostOrPort(t *testing.T, isTestingHost bool, input, expectedResult string, shouldErr bool) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
context.Req.Host = input
|
||||
var actualResult, testedObject string
|
||||
var err error
|
||||
|
||||
if isTestingHost {
|
||||
actualResult, err = context.Host()
|
||||
testedObject = "host"
|
||||
} else {
|
||||
actualResult, err = context.Port()
|
||||
testedObject = "port"
|
||||
}
|
||||
|
||||
if shouldErr && err == nil {
|
||||
t.Errorf("Expected error, found nil!")
|
||||
return
|
||||
}
|
||||
|
||||
if !shouldErr && err != nil {
|
||||
t.Errorf("Expected no error, found %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if actualResult != expectedResult {
|
||||
t.Errorf("Expected %s %s, found %s", testedObject, expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethod(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
method := "POST"
|
||||
context.Req.Method = method
|
||||
|
||||
if method != context.Method() {
|
||||
t.Errorf("Expected method %s, found %s", method, context.Method())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPathMatches(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
tests := []struct {
|
||||
urlStr string
|
||||
pattern string
|
||||
shouldMatch bool
|
||||
}{
|
||||
// Test 0
|
||||
{
|
||||
urlStr: "http://localhost/",
|
||||
pattern: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
// Test 1
|
||||
{
|
||||
urlStr: "http://localhost",
|
||||
pattern: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
// Test 1
|
||||
{
|
||||
urlStr: "http://localhost/",
|
||||
pattern: "/",
|
||||
shouldMatch: true,
|
||||
},
|
||||
// Test 3
|
||||
{
|
||||
urlStr: "http://localhost/?param=val",
|
||||
pattern: "/",
|
||||
shouldMatch: true,
|
||||
},
|
||||
// Test 4
|
||||
{
|
||||
urlStr: "http://localhost/dir1/dir2",
|
||||
pattern: "/dir2",
|
||||
shouldMatch: false,
|
||||
},
|
||||
// Test 5
|
||||
{
|
||||
urlStr: "http://localhost/dir1/dir2",
|
||||
pattern: "/dir1",
|
||||
shouldMatch: true,
|
||||
},
|
||||
// Test 6
|
||||
{
|
||||
urlStr: "http://localhost:444/dir1/dir2",
|
||||
pattern: "/dir1",
|
||||
shouldMatch: true,
|
||||
},
|
||||
// Test 7
|
||||
{
|
||||
urlStr: "http://localhost/dir1/dir2",
|
||||
pattern: "*/dir2",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
var err error
|
||||
context.Req.URL, err = url.Parse(test.urlStr)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare test URL from string %s! Error was: %s", test.urlStr, err)
|
||||
}
|
||||
|
||||
matches := context.PathMatches(test.pattern)
|
||||
if matches != test.shouldMatch {
|
||||
t.Errorf(testPrefix+"Expected and actual result differ: expected to match [%t], actual matches [%t]", test.shouldMatch, matches)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
tests := []struct {
|
||||
inputString string
|
||||
inputLength int
|
||||
expected string
|
||||
}{
|
||||
// Test 0 - small length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: 1,
|
||||
expected: "s",
|
||||
},
|
||||
// Test 1 - exact length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: 6,
|
||||
expected: "string",
|
||||
},
|
||||
// Test 2 - bigger length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: 10,
|
||||
expected: "string",
|
||||
},
|
||||
// Test 3 - zero length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: 0,
|
||||
expected: "",
|
||||
},
|
||||
// Test 4 - negative, smaller length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: -5,
|
||||
expected: "tring",
|
||||
},
|
||||
// Test 5 - negative, exact length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: -6,
|
||||
expected: "string",
|
||||
},
|
||||
// Test 6 - negative, bigger length
|
||||
{
|
||||
inputString: "string",
|
||||
inputLength: -7,
|
||||
expected: "string",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actual := context.Truncate(test.inputString, test.inputLength)
|
||||
if actual != test.expected {
|
||||
t.Errorf(getTestPrefix(i)+"Expected '%s', found '%s'. Input was Truncate(%q, %d)", test.expected, actual, test.inputString, test.inputLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripHTML(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
// Test 0 - no tags
|
||||
{
|
||||
input: `h1`,
|
||||
expected: `h1`,
|
||||
},
|
||||
// Test 1 - happy path
|
||||
{
|
||||
input: `<h1>h1</h1>`,
|
||||
expected: `h1`,
|
||||
},
|
||||
// Test 2 - tag in quotes
|
||||
{
|
||||
input: `<h1">">h1</h1>`,
|
||||
expected: `h1`,
|
||||
},
|
||||
// Test 3 - multiple tags
|
||||
{
|
||||
input: `<h1><b>h1</b></h1>`,
|
||||
expected: `h1`,
|
||||
},
|
||||
// Test 4 - tags not closed
|
||||
{
|
||||
input: `<h1`,
|
||||
expected: `<h1`,
|
||||
},
|
||||
// Test 5 - false start
|
||||
{
|
||||
input: `<h1<b>hi`,
|
||||
expected: `<h1hi`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actual := context.StripHTML(test.input)
|
||||
if actual != test.expected {
|
||||
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was StripHTML(%s)", test.expected, actual, test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripExt(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
// Test 0 - empty input
|
||||
{
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
// Test 1 - relative file with ext
|
||||
{
|
||||
input: "file.ext",
|
||||
expected: "file",
|
||||
},
|
||||
// Test 2 - relative file without ext
|
||||
{
|
||||
input: "file",
|
||||
expected: "file",
|
||||
},
|
||||
// Test 3 - absolute file without ext
|
||||
{
|
||||
input: "/file",
|
||||
expected: "/file",
|
||||
},
|
||||
// Test 4 - absolute file with ext
|
||||
{
|
||||
input: "/file.ext",
|
||||
expected: "/file",
|
||||
},
|
||||
// Test 5 - with ext but ends with /
|
||||
{
|
||||
input: "/dir.ext/",
|
||||
expected: "/dir.ext/",
|
||||
},
|
||||
// Test 6 - file with ext under dir with ext
|
||||
{
|
||||
input: "/dir.ext/file.ext",
|
||||
expected: "/dir.ext/file",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actual := context.StripExt(test.input)
|
||||
if actual != test.expected {
|
||||
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was StripExt(%q)", test.expected, actual, test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initTestContext() (Context, error) {
|
||||
body := bytes.NewBufferString("request body")
|
||||
request, err := http.NewRequest("GET", "https://localhost", body)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
return Context{Root: http.Dir(os.TempDir()), Req: request}, nil
|
||||
}
|
||||
|
||||
func getContextOrFail(t *testing.T) Context {
|
||||
context, err := initTestContext()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare test context")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
func getTestPrefix(testN int) string {
|
||||
return fmt.Sprintf("Test [%d]: ", testN)
|
||||
}
|
||||
|
||||
func TestTemplates(t *testing.T) {
|
||||
tests := []struct{ tmpl, expected string }{
|
||||
{`{{.ToUpper "aAA"}}`, "AAA"},
|
||||
{`{{"bbb" | .ToUpper}}`, "BBB"},
|
||||
{`{{.ToLower "CCc"}}`, "ccc"},
|
||||
{`{{range (.Split "a,b,c" ",")}}{{.}}{{end}}`, "abc"},
|
||||
{`{{range .Split "a,b,c" ","}}{{.}}{{end}}`, "abc"},
|
||||
{`{{range .Slice "a" "b" "c"}}{{.}}{{end}}`, "abc"},
|
||||
{`{{with .Map "A" "a" "B" "b" "c" "d"}}{{.A}}{{.B}}{{.c}}{{end}}`, "abd"},
|
||||
}
|
||||
for i, test := range tests {
|
||||
ctx := getContextOrFail(t)
|
||||
tmpl, err := template.New("").Parse(test.tmpl)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %s", i, err)
|
||||
continue
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
err = tmpl.Execute(buf, ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %s", i, err)
|
||||
continue
|
||||
}
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Test %d: Results do not match. '%s' != '%s'", i, buf.String(), test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// TODO: Should this be a generic graceful listener available in its own package or something?
|
||||
// Also, passing in a WaitGroup is a little awkward. Why can't this listener just keep
|
||||
// the waitgroup internal to itself?
|
||||
|
||||
// newGracefulListener returns a gracefulListener that wraps l and
|
||||
// uses wg (stored in the host server) to count connections.
|
||||
func newGracefulListener(l net.Listener, wg *sync.WaitGroup) *gracefulListener {
|
||||
gl := &gracefulListener{Listener: l, stop: make(chan error), connWg: wg}
|
||||
go func() {
|
||||
<-gl.stop
|
||||
gl.Lock()
|
||||
gl.stopped = true
|
||||
gl.Unlock()
|
||||
gl.stop <- gl.Listener.Close()
|
||||
}()
|
||||
return gl
|
||||
}
|
||||
|
||||
// gracefuListener is a net.Listener which can
|
||||
// count the number of connections on it. Its
|
||||
// methods mainly wrap net.Listener to be graceful.
|
||||
type gracefulListener struct {
|
||||
net.Listener
|
||||
stop chan error
|
||||
stopped bool
|
||||
sync.Mutex // protects the stopped flag
|
||||
connWg *sync.WaitGroup // pointer to the host's wg used for counting connections
|
||||
}
|
||||
|
||||
// Accept accepts a connection.
|
||||
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
|
||||
c, err = gl.Listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c = gracefulConn{Conn: c, connWg: gl.connWg}
|
||||
gl.connWg.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
// Close immediately closes the listener.
|
||||
func (gl *gracefulListener) Close() error {
|
||||
gl.Lock()
|
||||
if gl.stopped {
|
||||
gl.Unlock()
|
||||
return syscall.EINVAL
|
||||
}
|
||||
gl.Unlock()
|
||||
gl.stop <- nil
|
||||
return <-gl.stop
|
||||
}
|
||||
|
||||
// gracefulConn represents a connection on a
|
||||
// gracefulListener so that we can keep track
|
||||
// of the number of connections, thus facilitating
|
||||
// a graceful shutdown.
|
||||
type gracefulConn struct {
|
||||
net.Conn
|
||||
connWg *sync.WaitGroup // pointer to the host server's connection waitgroup
|
||||
}
|
||||
|
||||
// Close closes c's underlying connection while updating the wg count.
|
||||
func (c gracefulConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// close can fail on http2 connections (as of Oct. 2015, before http2 in std lib)
|
||||
// so don't decrement count unless close succeeds
|
||||
c.connWg.Done()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func activateHTTPS(cctx caddy.Context) error {
|
||||
if !caddy.Quiet {
|
||||
fmt.Print("Activating privacy features...")
|
||||
}
|
||||
|
||||
ctx := cctx.(*httpContext)
|
||||
|
||||
// pre-screen each config and earmark the ones that qualify for managed TLS
|
||||
markQualifiedForAutoHTTPS(ctx.siteConfigs)
|
||||
|
||||
// place certificates and keys on disk
|
||||
for _, c := range ctx.siteConfigs {
|
||||
err := c.TLS.ObtainCert(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update TLS configurations
|
||||
err := enableAutoHTTPS(ctx.siteConfigs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set up redirects
|
||||
ctx.siteConfigs = makePlaintextRedirects(ctx.siteConfigs)
|
||||
|
||||
// renew all relevant certificates that need renewal. this is important
|
||||
// to do right away so we guarantee that renewals aren't missed, and
|
||||
// also the user can respond to any potential errors that occur.
|
||||
err = caddytls.RenewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !caddy.Quiet {
|
||||
fmt.Println(" done.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markQualifiedForAutoHTTPS scans each config and, if it
|
||||
// qualifies for managed TLS, it sets the Managed field of
|
||||
// the TLS config to true.
|
||||
func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
|
||||
for _, cfg := range configs {
|
||||
if caddytls.QualifiesForManagedTLS(cfg) && cfg.Addr.Scheme != "http" {
|
||||
cfg.TLS.Managed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enableAutoHTTPS configures each config to use TLS according to default settings.
|
||||
// It will only change configs that are marked as managed, and assumes that
|
||||
// certificates and keys are already on disk. If loadCertificates is true,
|
||||
// the certificates will be loaded from disk into the cache for this process
|
||||
// to use. If false, TLS will still be enabled and configured with default
|
||||
// settings, but no certificates will be parsed loaded into the cache, and
|
||||
// the returned error value will always be nil.
|
||||
func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
for _, cfg := range configs {
|
||||
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
cfg.TLS.Enabled = true
|
||||
cfg.Addr.Scheme = "https"
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.Addr.Host) {
|
||||
_, err := caddytls.CacheManagedCertificate(cfg.Addr.Host, cfg.TLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure any config values not explicitly set are set to default
|
||||
caddytls.SetDefaultTLSParams(cfg.TLS)
|
||||
|
||||
// Set default port of 443 if not explicitly set
|
||||
if cfg.Addr.Port == "" &&
|
||||
cfg.TLS.Enabled &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
|
||||
cfg.Addr.Host != "localhost" {
|
||||
cfg.Addr.Port = "443"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
|
||||
// hosts. You must pass in all configs, not just configs that qualify, since
|
||||
// we must know whether the same host already exists on port 80, and those would
|
||||
// not be in a list of configs that qualify for automatic HTTPS. This function will
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// hostHasOtherPort returns true if there is another config in the list with the same
|
||||
// hostname that has port otherPort, or false otherwise. All the configs are checked
|
||||
// against the hostname of allConfigs[thisConfigIdx].
|
||||
func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort string) bool {
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == thisConfigIdx {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Addr.Host == allConfigs[thisConfigIdx].Addr.Host &&
|
||||
otherCfg.Addr.Port == otherPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on port 80. The TLS field of cfg must not be nil.
|
||||
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
redirPort := cfg.Addr.Port
|
||||
if redirPort == "443" {
|
||||
// default port is redundant
|
||||
redirPort = ""
|
||||
}
|
||||
redirMiddleware := func(next Handler) Handler {
|
||||
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
toURL := "https://" + r.Host
|
||||
if redirPort != "" {
|
||||
toURL += ":" + redirPort
|
||||
}
|
||||
toURL += r.URL.RequestURI()
|
||||
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
host := cfg.Addr.Host
|
||||
port := "80"
|
||||
addr := net.JoinHostPort(host, port)
|
||||
return &SiteConfig{
|
||||
Addr: Address{Original: addr, Host: host, Port: port},
|
||||
ListenHost: cfg.ListenHost,
|
||||
middleware: []Middleware{redirMiddleware},
|
||||
TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(&SiteConfig{
|
||||
Addr: Address{
|
||||
Host: "example.com",
|
||||
Port: "1234",
|
||||
},
|
||||
ListenHost: "93.184.216.34",
|
||||
TLS: new(caddytls.Config),
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Addr.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected {
|
||||
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Addr.Port, "80"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.middleware == nil || len(cfg.middleware) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.middleware)
|
||||
}
|
||||
|
||||
handler := cfg.middleware[0](nil)
|
||||
|
||||
// Check redirect for correctness
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://foo/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err := handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Expected status return to be 0, but was %d", status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected returned error to be nil, but was %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), "https://foo:1234/bar?q=1"; got != want {
|
||||
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
|
||||
}
|
||||
|
||||
// browsers can infer a default port from scheme, so make sure the port
|
||||
// doesn't get added in explicitly for default ports like 443 for https.
|
||||
cfg = redirPlaintextHost(&SiteConfig{Addr: Address{Host: "example.com", Port: "443"}, TLS: new(caddytls.Config)})
|
||||
handler = cfg.middleware[0](nil)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "http://foo/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err = handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Expected status return to be 0, but was %d", status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected returned error to be nil, but was %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), "https://foo/bar?q=1"; got != want {
|
||||
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHasOtherPort(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com", Port: "80"}},
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "80"}},
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "443"}},
|
||||
}
|
||||
|
||||
if hostHasOtherPort(configs, 0, "80") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
|
||||
}
|
||||
if hostHasOtherPort(configs, 0, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
|
||||
}
|
||||
if !hostHasOtherPort(configs, 1, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePlaintextRedirects(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
// Happy path = standard redirect from 80 to 443
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Host on port 80 already defined; don't change it (no redirect)
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "80", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Redirect from port 80 to port 5000 in this case
|
||||
{Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Can redirect from 80 to either 443 or 5001, but choose 443
|
||||
{Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{Managed: true}},
|
||||
}
|
||||
|
||||
result := makePlaintextRedirects(configs)
|
||||
expectedRedirCount := 3
|
||||
|
||||
if len(result) != len(configs)+expectedRedirCount {
|
||||
t.Errorf("Expected %d redirect(s) to be added, but got %d",
|
||||
expectedRedirCount, len(result)-len(configs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAutoHTTPS(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
enableAutoHTTPS(configs, false)
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
}
|
||||
if configs[0].Addr.Scheme != "https" {
|
||||
t.Errorf("Expected config 0 to have Addr.Scheme == \"https\", but it was \"%s\"",
|
||||
configs[0].Addr.Scheme)
|
||||
}
|
||||
if configs[1].TLS != nil && configs[1].TLS.Enabled {
|
||||
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: ""}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com"}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
markQualifiedForAutoHTTPS(configs)
|
||||
|
||||
count := 0
|
||||
for _, cfg := range configs {
|
||||
if cfg.TLS.Managed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != expectedManagedCount {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCaseSettings()
|
||||
}
|
||||
|
||||
type (
|
||||
// Middleware is the middle layer which represents the traditional
|
||||
// idea of middleware: it chains one Handler to the next by being
|
||||
// passed the next Handler in the chain.
|
||||
Middleware func(Handler) Handler
|
||||
|
||||
// Handler is like http.Handler except ServeHTTP may return a status
|
||||
// code and/or error.
|
||||
//
|
||||
// If ServeHTTP writes to the response body, it should return a status
|
||||
// code of 0. This signals to other handlers above it that the response
|
||||
// body is already written, and that they should not write to it also.
|
||||
//
|
||||
// If ServeHTTP encounters an error, it should return the error value
|
||||
// so it can be logged by designated error-handling middleware.
|
||||
//
|
||||
// If writing a response after calling another ServeHTTP method, the
|
||||
// returned status code SHOULD be used when writing the response.
|
||||
//
|
||||
// If handling errors after calling another ServeHTTP method, the
|
||||
// returned error value SHOULD be logged or handled accordingly.
|
||||
//
|
||||
// Otherwise, return values should be propagated down the middleware
|
||||
// chain by returning them unchanged.
|
||||
Handler interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// HandlerFunc is a convenience type like http.HandlerFunc, except
|
||||
// ServeHTTP returns a status code and an error. See Handler
|
||||
// documentation for more information.
|
||||
HandlerFunc func(http.ResponseWriter, *http.Request) (int, error)
|
||||
|
||||
// RequestMatcher checks to see if current request should be handled
|
||||
// by underlying handler.
|
||||
RequestMatcher interface {
|
||||
Match(r *http.Request) bool
|
||||
}
|
||||
|
||||
// HandlerConfig is a middleware configuration.
|
||||
// This makes it possible for middlewares to have a common
|
||||
// configuration interface.
|
||||
//
|
||||
// TODO The long term plan is to get all middleware implement this
|
||||
// interface for configurations.
|
||||
HandlerConfig interface {
|
||||
RequestMatcher
|
||||
BasePath() string
|
||||
}
|
||||
|
||||
// ConfigSelector selects a configuration.
|
||||
ConfigSelector []HandlerConfig
|
||||
)
|
||||
|
||||
// ServeHTTP implements the Handler interface.
|
||||
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return f(w, r)
|
||||
}
|
||||
|
||||
// Select selects a Config.
|
||||
// This chooses the config with the longest length.
|
||||
func (c ConfigSelector) Select(r *http.Request) (config HandlerConfig) {
|
||||
for i := range c {
|
||||
if !c[i].Match(r) {
|
||||
continue
|
||||
}
|
||||
if config == nil || len(c[i].BasePath()) > len(config.BasePath()) {
|
||||
config = c[i]
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// IndexFile looks for a file in /root/fpath/indexFile for each string
|
||||
// in indexFiles. If an index file is found, it returns the root-relative
|
||||
// path to the file and true. If no index file is found, empty string
|
||||
// and false is returned. fpath must end in a forward slash '/'
|
||||
// otherwise no index files will be tried (directory paths must end
|
||||
// in a forward slash according to HTTP).
|
||||
//
|
||||
// All paths passed into and returned from this function use '/' as the
|
||||
// path separator, just like URLs. IndexFle handles path manipulation
|
||||
// internally for systems that use different path separators.
|
||||
func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) {
|
||||
if fpath[len(fpath)-1] != '/' || root == nil {
|
||||
return "", false
|
||||
}
|
||||
for _, indexFile := range indexFiles {
|
||||
// func (http.FileSystem).Open wants all paths separated by "/",
|
||||
// regardless of operating system convention, so use
|
||||
// path.Join instead of filepath.Join
|
||||
fp := path.Join(fpath, indexFile)
|
||||
f, err := root.Open(fp)
|
||||
if err == nil {
|
||||
f.Close()
|
||||
return fp, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// SetLastModifiedHeader checks if the provided modTime is valid and if it is sets it
|
||||
// as a Last-Modified header to the ResponseWriter. If the modTime is in the future
|
||||
// the current time is used instead.
|
||||
func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
|
||||
if modTime.IsZero() || modTime.Equal(time.Unix(0, 0)) {
|
||||
// the time does not appear to be valid. Don't put it in the response
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 2616 - Section 14.29 - Last-Modified:
|
||||
// An origin server MUST NOT send a Last-Modified date which is later than the
|
||||
// server's time of message origination. In such cases, where the resource's last
|
||||
// modification would indicate some time in the future, the server MUST replace
|
||||
// that date with the message origination date.
|
||||
now := currentTime()
|
||||
if modTime.After(now) {
|
||||
modTime = now
|
||||
}
|
||||
|
||||
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
// CaseSensitivePath determines if paths should be case sensitive.
|
||||
// This is configurable via CASE_SENSITIVE_PATH environment variable.
|
||||
var CaseSensitivePath = true
|
||||
|
||||
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
|
||||
// initCaseSettings loads case sensitivity config from environment variable.
|
||||
//
|
||||
// This could have been in init, but init cannot be called from tests.
|
||||
func initCaseSettings() {
|
||||
switch os.Getenv(caseSensitivePathEnv) {
|
||||
case "0", "false":
|
||||
CaseSensitivePath = false
|
||||
default:
|
||||
CaseSensitivePath = true
|
||||
}
|
||||
}
|
||||
|
||||
// MergeRequestMatchers merges multiple RequestMatchers into one.
|
||||
// This allows a middleware to use multiple RequestMatchers.
|
||||
func MergeRequestMatchers(matchers ...RequestMatcher) RequestMatcher {
|
||||
return requestMatchers(matchers)
|
||||
}
|
||||
|
||||
type requestMatchers []RequestMatcher
|
||||
|
||||
// Match satisfies RequestMatcher interface.
|
||||
func (m requestMatchers) Match(r *http.Request) bool {
|
||||
for _, matcher := range m {
|
||||
if !matcher.Match(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// currentTime, as it is defined here, returns time.Now().
|
||||
// It's defined as a variable for mocking time in tests.
|
||||
var currentTime = func() time.Time { return time.Now() }
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// Middleware functions so that the assignment to the
|
||||
// Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// plugins can use this as a convenience.
|
||||
var EmptyNext = HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { return 0, nil })
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// plugins can use this as a convenience.
|
||||
func SameNext(next1, next2 Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathCaseSensitivity(t *testing.T) {
|
||||
tests := []struct {
|
||||
basePath string
|
||||
path string
|
||||
caseSensitive bool
|
||||
expected bool
|
||||
}{
|
||||
{"/", "/file", true, true},
|
||||
{"/a", "/file", true, false},
|
||||
{"/f", "/file", true, true},
|
||||
{"/f", "/File", true, false},
|
||||
{"/f", "/File", false, true},
|
||||
{"/file", "/file", true, true},
|
||||
{"/file", "/file", false, true},
|
||||
{"/files", "/file", false, false},
|
||||
{"/files", "/file", true, false},
|
||||
{"/folder", "/folder/file.txt", true, true},
|
||||
{"/folders", "/folder/file.txt", true, false},
|
||||
{"/folder", "/Folder/file.txt", false, true},
|
||||
{"/folders", "/Folder/file.txt", false, false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
CaseSensitivePath = test.caseSensitive
|
||||
valid := Path(test.path).Matches(test.basePath)
|
||||
if test.expected != valid {
|
||||
t.Errorf("Test %d: Expected %v, found %v", i, test.expected, valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathCaseSensitiveEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
envValue string
|
||||
expected bool
|
||||
}{
|
||||
{"1", true},
|
||||
{"0", false},
|
||||
{"false", false},
|
||||
{"true", true},
|
||||
{"", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
os.Setenv(caseSensitivePathEnv, test.envValue)
|
||||
initCaseSettings()
|
||||
if test.expected != CaseSensitivePath {
|
||||
t.Errorf("Test %d: Expected %v, found %v", i, test.expected, CaseSensitivePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Path represents a URI path.
|
||||
type Path string
|
||||
|
||||
// Matches checks to see if other matches p.
|
||||
//
|
||||
// Path matching will probably not always be a direct
|
||||
// comparison; this method assures that paths can be
|
||||
// easily and consistently matched.
|
||||
func (p Path) Matches(other string) bool {
|
||||
if CaseSensitivePath {
|
||||
return strings.HasPrefix(string(p), other)
|
||||
}
|
||||
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other))
|
||||
}
|
||||
|
||||
// PathMatcher is a Path RequestMatcher.
|
||||
type PathMatcher string
|
||||
|
||||
// Match satisfies RequestMatcher.
|
||||
func (p PathMatcher) Match(r *http.Request) bool {
|
||||
return Path(r.URL.Path).Matches(string(p))
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
const serverType = "http"
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&Host, "host", DefaultHost, "Default host")
|
||||
flag.StringVar(&Port, "port", DefaultPort, "Default port")
|
||||
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site")
|
||||
flag.DurationVar(&GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") // TODO
|
||||
flag.BoolVar(&HTTP2, "http2", true, "Use HTTP/2")
|
||||
flag.BoolVar(&QUIC, "quic", false, "Use experimental QUIC")
|
||||
|
||||
caddy.RegisterServerType(serverType, caddy.ServerType{
|
||||
Directives: directives,
|
||||
DefaultInput: func() caddy.Input {
|
||||
if Port == DefaultPort && Host != "" {
|
||||
// by leaving the port blank in this case we give auto HTTPS
|
||||
// a chance to set the port to 443 for us
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s\nroot %s", Host, Root)),
|
||||
ServerTypeName: serverType,
|
||||
}
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
|
||||
ServerTypeName: serverType,
|
||||
}
|
||||
},
|
||||
NewContext: newContext,
|
||||
})
|
||||
caddy.RegisterCaddyfileLoader("short", caddy.LoaderFunc(shortCaddyfileLoader))
|
||||
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
|
||||
caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS })
|
||||
}
|
||||
|
||||
func newContext() caddy.Context {
|
||||
return &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
}
|
||||
|
||||
type httpContext struct {
|
||||
// keysToSiteConfigs maps an address at the top of a
|
||||
// server block (a "key") to its SiteConfig. Not all
|
||||
// SiteConfigs will be represented here, only ones
|
||||
// that appeared in the Caddyfile.
|
||||
keysToSiteConfigs map[string]*SiteConfig
|
||||
|
||||
// siteConfigs is the master list of all site configs.
|
||||
siteConfigs []*SiteConfig
|
||||
}
|
||||
|
||||
func (h *httpContext) saveConfig(key string, cfg *SiteConfig) {
|
||||
h.siteConfigs = append(h.siteConfigs, cfg)
|
||||
h.keysToSiteConfigs[key] = cfg
|
||||
}
|
||||
|
||||
// InspectServerBlocks make sure that everything checks out before
|
||||
// executing directives and otherwise prepares the directives to
|
||||
// be parsed and executed.
|
||||
func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
// For each address in each server block, make a new config
|
||||
for _, sb := range serverBlocks {
|
||||
for _, key := range sb.Keys {
|
||||
key = strings.ToLower(key)
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site address: %s", key)
|
||||
}
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return serverBlocks, err
|
||||
}
|
||||
|
||||
// Fill in address components from command line so that middleware
|
||||
// have access to the correct information during setup
|
||||
if addr.Host == "" && Host != DefaultHost {
|
||||
addr.Host = Host
|
||||
}
|
||||
if addr.Port == "" && Port != DefaultPort {
|
||||
addr.Port = Port
|
||||
}
|
||||
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &SiteConfig{
|
||||
Addr: addr,
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{Hostname: addr.Host},
|
||||
HiddenFiles: []string{sourceFile},
|
||||
}
|
||||
h.saveConfig(key, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// For sites that have gzip (which gets chained in
|
||||
// before the error handler) we should ensure that the
|
||||
// errors directive also appears so error pages aren't
|
||||
// written after the gzip writer is closed. See #616.
|
||||
for _, sb := range serverBlocks {
|
||||
_, hasGzip := sb.Tokens["gzip"]
|
||||
_, hasErrors := sb.Tokens["errors"]
|
||||
if hasGzip && !hasErrors {
|
||||
sb.Tokens["errors"] = []caddyfile.Token{{Text: "errors"}}
|
||||
}
|
||||
}
|
||||
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// MakeServers uses the newly-created siteConfigs to
|
||||
// create and return a list of server instances.
|
||||
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
// make sure TLS is disabled for explicitly-HTTP sites
|
||||
// (necessary when HTTP address shares a block containing tls)
|
||||
for _, cfg := range h.siteConfigs {
|
||||
if !cfg.TLS.Enabled {
|
||||
continue
|
||||
}
|
||||
if cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http" {
|
||||
cfg.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
|
||||
} else if cfg.Addr.Scheme == "" {
|
||||
// set scheme to https ourselves, since TLS is enabled
|
||||
// and it was not explicitly set to something else. this
|
||||
// makes it appear as "https" when we print the list of
|
||||
// running sites; otherwise "http" would be assumed which
|
||||
// is incorrect for this site.
|
||||
cfg.Addr.Scheme = "https"
|
||||
}
|
||||
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.OnDemand) {
|
||||
// this is vital, otherwise the function call below that
|
||||
// sets the listener address will use the default port
|
||||
// instead of 443 because it doesn't know about TLS.
|
||||
cfg.Addr.Port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// we must map (group) each config to a bind address
|
||||
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// then we create a server for each group
|
||||
var servers []caddy.Server
|
||||
for addr, group := range groups {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// GetConfig gets the SiteConfig that corresponds to c.
|
||||
// If none exist (should only happen in tests), then a
|
||||
// new, empty one will be created.
|
||||
func GetConfig(c *caddy.Controller) *SiteConfig {
|
||||
ctx := c.Context().(*httpContext)
|
||||
if cfg, ok := ctx.keysToSiteConfigs[c.Key]; ok {
|
||||
return cfg
|
||||
}
|
||||
// we should only get here during tests because directive
|
||||
// actions typically skip the server blocks where we make
|
||||
// the configs
|
||||
ctx.saveConfig(c.Key, &SiteConfig{Root: Root, TLS: new(caddytls.Config)})
|
||||
return GetConfig(c)
|
||||
}
|
||||
|
||||
// shortCaddyfileLoader loads a Caddyfile if positional arguments are
|
||||
// detected, or, in other words, if un-named arguments are provided to
|
||||
// the program. A "short Caddyfile" is one in which each argument
|
||||
// is a line of the Caddyfile. The default host and port are prepended
|
||||
// according to the Host and Port values.
|
||||
func shortCaddyfileLoader(serverType string) (caddy.Input, error) {
|
||||
if flag.NArg() > 0 && serverType == "http" {
|
||||
confBody := fmt.Sprintf("%s:%s\n%s", Host, Port, strings.Join(flag.Args(), "\n"))
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(confBody),
|
||||
Filepath: "args",
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// groupSiteConfigsByListenAddr groups site configs by their listen
|
||||
// (bind) address, so sites that use the same listener can be served
|
||||
// on the same server instance. The return value maps the listen
|
||||
// address (what you pass into net.Listen) to the list of site configs.
|
||||
// This function does NOT vet the configs to ensure they are compatible.
|
||||
func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConfig, error) {
|
||||
groups := make(map[string][]*SiteConfig)
|
||||
|
||||
for _, conf := range configs {
|
||||
// We would add a special case here so that localhost addresses
|
||||
// bind to 127.0.0.1 if conf.ListenHost is not already set, which
|
||||
// would prevent outsiders from even connecting; but that was problematic:
|
||||
// https://forum.caddyserver.com/t/wildcard-virtual-domains-with-wildcard-roots/221/5?u=matt
|
||||
|
||||
if conf.Addr.Port == "" {
|
||||
conf.Addr.Port = Port
|
||||
}
|
||||
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Addr.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrstr := addr.String()
|
||||
groups[addrstr] = append(groups[addrstr], conf)
|
||||
}
|
||||
|
||||
return groups, 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.
|
||||
type Address struct {
|
||||
Original, Scheme, Host, Port, Path string
|
||||
}
|
||||
|
||||
// String returns a human-friendly print of the address.
|
||||
func (a Address) String() string {
|
||||
if a.Host == "" && a.Port == "" {
|
||||
return ""
|
||||
}
|
||||
scheme := a.Scheme
|
||||
if scheme == "" {
|
||||
if a.Port == "443" {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
}
|
||||
s := scheme
|
||||
if s != "" {
|
||||
s += "://"
|
||||
}
|
||||
s += a.Host
|
||||
if a.Port != "" &&
|
||||
((scheme == "https" && a.Port != "443") ||
|
||||
(scheme == "http" && a.Port != "80")) {
|
||||
s += ":" + a.Port
|
||||
}
|
||||
if a.Path != "" {
|
||||
s += a.Path
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// VHost returns a sensible concatenation of Host:Port/Path from a.
|
||||
// It's basically the a.Original but without the scheme.
|
||||
func (a Address) VHost() string {
|
||||
if idx := strings.Index(a.Original, "://"); idx > -1 {
|
||||
return a.Original[idx+3:]
|
||||
}
|
||||
return a.Original
|
||||
}
|
||||
|
||||
// standardizeAddress parses an address string into a structured format with separate
|
||||
// scheme, host, port, and path portions, as well as the original input string.
|
||||
func standardizeAddress(str string) (Address, error) {
|
||||
input := str
|
||||
|
||||
// Split input into components (prepend with // to assert host 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 = "80"
|
||||
} else if u.Scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// repeated or conflicting scheme is confusing, so error
|
||||
if u.Scheme != "" && (port == "http" || port == "https") {
|
||||
return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (u.Scheme == "http" && port == "443") || (u.Scheme == "https" && port == "80") {
|
||||
return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
// standardize http and https ports to their respective port numbers
|
||||
if port == "http" {
|
||||
u.Scheme = "http"
|
||||
port = "80"
|
||||
} else if port == "https" {
|
||||
u.Scheme = "https"
|
||||
port = "443"
|
||||
}
|
||||
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
|
||||
}
|
||||
|
||||
// directives is the list of all directives known to exist for the
|
||||
// http server type, including non-standard (3rd-party) directives.
|
||||
// The ordering of this list is important.
|
||||
var directives = []string{
|
||||
// primitive actions that set up the fundamental vitals of each config
|
||||
"root",
|
||||
"tls",
|
||||
"bind",
|
||||
|
||||
// services/utilities, or other directives that don't necessarily inject handlers
|
||||
"startup",
|
||||
"shutdown",
|
||||
"realip", // github.com/captncraig/caddy-realip
|
||||
"git", // github.com/abiosoft/caddy-git
|
||||
|
||||
// directives that add middleware to the stack
|
||||
"log",
|
||||
"rewrite",
|
||||
"ext",
|
||||
"gzip",
|
||||
"locale", // github.com/simia-tech/caddy-locale
|
||||
"errors",
|
||||
"minify", // github.com/hacdias/caddy-minify
|
||||
"ipfilter", // github.com/pyed/ipfilter
|
||||
"search", // github.com/pedronasser/caddy-search
|
||||
"header",
|
||||
"redir",
|
||||
"cors", // github.com/captncraig/cors/caddy
|
||||
"mime",
|
||||
"basicauth",
|
||||
"jwt", // github.com/BTBurke/caddy-jwt
|
||||
"jsonp", // github.com/pschlump/caddy-jsonp
|
||||
"upload", // blitznote.com/src/caddy.upload
|
||||
"internal",
|
||||
"pprof",
|
||||
"expvar",
|
||||
"proxy",
|
||||
"fastcgi",
|
||||
"websocket",
|
||||
"markdown",
|
||||
"templates",
|
||||
"browse",
|
||||
"filemanager", // github.com/hacdias/caddy-filemanager
|
||||
"hugo", // github.com/hacdias/caddy-hugo
|
||||
"mailout", // github.com/SchumacherFM/mailout
|
||||
"prometheus", // github.com/miekg/caddy-prometheus
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultHost is the default host.
|
||||
DefaultHost = ""
|
||||
// DefaultPort is the default port.
|
||||
DefaultPort = "2015"
|
||||
// DefaultRoot is the default root folder.
|
||||
DefaultRoot = "."
|
||||
)
|
||||
|
||||
// These "soft defaults" are configurable by
|
||||
// command line flags, etc.
|
||||
var (
|
||||
// Root is the site root
|
||||
Root = DefaultRoot
|
||||
|
||||
// Host is the site host
|
||||
Host = DefaultHost
|
||||
|
||||
// Port is the site port
|
||||
Port = DefaultPort
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
|
||||
// HTTP2 indicates whether HTTP2 is enabled or not.
|
||||
HTTP2 bool
|
||||
|
||||
// QUIC indicates whether QUIC is enabled or not.
|
||||
QUIC bool
|
||||
)
|
||||
@@ -0,0 +1,139 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
func TestStandardizeAddress(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},
|
||||
{`localhost:http`, "http", "localhost", "80", "", false},
|
||||
{`localhost:https`, "https", "localhost", "443", "", false},
|
||||
{`:http`, "http", "", "80", "", false},
|
||||
{`:https`, "https", "", "443", "", false},
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", "", false},
|
||||
{`https://localhost`, "https", "localhost", "443", "", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false},
|
||||
{`http://[::1]`, "http", "::1", "80", "", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", "", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
|
||||
{``, "", "", "", "", false},
|
||||
{`::1`, "", "::1", "", "", true},
|
||||
{`localhost::`, "", "localhost::", "", "", true},
|
||||
{`#$%@`, "", "", "", "", true},
|
||||
{`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},
|
||||
{`host:https/path`, "https", "host", "443", "/path", false},
|
||||
{`/path`, "", "", "", "/path", false},
|
||||
} {
|
||||
actual, err := standardizeAddress(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 TestAddressVHost(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
addr Address
|
||||
expected string
|
||||
}{
|
||||
{Address{Original: "host:1234"}, "host:1234"},
|
||||
{Address{Original: "host:1234/foo"}, "host:1234/foo"},
|
||||
{Address{Original: "host/foo"}, "host/foo"},
|
||||
{Address{Original: "http://host/foo"}, "host/foo"},
|
||||
{Address{Original: "https://host/foo"}, "host/foo"},
|
||||
} {
|
||||
actual := test.addr.VHost()
|
||||
if actual != test.expected {
|
||||
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
|
||||
Port = "9999"
|
||||
filename := "Testfile"
|
||||
ctx := newContext().(*httpContext)
|
||||
input := strings.NewReader(`localhost`)
|
||||
sblocks, err := caddyfile.Parse(filename, input, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error setting up test, got: %v", err)
|
||||
}
|
||||
_, err = ctx.InspectServerBlocks(filename, sblocks)
|
||||
if err != nil {
|
||||
t.Fatalf("Didn't expect an error, but got: %v", err)
|
||||
}
|
||||
addr := ctx.keysToSiteConfigs["localhost"].Addr
|
||||
if addr.Port != Port {
|
||||
t.Errorf("Expected the port on the address to be set, but got: %#v", addr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResponseRecorder is a type of http.ResponseWriter that captures
|
||||
// the status code written to it and also the size of the body
|
||||
// written in the response. A status code does not have
|
||||
// to be written, however, in which case 200 must be assumed.
|
||||
// It is best to have the constructor initialize this type
|
||||
// with that default status code.
|
||||
//
|
||||
// Setting the Replacer field allows middlewares to type-assert
|
||||
// the http.ResponseWriter to ResponseRecorder and set their own
|
||||
// placeholder values for logging utilities to use.
|
||||
//
|
||||
// Beware when accessing the Replacer value; it may be nil!
|
||||
type ResponseRecorder struct {
|
||||
http.ResponseWriter
|
||||
Replacer Replacer
|
||||
status int
|
||||
size int
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// NewResponseRecorder makes and returns a new responseRecorder,
|
||||
// which captures the HTTP Status code from the ResponseWriter
|
||||
// and also the length of the response body written through it.
|
||||
// Because a status is not set unless WriteHeader is called
|
||||
// explicitly, this constructor initializes with a status code
|
||||
// of 200 to cover the default case.
|
||||
func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
|
||||
return &ResponseRecorder{
|
||||
ResponseWriter: w,
|
||||
status: http.StatusOK,
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHeader records the status code and calls the
|
||||
// underlying ResponseWriter's WriteHeader method.
|
||||
func (r *ResponseRecorder) WriteHeader(status int) {
|
||||
r.status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write is a wrapper that records the size of the body
|
||||
// that gets written.
|
||||
func (r *ResponseRecorder) Write(buf []byte) (int, error) {
|
||||
n, err := r.ResponseWriter.Write(buf)
|
||||
if err == nil {
|
||||
r.size += n
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Size is a Getter to size property
|
||||
func (r *ResponseRecorder) Size() int {
|
||||
return r.size
|
||||
}
|
||||
|
||||
// Status is a Getter to status property
|
||||
func (r *ResponseRecorder) Status() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||
func (r *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, errors.New("not a Hijacker")
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or does nothing.
|
||||
func (r *ResponseRecorder) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic("not a Flusher") // should be recovered at the beginning of middleware stack
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
func (r *ResponseRecorder) CloseNotify() <-chan bool {
|
||||
if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic("not a CloseNotifier")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -0,0 +1,190 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// requestReplacer is a strings.Replacer which is used to
|
||||
// encode literal \r and \n characters and keep everything
|
||||
// on one line
|
||||
var requestReplacer = strings.NewReplacer(
|
||||
"\r", "\\r",
|
||||
"\n", "\\n",
|
||||
)
|
||||
|
||||
// Replacer is a type which can replace placeholder
|
||||
// substrings in a string with actual values from a
|
||||
// http.Request and ResponseRecorder. Always use
|
||||
// NewReplacer to get one of these. Any placeholders
|
||||
// made with Set() should overwrite existing values if
|
||||
// the key is already used.
|
||||
type Replacer interface {
|
||||
Replace(string) string
|
||||
Set(key, value string)
|
||||
}
|
||||
|
||||
// replacer implements Replacer. customReplacements
|
||||
// is used to store custom replacements created with
|
||||
// Set() until the time of replacement, at which point
|
||||
// they will be used to overwrite other replacements
|
||||
// if there is a name conflict.
|
||||
type replacer struct {
|
||||
replacements map[string]string
|
||||
customReplacements map[string]string
|
||||
emptyValue string
|
||||
responseRecorder *ResponseRecorder
|
||||
}
|
||||
|
||||
// NewReplacer makes a new replacer based on r and rr which
|
||||
// are used for request and response placeholders, respectively.
|
||||
// Request placeholders are created immediately, whereas
|
||||
// response placeholders are not created until Replace()
|
||||
// is invoked. rr may be nil if it is not available.
|
||||
// emptyValue should be the string that is used in place
|
||||
// of empty string (can still be empty string).
|
||||
func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Replacer {
|
||||
rep := &replacer{
|
||||
responseRecorder: rr,
|
||||
customReplacements: make(map[string]string),
|
||||
replacements: map[string]string{
|
||||
"{method}": r.Method,
|
||||
"{scheme}": func() string {
|
||||
if r.TLS != nil {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}(),
|
||||
"{hostname}": func() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
}(),
|
||||
"{host}": r.Host,
|
||||
"{hostonly}": func() string {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
return r.Host
|
||||
}
|
||||
return host
|
||||
}(),
|
||||
"{path}": r.URL.Path,
|
||||
"{path_escaped}": url.QueryEscape(r.URL.Path),
|
||||
"{query}": r.URL.RawQuery,
|
||||
"{query_escaped}": url.QueryEscape(r.URL.RawQuery),
|
||||
"{fragment}": r.URL.Fragment,
|
||||
"{proto}": r.Proto,
|
||||
"{remote}": func() string {
|
||||
if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
|
||||
return fwdFor
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}(),
|
||||
"{port}": func() string {
|
||||
_, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return port
|
||||
}(),
|
||||
"{uri}": r.URL.RequestURI(),
|
||||
"{uri_escaped}": url.QueryEscape(r.URL.RequestURI()),
|
||||
"{when}": time.Now().Format(timeFormat),
|
||||
"{file}": func() string {
|
||||
_, file := path.Split(r.URL.Path)
|
||||
return file
|
||||
}(),
|
||||
"{dir}": func() string {
|
||||
dir, _ := path.Split(r.URL.Path)
|
||||
return dir
|
||||
}(),
|
||||
"{request}": func() string {
|
||||
dump, err := httputil.DumpRequest(r, false)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return requestReplacer.Replace(string(dump))
|
||||
}(),
|
||||
},
|
||||
emptyValue: emptyValue,
|
||||
}
|
||||
|
||||
// Header placeholders (case-insensitive)
|
||||
for header, values := range r.Header {
|
||||
rep.replacements[headerReplacer+strings.ToLower(header)+"}"] = strings.Join(values, ",")
|
||||
}
|
||||
|
||||
return rep
|
||||
}
|
||||
|
||||
// Replace performs a replacement of values on s and returns
|
||||
// the string with the replaced values.
|
||||
func (r *replacer) Replace(s string) string {
|
||||
// Do not attempt replacements if no placeholder is found.
|
||||
if !strings.ContainsAny(s, "{}") {
|
||||
return s
|
||||
}
|
||||
|
||||
// Make response placeholders now
|
||||
if r.responseRecorder != nil {
|
||||
r.replacements["{status}"] = strconv.Itoa(r.responseRecorder.status)
|
||||
r.replacements["{size}"] = strconv.Itoa(r.responseRecorder.size)
|
||||
r.replacements["{latency}"] = time.Since(r.responseRecorder.start).String()
|
||||
}
|
||||
|
||||
// Include custom placeholders, overwriting existing ones if necessary
|
||||
for key, val := range r.customReplacements {
|
||||
r.replacements[key] = val
|
||||
}
|
||||
|
||||
// Header replacements - these are case-insensitive, so we can't just use strings.Replace()
|
||||
for strings.Contains(s, headerReplacer) {
|
||||
idxStart := strings.Index(s, headerReplacer)
|
||||
endOffset := idxStart + len(headerReplacer)
|
||||
idxEnd := strings.Index(s[endOffset:], "}")
|
||||
if idxEnd > -1 {
|
||||
placeholder := strings.ToLower(s[idxStart : endOffset+idxEnd+1])
|
||||
replacement := r.replacements[placeholder]
|
||||
if replacement == "" {
|
||||
replacement = r.emptyValue
|
||||
}
|
||||
s = s[:idxStart] + replacement + s[endOffset+idxEnd+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Regular replacements - these are easier because they're case-sensitive
|
||||
for placeholder, replacement := range r.replacements {
|
||||
if replacement == "" {
|
||||
replacement = r.emptyValue
|
||||
}
|
||||
s = strings.Replace(s, placeholder, replacement, -1)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Set sets key to value in the r.customReplacements map.
|
||||
func (r *replacer) Set(key, value string) {
|
||||
r.customReplacements["{"+key+"}"] = value
|
||||
}
|
||||
|
||||
const (
|
||||
timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
headerReplacer = "{>"
|
||||
)
|
||||
@@ -0,0 +1,131 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewReplacer(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
if err != nil {
|
||||
t.Fatal("Request Formation Failed\n")
|
||||
}
|
||||
rep := NewReplacer(request, recordRequest, "")
|
||||
|
||||
switch v := rep.(type) {
|
||||
case *replacer:
|
||||
if v.replacements["{host}"] != "localhost" {
|
||||
t.Error("Expected host to be localhost")
|
||||
}
|
||||
if v.replacements["{method}"] != "POST" {
|
||||
t.Error("Expected request method to be POST")
|
||||
}
|
||||
|
||||
// Response placeholders should only be set after call to Replace()
|
||||
if got, want := v.replacements["{status}"], ""; got != want {
|
||||
t.Errorf("Expected status to NOT be set before Replace() is called; was: %s", got)
|
||||
}
|
||||
rep.Replace("{foobar}")
|
||||
if got, want := v.replacements["{status}"], "200"; got != want {
|
||||
t.Errorf("Expected status to be %s, was: %s", want, got)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("Expected *replacer underlying Replacer type, got: %#v", rep)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
if err != nil {
|
||||
t.Fatal("Request Formation Failed\n")
|
||||
}
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to determine hostname\n")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
{"This hostname is {hostname}", "This hostname is " + hostname},
|
||||
{"This host is {host}.", "This host is localhost."},
|
||||
{"This request method is {method}.", "This request method is POST."},
|
||||
{"The response status is {status}.", "The response status is 200."},
|
||||
{"The Custom header is {>Custom}.", "The Custom header is foobarbaz."},
|
||||
{"The request is {request}.", "The request is POST / HTTP/1.1\\r\\nHost: localhost\\r\\nCustom: foobarbaz\\r\\nShorterval: 1\\r\\n\\r\\n."},
|
||||
{"The cUsToM header is {>cUsToM}...", "The cUsToM header is foobarbaz..."},
|
||||
{"The Non-Existent header is {>Non-Existent}.", "The Non-Existent header is -."},
|
||||
{"Bad {host placeholder...", "Bad {host placeholder..."},
|
||||
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
|
||||
{"Bad {>Custom placeholder {>ShorterVal}", "Bad -"},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
if expected, actual := c.expect, repl.Replace(c.template); expected != actual {
|
||||
t.Errorf("for template '%s', expected '%s', got '%s'", c.template, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
complexCases := []struct {
|
||||
template string
|
||||
replacements map[string]string
|
||||
expect string
|
||||
}{
|
||||
{"/a{1}/{2}", map[string]string{"{1}": "12", "{2}": ""}, "/a12/"},
|
||||
}
|
||||
|
||||
for _, c := range complexCases {
|
||||
repl := &replacer{
|
||||
replacements: c.replacements,
|
||||
}
|
||||
if expected, actual := c.expect, repl.Replace(c.template); expected != actual {
|
||||
t.Errorf("for template '%s', expected '%s', got '%s'", c.template, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Request Formation Failed \n")
|
||||
}
|
||||
repl := NewReplacer(request, recordRequest, "")
|
||||
|
||||
repl.Set("host", "getcaddy.com")
|
||||
repl.Set("method", "GET")
|
||||
repl.Set("status", "201")
|
||||
repl.Set("variable", "value")
|
||||
|
||||
if repl.Replace("This host is {host}") != "This host is getcaddy.com" {
|
||||
t.Error("Expected host replacement failed")
|
||||
}
|
||||
if repl.Replace("This request method is {method}") != "This request method is GET" {
|
||||
t.Error("Expected method replacement failed")
|
||||
}
|
||||
if repl.Replace("The response status is {status}") != "The response status is 201" {
|
||||
t.Error("Expected status replacement failed")
|
||||
}
|
||||
if repl.Replace("The value of variable is {variable}") != "The value of variable is value" {
|
||||
t.Error("Expected variable replacement failed")
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,36 @@
|
||||
package setup
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
func parseRoller(c *Controller) (*middleware.LogRoller, error) {
|
||||
// LogRoller implements a type that provides a rolling logger.
|
||||
type LogRoller struct {
|
||||
Filename string
|
||||
MaxSize int
|
||||
MaxAge int
|
||||
MaxBackups int
|
||||
LocalTime bool
|
||||
}
|
||||
|
||||
// GetLogWriter returns an io.Writer that writes to a rolling logger.
|
||||
func (l LogRoller) GetLogWriter() io.Writer {
|
||||
return &lumberjack.Logger{
|
||||
Filename: l.Filename,
|
||||
MaxSize: l.MaxSize,
|
||||
MaxAge: l.MaxAge,
|
||||
MaxBackups: l.MaxBackups,
|
||||
LocalTime: l.LocalTime,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRoller parses roller contents out of c.
|
||||
func ParseRoller(c *caddy.Controller) (*LogRoller, error) {
|
||||
var size, age, keep int
|
||||
// This is kind of a hack to support nested blocks:
|
||||
// As we are already in a block: either log or errors,
|
||||
@@ -31,7 +55,7 @@ func parseRoller(c *Controller) (*middleware.LogRoller, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &middleware.LogRoller{
|
||||
return &LogRoller{
|
||||
MaxSize: size,
|
||||
MaxAge: age,
|
||||
MaxBackups: keep,
|
||||
@@ -0,0 +1,403 @@
|
||||
// Package httpserver implements an HTTP server on top of Caddy.
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/h2quic"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// Server is the HTTP server implementation.
|
||||
type Server struct {
|
||||
Server *http.Server
|
||||
quicServer *h2quic.Server
|
||||
listener net.Listener
|
||||
listenerMu sync.Mutex
|
||||
sites []*SiteConfig
|
||||
connTimeout time.Duration // max time to wait for a connection before force stop
|
||||
connWg sync.WaitGroup // one increment per connection
|
||||
tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine
|
||||
vhosts *vhostTrie
|
||||
}
|
||||
|
||||
// ensure it satisfies the interface
|
||||
var _ caddy.GracefulServer = new(Server)
|
||||
|
||||
// NewServer creates a new Server instance that will listen on addr
|
||||
// and will serve the sites configured in group.
|
||||
func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
s := &Server{
|
||||
Server: &http.Server{
|
||||
Addr: addr,
|
||||
// TODO: Make these values configurable?
|
||||
// ReadTimeout: 2 * time.Minute,
|
||||
// WriteTimeout: 2 * time.Minute,
|
||||
// MaxHeaderBytes: 1 << 16,
|
||||
},
|
||||
vhosts: newVHostTrie(),
|
||||
sites: group,
|
||||
connTimeout: GracefulTimeout,
|
||||
}
|
||||
s.Server.Handler = s // this is weird, but whatever
|
||||
s.Server.ConnState = func(c net.Conn, cs http.ConnState) {
|
||||
if cs == http.StateIdle {
|
||||
s.listenerMu.Lock()
|
||||
// server stopped, close idle connection
|
||||
if s.listener == nil {
|
||||
c.Close()
|
||||
}
|
||||
s.listenerMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Disable HTTP/2 if desired
|
||||
if !HTTP2 {
|
||||
s.Server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
}
|
||||
|
||||
// Enable QUIC if desired
|
||||
if QUIC {
|
||||
s.quicServer = &h2quic.Server{Server: s.Server}
|
||||
s.Server.Handler = s.wrapWithSvcHeaders(s.Server.Handler)
|
||||
}
|
||||
|
||||
// We have to bound our wg with one increment
|
||||
// to prevent a "race condition" that is hard-coded
|
||||
// into sync.WaitGroup.Wait() - basically, an add
|
||||
// with a positive delta must be guaranteed to
|
||||
// occur before Wait() is called on the wg.
|
||||
// In a way, this kind of acts as a safety barrier.
|
||||
s.connWg.Add(1)
|
||||
|
||||
// Set up TLS configuration
|
||||
var tlsConfigs []*caddytls.Config
|
||||
var err error
|
||||
for _, site := range group {
|
||||
tlsConfigs = append(tlsConfigs, site.TLS)
|
||||
}
|
||||
s.Server.TLSConfig, err = caddytls.MakeTLSConfig(tlsConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compile custom middleware for every site (enables virtual hosting)
|
||||
for _, site := range group {
|
||||
stack := Handler(staticfiles.FileServer{Root: http.Dir(site.Root), Hide: site.HiddenFiles})
|
||||
for i := len(site.middleware) - 1; i >= 0; i-- {
|
||||
stack = site.middleware[i](stack)
|
||||
}
|
||||
site.middlewareChain = stack
|
||||
s.vhosts.Insert(site.Addr.VHost(), site)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) wrapWithSvcHeaders(previousHandler http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.quicServer.SetQuicHeaders(w.Header())
|
||||
previousHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen creates an active listener for s that can be
|
||||
// used to serve requests.
|
||||
func (s *Server) Listen() (net.Listener, error) {
|
||||
if s.Server == nil {
|
||||
return nil, fmt.Errorf("Server field is nil")
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", s.Server.Addr)
|
||||
if err != nil {
|
||||
var succeeded bool
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows has been known to keep sockets open even after closing the listeners.
|
||||
// Tests reveal this error case easily because they call Start() then Stop()
|
||||
// in succession. TODO: Better way to handle this? And why limit this to Windows?
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
ln, err = net.Listen("tcp", s.Server.Addr)
|
||||
if err == nil {
|
||||
succeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !succeeded {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Very important to return a concrete caddy.Listener
|
||||
// implementation for graceful restarts.
|
||||
return ln.(*net.TCPListener), nil
|
||||
}
|
||||
|
||||
// ListenPacket is a noop to implement the Server interface.
|
||||
func (s *Server) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
||||
|
||||
// Serve serves requests on ln. It blocks until ln is closed.
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
if tcpLn, ok := ln.(*net.TCPListener); ok {
|
||||
ln = tcpKeepAliveListener{TCPListener: tcpLn}
|
||||
}
|
||||
|
||||
ln = newGracefulListener(ln, &s.connWg)
|
||||
|
||||
s.listenerMu.Lock()
|
||||
s.listener = ln
|
||||
s.listenerMu.Unlock()
|
||||
|
||||
if s.Server.TLSConfig != nil {
|
||||
// Create TLS listener - note that we do not replace s.listener
|
||||
// with this TLS listener; tls.listener is unexported and does
|
||||
// not implement the File() method we need for graceful restarts
|
||||
// on POSIX systems.
|
||||
// TODO: Is this ^ still relevant anymore? Maybe we can now that it's a net.Listener...
|
||||
ln = tls.NewListener(ln, s.Server.TLSConfig)
|
||||
|
||||
// Rotate TLS session ticket keys
|
||||
s.tlsGovChan = caddytls.RotateSessionTicketKeys(s.Server.TLSConfig)
|
||||
}
|
||||
|
||||
if QUIC {
|
||||
go func() {
|
||||
err := s.quicServer.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] listening for QUIC connections: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err := s.Server.Serve(ln)
|
||||
if QUIC {
|
||||
s.quicServer.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ServePacket is a noop to implement the Server interface.
|
||||
func (s *Server) ServePacket(pc net.PacketConn) error { return nil }
|
||||
|
||||
// ServeHTTP is the entry point of all HTTP requests.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
// We absolutely need to be sure we stay alive up here,
|
||||
// even though, in theory, the errors middleware does this.
|
||||
if rec := recover(); rec != nil {
|
||||
log.Printf("[PANIC] %v", rec)
|
||||
DefaultErrorFunc(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
w.Header().Set("Server", "Caddy")
|
||||
|
||||
sanitizePath(r)
|
||||
|
||||
status, _ := s.serveHTTP(w, r)
|
||||
|
||||
// Fallback error response in case error handling wasn't chained in
|
||||
if status >= 400 {
|
||||
DefaultErrorFunc(w, r, status)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// strip out the port because it's not used in virtual
|
||||
// hosting; the port is irrelevant because each listener
|
||||
// is on a different port.
|
||||
hostname, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
hostname = r.Host
|
||||
}
|
||||
|
||||
// look up the virtualhost; if no match, serve error
|
||||
vhost, pathPrefix := s.vhosts.Match(hostname + r.URL.Path)
|
||||
|
||||
if vhost == nil {
|
||||
// check for ACME challenge even if vhost is nil;
|
||||
// could be a new host coming online soon
|
||||
if caddytls.HTTPChallengeHandler(w, r, caddytls.DefaultHTTPAlternatePort) {
|
||||
return 0, nil
|
||||
}
|
||||
// otherwise, log the error and write a message to the client
|
||||
remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteHost = r.RemoteAddr
|
||||
}
|
||||
WriteTextResponse(w, http.StatusNotFound, "No such site at "+s.Server.Addr)
|
||||
log.Printf("[INFO] %s - No such site at %s (Remote: %s, Referer: %s)",
|
||||
hostname, s.Server.Addr, remoteHost, r.Header.Get("Referer"))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// we still check for ACME challenge if the vhost exists,
|
||||
// because we must apply its HTTP challenge config settings
|
||||
if s.proxyHTTPChallenge(vhost, w, r) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// trim the path portion of the site address from the beginning of
|
||||
// the URL path, so a request to example.com/foo/blog on the site
|
||||
// defined as example.com/foo appears as /blog instead of /foo/blog.
|
||||
if pathPrefix != "/" {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, pathPrefix)
|
||||
if !strings.HasPrefix(r.URL.Path, "/") {
|
||||
r.URL.Path = "/" + r.URL.Path
|
||||
}
|
||||
}
|
||||
|
||||
return vhost.middlewareChain.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// proxyHTTPChallenge solves the ACME HTTP challenge if r is the HTTP
|
||||
// request for the challenge. If it is, and if the request has been
|
||||
// fulfilled (response written), true is returned; false otherwise.
|
||||
// If you don't have a vhost, just call the challenge handler directly.
|
||||
func (s *Server) proxyHTTPChallenge(vhost *SiteConfig, w http.ResponseWriter, r *http.Request) bool {
|
||||
if vhost.Addr.Port != caddytls.HTTPChallengePort {
|
||||
return false
|
||||
}
|
||||
if vhost.TLS != nil && vhost.TLS.Manual {
|
||||
return false
|
||||
}
|
||||
altPort := caddytls.DefaultHTTPAlternatePort
|
||||
if vhost.TLS != nil && vhost.TLS.AltHTTPPort != "" {
|
||||
altPort = vhost.TLS.AltHTTPPort
|
||||
}
|
||||
return caddytls.HTTPChallengeHandler(w, r, altPort)
|
||||
}
|
||||
|
||||
// Address returns the address s was assigned to listen on.
|
||||
func (s *Server) Address() string {
|
||||
return s.Server.Addr
|
||||
}
|
||||
|
||||
// Stop stops s gracefully (or forcefully after timeout) and
|
||||
// closes its listener.
|
||||
func (s *Server) Stop() (err error) {
|
||||
s.Server.SetKeepAlivesEnabled(false)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// force connections to close after timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.connWg.Done() // decrement our initial increment used as a barrier
|
||||
s.connWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for remaining connections to finish or
|
||||
// force them all to close after timeout
|
||||
select {
|
||||
case <-time.After(s.connTimeout):
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
// Close the listener now; this stops the server without delay
|
||||
s.listenerMu.Lock()
|
||||
if s.listener != nil {
|
||||
err = s.listener.Close()
|
||||
s.listener = nil
|
||||
}
|
||||
s.listenerMu.Unlock()
|
||||
|
||||
// Closing this signals any TLS governor goroutines to exit
|
||||
if s.tlsGovChan != nil {
|
||||
close(s.tlsGovChan)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// sanitizePath collapses any ./ ../ /// madness
|
||||
// which helps prevent path traversal attacks.
|
||||
// Note to middleware: use URL.RawPath If you need
|
||||
// the "original" URL.Path value.
|
||||
func sanitizePath(r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
return
|
||||
}
|
||||
cleanedPath := path.Clean(r.URL.Path)
|
||||
if cleanedPath == "." {
|
||||
r.URL.Path = "/"
|
||||
} else {
|
||||
if !strings.HasPrefix(cleanedPath, "/") {
|
||||
cleanedPath = "/" + cleanedPath
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(cleanedPath, "/") {
|
||||
cleanedPath = cleanedPath + "/"
|
||||
}
|
||||
r.URL.Path = cleanedPath
|
||||
}
|
||||
}
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming caddy.Quiet == false.
|
||||
func (s *Server) OnStartupComplete() {
|
||||
if caddy.Quiet {
|
||||
return
|
||||
}
|
||||
for _, site := range s.sites {
|
||||
output := site.Addr.String()
|
||||
if caddy.IsLoopback(s.Address()) && !caddy.IsLoopback(site.Addr.Host) {
|
||||
output += " (only accessible on this machine)"
|
||||
}
|
||||
fmt.Println(output)
|
||||
}
|
||||
}
|
||||
|
||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
// go away.
|
||||
//
|
||||
// Borrowed from the Go standard library.
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
// Accept accepts the connection with a keep-alive enabled.
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
// File implements caddy.Listener; it returns the underlying file of the listener.
|
||||
func (ln tcpKeepAliveListener) File() (*os.File, error) {
|
||||
return ln.TCPListener.File()
|
||||
}
|
||||
|
||||
// DefaultErrorFunc responds to an HTTP request with a simple description
|
||||
// of the specified HTTP status code.
|
||||
func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) {
|
||||
WriteTextResponse(w, status, fmt.Sprintf("%d %s\n", status, http.StatusText(status)))
|
||||
}
|
||||
|
||||
// WriteTextResponse writes body with code status to w. The body will
|
||||
// be interpreted as plain text.
|
||||
func WriteTextResponse(w http.ResponseWriter, status int, body string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(body))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddress(t *testing.T) {
|
||||
addr := "127.0.0.1:9005"
|
||||
srv := &Server{Server: &http.Server{Addr: addr}}
|
||||
|
||||
if got, want := srv.Address(), addr; got != want {
|
||||
t.Errorf("Expected '%s' but got '%s'", want, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package httpserver
|
||||
|
||||
import "github.com/mholt/caddy/caddytls"
|
||||
|
||||
// SiteConfig contains information about a site
|
||||
// (also known as a virtual host).
|
||||
type SiteConfig struct {
|
||||
// The address of the site
|
||||
Addr Address
|
||||
|
||||
// The hostname to bind listener to;
|
||||
// defaults to Addr.Host
|
||||
ListenHost string
|
||||
|
||||
// TLS configuration
|
||||
TLS *caddytls.Config
|
||||
|
||||
// Uncompiled middleware stack
|
||||
middleware []Middleware
|
||||
|
||||
// Compiled middleware stack
|
||||
middlewareChain Handler
|
||||
|
||||
// Directory from which to serve files
|
||||
Root string
|
||||
|
||||
// A list of files to hide (for example, the
|
||||
// source Caddyfile). TODO: Enforcing this
|
||||
// should be centralized, for example, a
|
||||
// standardized way of loading files from disk
|
||||
// for a request.
|
||||
HiddenFiles []string
|
||||
}
|
||||
|
||||
// AddMiddleware adds a middleware to a site's middleware stack.
|
||||
func (s *SiteConfig) AddMiddleware(m Middleware) {
|
||||
s.middleware = append(s.middleware, m)
|
||||
}
|
||||
|
||||
// TLSConfig returns s.TLS.
|
||||
func (s SiteConfig) TLSConfig() *caddytls.Config {
|
||||
return s.TLS
|
||||
}
|
||||
|
||||
// Host returns s.Addr.Host.
|
||||
func (s SiteConfig) Host() string {
|
||||
return s.Addr.Host
|
||||
}
|
||||
|
||||
// Port returns s.Addr.Port.
|
||||
func (s SiteConfig) Port() string {
|
||||
return s.Addr.Port
|
||||
}
|
||||
|
||||
// Middleware returns s.middleware (useful for tests).
|
||||
func (s SiteConfig) Middleware() []Middleware {
|
||||
return s.middleware
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// vhostTrie facilitates virtual hosting. It matches
|
||||
// requests first by hostname (with support for
|
||||
// wildcards as TLS certificates support them), then
|
||||
// by longest matching path.
|
||||
type vhostTrie struct {
|
||||
edges map[string]*vhostTrie
|
||||
site *SiteConfig // site to match on this node; also known as a virtual host
|
||||
path string // the path portion of the key for the associated site
|
||||
}
|
||||
|
||||
// newVHostTrie returns a new vhostTrie.
|
||||
func newVHostTrie() *vhostTrie {
|
||||
return &vhostTrie{edges: make(map[string]*vhostTrie)}
|
||||
}
|
||||
|
||||
// Insert adds stack to t keyed by key. The key should be
|
||||
// a valid "host/path" combination (or just host).
|
||||
func (t *vhostTrie) Insert(key string, site *SiteConfig) {
|
||||
host, path := t.splitHostPath(key)
|
||||
if _, ok := t.edges[host]; !ok {
|
||||
t.edges[host] = newVHostTrie()
|
||||
}
|
||||
t.edges[host].insertPath(path, path, site)
|
||||
}
|
||||
|
||||
// insertPath expects t to be a host node (not a root node),
|
||||
// and inserts site into the t according to remainingPath.
|
||||
func (t *vhostTrie) insertPath(remainingPath, originalPath string, site *SiteConfig) {
|
||||
if remainingPath == "" {
|
||||
t.site = site
|
||||
t.path = originalPath
|
||||
return
|
||||
}
|
||||
ch := string(remainingPath[0])
|
||||
if _, ok := t.edges[ch]; !ok {
|
||||
t.edges[ch] = newVHostTrie()
|
||||
}
|
||||
t.edges[ch].insertPath(remainingPath[1:], originalPath, site)
|
||||
}
|
||||
|
||||
// Match returns the virtual host (site) in v with
|
||||
// the closest match to key. If there was a match,
|
||||
// it returns the SiteConfig and the path portion of
|
||||
// the key used to make the match. The matched path
|
||||
// would be a prefix of the path portion of the
|
||||
// key, if not the whole path portion of the key.
|
||||
// If there is no match, nil and empty string will
|
||||
// be returned.
|
||||
//
|
||||
// A typical key will be in the form "host" or "host/path".
|
||||
func (t *vhostTrie) Match(key string) (*SiteConfig, string) {
|
||||
host, path := t.splitHostPath(key)
|
||||
// try the given host, then, if no match, try wildcard hosts
|
||||
branch := t.matchHost(host)
|
||||
if branch == nil {
|
||||
branch = t.matchHost("0.0.0.0")
|
||||
}
|
||||
if branch == nil {
|
||||
branch = t.matchHost("")
|
||||
}
|
||||
if branch == nil {
|
||||
return nil, ""
|
||||
}
|
||||
node := branch.matchPath(path)
|
||||
if node == nil {
|
||||
return nil, ""
|
||||
}
|
||||
return node.site, node.path
|
||||
}
|
||||
|
||||
// matchHost returns the vhostTrie matching host. The matching
|
||||
// algorithm is the same as used to match certificates to host
|
||||
// with SNI during TLS handshakes. In other words, it supports,
|
||||
// to some degree, the use of wildcard (*) characters.
|
||||
func (t *vhostTrie) matchHost(host string) *vhostTrie {
|
||||
// try exact match
|
||||
if subtree, ok := t.edges[host]; ok {
|
||||
return subtree
|
||||
}
|
||||
|
||||
// then try replacing labels in the host
|
||||
// with wildcards until we get a match
|
||||
labels := strings.Split(host, ".")
|
||||
for i := range labels {
|
||||
labels[i] = "*"
|
||||
candidate := strings.Join(labels, ".")
|
||||
if subtree, ok := t.edges[candidate]; ok {
|
||||
return subtree
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchPath traverses t until it finds the longest key matching
|
||||
// remainingPath, and returns its node.
|
||||
func (t *vhostTrie) matchPath(remainingPath string) *vhostTrie {
|
||||
var longestMatch *vhostTrie
|
||||
for len(remainingPath) > 0 {
|
||||
ch := string(remainingPath[0])
|
||||
next, ok := t.edges[ch]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if next.site != nil {
|
||||
longestMatch = next
|
||||
}
|
||||
t = next
|
||||
remainingPath = remainingPath[1:]
|
||||
}
|
||||
return longestMatch
|
||||
}
|
||||
|
||||
// splitHostPath separates host from path in key.
|
||||
func (t *vhostTrie) splitHostPath(key string) (host, path string) {
|
||||
parts := strings.SplitN(key, "/", 2)
|
||||
host, path = strings.ToLower(parts[0]), "/"
|
||||
if len(parts) > 1 {
|
||||
path += parts[1]
|
||||
}
|
||||
// strip out the port (if present) from the host, since
|
||||
// each port has its own socket, and each socket has its
|
||||
// own listener, and each listener has its own server
|
||||
// instance, and each server instance has its own vhosts.
|
||||
// removing the port is a simple way to standardize so
|
||||
// when requests come in, we can be sure to get a match.
|
||||
hostname, _, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
host = hostname
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// String returns a list of all the entries in t; assumes that
|
||||
// t is a root node.
|
||||
func (t *vhostTrie) String() string {
|
||||
var s string
|
||||
for host, edge := range t.edges {
|
||||
s += edge.str(host)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *vhostTrie) str(prefix string) string {
|
||||
var s string
|
||||
for key, edge := range t.edges {
|
||||
if edge.site != nil {
|
||||
s += prefix + key + "\n"
|
||||
}
|
||||
s += edge.str(prefix + key)
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVHostTrie(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"example",
|
||||
"example.com",
|
||||
"*.example.com",
|
||||
"example.com/foo",
|
||||
"example.com/foo/bar",
|
||||
"*.example.com/test",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"not-in-trie.com", false, "", "/"},
|
||||
{"example", true, "example", "/"},
|
||||
{"example.com", true, "example.com", "/"},
|
||||
{"example.com/test", true, "example.com", "/"},
|
||||
{"example.com/foo", true, "example.com/foo", "/foo"},
|
||||
{"example.com/foo/", true, "example.com/foo", "/foo"},
|
||||
{"EXAMPLE.COM/foo", true, "example.com/foo", "/foo"},
|
||||
{"EXAMPLE.COM/Foo", true, "example.com", "/"},
|
||||
{"example.com/foo/bar", true, "example.com/foo/bar", "/foo/bar"},
|
||||
{"example.com/foo/bar/baz", true, "example.com/foo/bar", "/foo/bar"},
|
||||
{"example.com/foo/other", true, "example.com/foo", "/foo"},
|
||||
{"foo.example.com", true, "*.example.com", "/"},
|
||||
{"foo.example.com/else", true, "*.example.com", "/"},
|
||||
}, false)
|
||||
}
|
||||
|
||||
func TestVHostTrieWildcard1(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"example.com",
|
||||
"",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"not-in-trie.com", true, "", "/"},
|
||||
{"example.com", true, "example.com", "/"},
|
||||
{"example.com/foo", true, "example.com", "/"},
|
||||
{"not-in-trie.com/asdf", true, "", "/"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func TestVHostTrieWildcard2(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"0.0.0.0/asdf",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"example.com/asdf/foo", true, "0.0.0.0/asdf", "/asdf"},
|
||||
{"example.com/foo", false, "", "/"},
|
||||
{"host/asdf", true, "0.0.0.0/asdf", "/asdf"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func TestVHostTrieWildcard3(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"*/foo",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"example.com/foo", true, "*/foo", "/foo"},
|
||||
{"example.com", false, "", "/"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func TestVHostTriePort(t *testing.T) {
|
||||
// Make sure port is stripped out
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"example.com:1234",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"example.com/foo", true, "example.com:1234", "/"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func populateTestTrie(trie *vhostTrie, keys []string) {
|
||||
for _, key := range keys {
|
||||
// we wrap this in a func, passing in the key, otherwise the
|
||||
// handler always writes the last key to the response, even
|
||||
// if the handler is actually from one of the earlier keys.
|
||||
func(key string) {
|
||||
site := &SiteConfig{
|
||||
middlewareChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Write([]byte(key))
|
||||
return 0, nil
|
||||
}),
|
||||
}
|
||||
trie.Insert(key, site)
|
||||
}(key)
|
||||
}
|
||||
}
|
||||
|
||||
type vhostTrieTest struct {
|
||||
query string
|
||||
expectMatch bool
|
||||
expectedKey string
|
||||
matchedPrefix string // the path portion of a key that is expected to be matched
|
||||
}
|
||||
|
||||
func assertTestTrie(t *testing.T, trie *vhostTrie, tests []vhostTrieTest, hasWildcardHosts bool) {
|
||||
for i, test := range tests {
|
||||
site, pathPrefix := trie.Match(test.query)
|
||||
|
||||
if !test.expectMatch {
|
||||
if site != nil {
|
||||
// If not expecting a value, then just make sure we didn't get one
|
||||
t.Errorf("Test %d: Expected no matches, but got %v", i, site)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, we must assert we got a value
|
||||
if site == nil {
|
||||
t.Errorf("Test %d: Expected non-nil return value, but got: %v", i, site)
|
||||
continue
|
||||
}
|
||||
|
||||
// And it must be the correct value
|
||||
resp := httptest.NewRecorder()
|
||||
site.middlewareChain.ServeHTTP(resp, nil)
|
||||
actualHandlerKey := resp.Body.String()
|
||||
if actualHandlerKey != test.expectedKey {
|
||||
t.Errorf("Test %d: Expected match '%s' but matched '%s'",
|
||||
i, test.expectedKey, actualHandlerKey)
|
||||
}
|
||||
|
||||
// The path prefix must also be correct
|
||||
if test.matchedPrefix != pathPrefix {
|
||||
t.Errorf("Test %d: Expected matched path prefix to be '%s', got '%s'",
|
||||
i, test.matchedPrefix, pathPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
// Package inner provides a simple middleware that (a) prevents access
|
||||
// Package internalsrv provides a simple middleware that (a) prevents access
|
||||
// to internal locations and (b) allows to return files from internal location
|
||||
// by setting a special header, e.g. in a proxy response.
|
||||
package inner
|
||||
//
|
||||
// The package is named internalsrv so as not to conflict with Go tooling
|
||||
// convention which treats folders called "internal" differently.
|
||||
package internalsrv
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Internal middleware protects internal locations from external requests -
|
||||
// but allows access from the inside by using a special HTTP header.
|
||||
type Internal struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Paths []string
|
||||
}
|
||||
|
||||
@@ -25,12 +28,12 @@ func isInternalRedirect(w http.ResponseWriter) bool {
|
||||
return w.Header().Get(redirectHeader) != ""
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middlware.Handler interface.
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (i Internal) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
// Internal location requested? -> Not found.
|
||||
for _, prefix := range i.Paths {
|
||||
if middleware.Path(r.URL.Path).Matches(prefix) {
|
||||
if httpserver.Path(r.URL.Path).Matches(prefix) {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user